protontool/cli/
mod.rs

1//! Command-line interface for protontool.
2//!
3//! Handles argument parsing and dispatches to appropriate modes:
4//! - GUI mode for interactive prefix management
5//! - Verb execution for installing components
6//! - Prefix creation/deletion
7//! - Running commands with Wine environment
8
9pub mod util;
10
11use std::env;
12use std::path::PathBuf;
13use std::process;
14
15use crate::cli::util::{enable_logging, exit_with_error, ArgParser};
16use crate::gui::{
17    get_prefix_name_gui, prompt_filesystem_access, select_custom_prefix_gui,
18    select_prefix_location_gui, select_proton_with_gui, select_steam_app_with_gui,
19    select_steam_installation, select_steam_library_paths, select_verb_category_gui,
20    select_verbs_with_gui, show_main_menu_gui, GuiAction,
21};
22use crate::steam::{
23    find_proton_app, find_proton_by_name, find_steam_installations, get_proton_apps,
24    get_steam_apps, get_steam_lib_paths,
25};
26use crate::util::output_to_string;
27use crate::wine::Wine;
28
29/// Main CLI entry point. Parses arguments and dispatches to appropriate handler.
30/// If `args` is None, uses command-line arguments from env::args().
31pub fn main_cli(args: Option<Vec<String>>) {
32    let args = args.unwrap_or_else(|| env::args().skip(1).collect());
33
34    let mut parser = ArgParser::new(
35        "protontool",
36        "A tool for managing Wine/Proton prefixes with built-in component installation.\n\n\
37         Usage:\n\n\
38         Install components (DLLs, fonts, settings) for a Steam game:\n\
39         $ protontool APPID <verb> [verb...]\n\n\
40         Search for games to find the APPID:\n\
41         $ protontool -s GAME_NAME\n\n\
42         List all installed games:\n\
43         $ protontool -l\n\n\
44         Launch the GUI to select games and components:\n\
45         $ protontool --gui\n\n\
46         Create a custom prefix (non-Steam apps):\n\
47         $ protontool --create-prefix ~/MyPrefix --proton 'Proton 9.0'\n\n\
48         Delete a custom prefix:\n\
49         $ protontool --delete-prefix ~/MyPrefix\n\n\
50         Environment variables:\n\n\
51         PROTON_VERSION: name of the preferred Proton installation\n\
52         STEAM_DIR: path to custom Steam installation\n\
53         WINE: path to a custom 'wine' executable\n\
54         WINESERVER: path to a custom 'wineserver' executable",
55    );
56
57    parser.add_flag("verbose", &["-v", "--verbose"], "Increase log verbosity");
58    parser.add_flag(
59        "no_term",
60        &["--no-term"],
61        "Program was launched from desktop",
62    );
63    parser.add_option(
64        "search",
65        &["-s", "--search"],
66        "Search for game(s) with the given name",
67    );
68    parser.add_flag("list", &["-l", "--list"], "List all apps");
69    parser.add_option(
70        "command",
71        &["-c", "--command"],
72        "Run a command with Wine environment variables",
73    );
74    parser.add_flag("gui", &["--gui"], "Launch the protontool GUI");
75    parser.add_flag(
76        "background_wineserver",
77        &["--background-wineserver"],
78        "Start wineserver in background before running commands",
79    );
80    parser.add_flag(
81        "cwd_app",
82        &["--cwd-app"],
83        "Set working directory to app's install dir",
84    );
85    parser.add_multi_option(
86        "steam_library",
87        &["--steam-library", "-S"],
88        "Additional Steam library path (can be specified multiple times)",
89    );
90    parser.add_option(
91        "create_prefix",
92        &["--create-prefix"],
93        "Create a new Wine prefix at the given path",
94    );
95    parser.add_option(
96        "delete_prefix",
97        &["--delete-prefix"],
98        "Delete an existing custom prefix at the given path",
99    );
100    parser.add_option(
101        "prefix",
102        &["--prefix", "-p"],
103        "Use an existing custom prefix path",
104    );
105    parser.add_option(
106        "proton",
107        &["--proton"],
108        "Proton version to use (e.g., 'Proton 9.0')",
109    );
110    parser.add_option(
111        "arch",
112        &["--arch"],
113        "Prefix architecture: win32 or win64 (default: win64)",
114    );
115    parser.add_flag("version", &["-V", "--version"], "Show version");
116    parser.add_flag("help", &["-h", "--help"], "Show help");
117
118    let parsed = match parser.parse(&args) {
119        Ok(p) => p,
120        Err(e) => {
121            eprintln!("{}", parser.help());
122            eprintln!("protontool: error: {}", e);
123            process::exit(2);
124        }
125    };
126
127    if parsed.get_flag("help") {
128        println!("{}", parser.help());
129        return;
130    }
131
132    if parsed.get_flag("version") {
133        println!("protontool ({})", crate::VERSION);
134        return;
135    }
136
137    let no_term = parsed.get_flag("no_term");
138    let verbose = parsed.get_count("verbose");
139
140    enable_logging(verbose);
141
142    let do_command = parsed.get_option("command").is_some();
143    let do_list_apps = parsed.get_option("search").is_some() || parsed.get_flag("list");
144    let do_gui = parsed.get_flag("gui");
145    let do_create_prefix = parsed.get_option("create_prefix").is_some();
146    let do_delete_prefix = parsed.get_option("delete_prefix").is_some();
147    let do_use_prefix = parsed.get_option("prefix").is_some();
148
149    let positional = parsed.positional();
150    let appid: Option<u32> = positional.first().and_then(|s| s.parse().ok());
151    let verbs_to_run: Vec<String> = if positional.len() > 1 {
152        positional[1..].to_vec()
153    } else {
154        vec![]
155    };
156    let do_run_verbs = appid.is_some() && !verbs_to_run.is_empty();
157
158    if !do_command
159        && !do_list_apps
160        && !do_gui
161        && !do_run_verbs
162        && !do_create_prefix
163        && !do_delete_prefix
164        && !do_use_prefix
165    {
166        if args.is_empty() {
167            // Default to GUI mode when no args
168            run_gui_mode(no_term);
169            return;
170        }
171        println!("{}", parser.help());
172        return;
173    }
174
175    // Allow combining -c with --prefix (command mode with custom prefix)
176    let do_prefix_command = do_command && do_use_prefix;
177
178    let action_count = if do_prefix_command {
179        1 // Treat prefix + command as single action
180    } else {
181        [
182            do_list_apps,
183            do_gui,
184            do_run_verbs,
185            do_command,
186            do_create_prefix,
187            do_delete_prefix,
188            do_use_prefix,
189        ]
190        .iter()
191        .filter(|&&x| x)
192        .count()
193    };
194
195    if action_count != 1 {
196        eprintln!("Only one action can be performed at a time.");
197        println!("{}", parser.help());
198        return;
199    }
200
201    if do_gui {
202        run_gui_mode(no_term);
203    } else if do_list_apps {
204        run_list_mode(&parsed, no_term);
205    } else if do_run_verbs {
206        run_verb_mode(appid.unwrap(), &verbs_to_run, &parsed, no_term);
207    } else if do_prefix_command {
208        let cmd = parsed.get_option("command").unwrap();
209        let prefix_path = parsed.get_option("prefix").unwrap();
210        run_prefix_command_mode(&prefix_path, &cmd, &parsed, no_term);
211    } else if do_command {
212        let cmd = parsed.get_option("command").unwrap();
213        run_command_mode(appid, &cmd, &parsed, no_term);
214    } else if do_create_prefix {
215        let prefix_path = parsed.get_option("create_prefix").unwrap();
216        run_create_prefix_mode(&prefix_path, &parsed, no_term);
217    } else if do_delete_prefix {
218        let prefix_path = parsed.get_option("delete_prefix").unwrap();
219        run_delete_prefix_mode(&prefix_path, no_term);
220    } else if do_use_prefix {
221        let prefix_path = parsed.get_option("prefix").unwrap();
222        run_custom_prefix_mode(&prefix_path, &verbs_to_run, &parsed, no_term);
223    }
224}
225
226/// Get Steam installation context (steam_path, steam_root, library_paths).
227/// Returns None if user cancels selection or no Steam found.
228fn get_steam_context(
229    no_term: bool,
230    extra_libraries: &[String],
231) -> Option<(PathBuf, PathBuf, Vec<PathBuf>)> {
232    let steam_installations = find_steam_installations();
233    if steam_installations.is_empty() {
234        exit_with_error("Steam installation directory could not be found.", no_term);
235    }
236
237    let installation = select_steam_installation(&steam_installations)?;
238    let steam_path = installation.steam_path.clone();
239    let steam_root = installation.steam_root.clone();
240
241    let extra_paths: Vec<PathBuf> = extra_libraries.iter().map(PathBuf::from).collect();
242    let steam_lib_paths = get_steam_lib_paths(&steam_path, &extra_paths);
243
244    let paths: Vec<&std::path::Path> = vec![&steam_path, &steam_root];
245    prompt_filesystem_access(&paths, no_term);
246
247    Some((steam_path, steam_root, steam_lib_paths))
248}
249
250/// Run the interactive GUI mode with main menu loop.
251fn run_gui_mode(no_term: bool) {
252    // Show main menu to choose action
253    loop {
254        let action = match show_main_menu_gui() {
255            Some(a) => a,
256            None => return, // User cancelled
257        };
258
259        match action {
260            GuiAction::ManageGame => run_gui_manage_game(no_term),
261            GuiAction::CreatePrefix => run_gui_create_prefix(no_term),
262            GuiAction::DeletePrefix => run_gui_delete_prefix(no_term),
263            GuiAction::ManagePrefix => run_gui_manage_prefix(no_term),
264        }
265    }
266}
267
268/// GUI flow for managing a Steam game's prefix.
269fn run_gui_manage_game(no_term: bool) {
270    // First, let user add extra Steam library paths via GUI
271    let extra_lib_paths = select_steam_library_paths();
272    let extra_libs: Vec<String> = extra_lib_paths
273        .iter()
274        .map(|p| p.to_string_lossy().to_string())
275        .collect();
276
277    let (steam_path, steam_root, steam_lib_paths) = match get_steam_context(no_term, &extra_libs) {
278        Some(ctx) => ctx,
279        None => {
280            exit_with_error("No Steam installation was selected.", no_term);
281        }
282    };
283
284    let steam_apps = get_steam_apps(&steam_root, &steam_path, &steam_lib_paths);
285
286    let windows_apps: Vec<_> = steam_apps
287        .iter()
288        .filter(|app| app.is_windows_app())
289        .collect();
290
291    if windows_apps.is_empty() {
292        exit_with_error(
293            "Found no games. You need to launch a game at least once before protontool can find it.",
294            no_term
295        );
296    }
297
298    let steam_app = match select_steam_app_with_gui(&steam_apps, None, &steam_path) {
299        Some(app) => app,
300        None => return,
301    };
302
303    let proton_app = match find_proton_app(&steam_path, &steam_apps, steam_app.appid) {
304        Some(app) => app,
305        None => {
306            exit_with_error("Proton installation could not be found!", no_term);
307        }
308    };
309
310    if !proton_app.is_proton_ready {
311        exit_with_error(
312            "Proton installation is incomplete. Have you launched a Steam app using this Proton version at least once?",
313            no_term
314        );
315    }
316
317    let prefix_path = steam_app.prefix_path.as_ref().unwrap();
318    let verb_runner = Wine::new(&proton_app, prefix_path);
319
320    // Show category selection, then verb selection
321    loop {
322        let category = match select_verb_category_gui() {
323            Some(cat) => cat,
324            None => return, // User cancelled - go back to main menu
325        };
326
327        let verbs = verb_runner.list_verbs(Some(category));
328        let selected = select_verbs_with_gui(
329            &verbs,
330            Some(&format!("Select {} to install", category.as_str())),
331        );
332
333        if selected.is_empty() {
334            continue; // Go back to category selection
335        }
336
337        // Run selected verbs
338        for verb_name in &selected {
339            println!("Running verb: {}", verb_name);
340            if let Err(e) = verb_runner.run_verb(verb_name) {
341                eprintln!("Error running {}: {}", verb_name, e);
342            }
343        }
344
345        println!("Completed running verbs.");
346    }
347}
348
349/// GUI flow for creating a new custom prefix.
350fn run_gui_create_prefix(no_term: bool) {
351    // Get prefix name from user
352    let prefix_name = match get_prefix_name_gui() {
353        Some(name) => name,
354        None => return,
355    };
356
357    // Get prefix location
358    let prefix_path = match select_prefix_location_gui(&prefix_name) {
359        Some(path) => path,
360        None => return,
361    };
362
363    // Get Steam context for Proton selection
364    let (steam_path, steam_root, steam_lib_paths) = match get_steam_context(no_term, &[]) {
365        Some(ctx) => ctx,
366        None => {
367            exit_with_error("No Steam installation was selected.", no_term);
368        }
369    };
370
371    let steam_apps = get_steam_apps(&steam_root, &steam_path, &steam_lib_paths);
372    let proton_apps = get_proton_apps(&steam_apps);
373
374    if proton_apps.is_empty() {
375        exit_with_error(
376            "No Proton installations found. Please install Proton through Steam first.",
377            no_term,
378        );
379    }
380
381    // Let user select Proton version
382    let proton_app = match select_proton_with_gui(&proton_apps) {
383        Some(app) => app,
384        None => return,
385    };
386
387    if !proton_app.is_proton_ready {
388        exit_with_error(
389            "Selected Proton installation is not ready. Please launch a game with this Proton version first.",
390            no_term
391        );
392    }
393
394    // Let user select architecture
395    let arch = match select_arch_gui() {
396        Some(a) => a,
397        None => return,
398    };
399
400    // Create the prefix
401    println!("Creating Wine prefix at: {}", prefix_path.display());
402    println!("Using Proton: {}", proton_app.name);
403    println!("Architecture: {}", arch.as_str());
404
405    if let Err(e) = std::fs::create_dir_all(&prefix_path) {
406        exit_with_error(
407            &format!("Failed to create prefix directory: {}", e),
408            no_term,
409        );
410    }
411
412    let wine_ctx = crate::wine::WineContext::from_proton_with_arch(&proton_app, &prefix_path, arch);
413    // Proton uses "files" subdirectory, older versions may use "dist"
414    let dist_dir = {
415        let files_dir = proton_app.install_path.join("files");
416        let dist_dir = proton_app.install_path.join("dist");
417        if files_dir.exists() {
418            files_dir
419        } else {
420            dist_dir
421        }
422    };
423
424    println!("Initializing prefix...");
425    if let Err(e) = crate::wine::prefix::init_prefix(&prefix_path, &dist_dir, true, Some(&wine_ctx))
426    {
427        exit_with_error(&format!("Failed to initialize prefix: {}", e), no_term);
428    }
429
430    // Save metadata
431    let metadata_path = prefix_path.join(".protontool");
432    let metadata = format!(
433        "proton_name={}\nproton_path={}\narch={}\ncreated={}\n",
434        proton_app.name,
435        proton_app.install_path.display(),
436        arch.as_str(),
437        chrono_lite_now()
438    );
439    std::fs::write(&metadata_path, metadata).ok();
440
441    println!("Prefix '{}' created successfully!", prefix_name);
442}
443
444/// GUI flow for deleting an existing custom prefix.
445fn run_gui_delete_prefix(no_term: bool) {
446    let prefixes_dir = crate::config::get_prefixes_dir();
447
448    // Ensure directory exists
449    std::fs::create_dir_all(&prefixes_dir).ok();
450
451    // Let user select a prefix to delete
452    let prefix_path = match select_custom_prefix_gui(&prefixes_dir) {
453        Some(path) => path,
454        None => return,
455    };
456
457    let prefix_name = prefix_path
458        .file_name()
459        .and_then(|n| n.to_str())
460        .unwrap_or("Unknown");
461
462    // Confirm deletion
463    let gui_tool = match crate::gui::get_gui_tool() {
464        Some(tool) => tool,
465        None => {
466            exit_with_error("No GUI tool available", no_term);
467        }
468    };
469
470    let confirm_text = format!(
471        "Are you sure you want to delete the prefix '{}'?\n\nThis will permanently remove:\n{}\n\nThis action cannot be undone!",
472        prefix_name,
473        prefix_path.display()
474    );
475
476    let confirm = std::process::Command::new(&gui_tool)
477        .args([
478            "--question",
479            "--title",
480            "Confirm Delete",
481            "--text",
482            &confirm_text,
483            "--width",
484            "450",
485        ])
486        .status()
487        .map(|s| s.success())
488        .unwrap_or(false);
489
490    if !confirm {
491        println!("Deletion cancelled.");
492        return;
493    }
494
495    // Delete the prefix directory
496    match std::fs::remove_dir_all(&prefix_path) {
497        Ok(()) => {
498            println!("Prefix '{}' deleted successfully.", prefix_name);
499
500            // Show success message
501            let _ = std::process::Command::new(&gui_tool)
502                .args([
503                    "--info",
504                    "--title",
505                    "Prefix Deleted",
506                    "--text",
507                    &format!("Prefix '{}' has been deleted.", prefix_name),
508                    "--width",
509                    "300",
510                ])
511                .status();
512        }
513        Err(e) => {
514            let error_msg = format!("Failed to delete prefix: {}", e);
515            eprintln!("{}", error_msg);
516
517            let _ = std::process::Command::new(&gui_tool)
518                .args([
519                    "--error",
520                    "--title",
521                    "Delete Failed",
522                    "--text",
523                    &error_msg,
524                    "--width",
525                    "400",
526                ])
527                .status();
528        }
529    }
530}
531
532/// GUI flow for managing an existing custom prefix.
533fn run_gui_manage_prefix(no_term: bool) {
534    // Get the default prefixes directory
535    let prefixes_dir = crate::config::get_prefixes_dir();
536
537    // Ensure directory exists
538    std::fs::create_dir_all(&prefixes_dir).ok();
539
540    // Let user select a prefix
541    let prefix_path = match select_custom_prefix_gui(&prefixes_dir) {
542        Some(path) => path,
543        None => return,
544    };
545
546    // Get Steam context for Proton
547    let (steam_path, steam_root, steam_lib_paths) = match get_steam_context(no_term, &[]) {
548        Some(ctx) => ctx,
549        None => {
550            exit_with_error("No Steam installation was selected.", no_term);
551        }
552    };
553
554    let steam_apps = get_steam_apps(&steam_root, &steam_path, &steam_lib_paths);
555
556    // Try to read saved Proton and arch info
557    let metadata_path = prefix_path.join(".protontool");
558    let metadata_content = std::fs::read_to_string(&metadata_path).ok();
559
560    let proton_app = if let Some(ref metadata) = metadata_content {
561        let proton_name = metadata
562            .lines()
563            .find(|l| l.starts_with("proton_name="))
564            .and_then(|l| l.strip_prefix("proton_name="));
565
566        if let Some(name) = proton_name {
567            find_proton_by_name(&steam_apps, name)
568        } else {
569            None
570        }
571    } else {
572        None
573    };
574
575    // Read saved architecture (default to win64)
576    let saved_arch = metadata_content
577        .as_ref()
578        .and_then(|m| m.lines().find(|l| l.starts_with("arch=")))
579        .and_then(|l| l.strip_prefix("arch="))
580        .and_then(crate::wine::WineArch::from_str)
581        .unwrap_or(crate::wine::WineArch::Win64);
582
583    let proton_app = match proton_app {
584        Some(app) => {
585            println!("Using saved Proton version: {}", app.name);
586            app
587        }
588        None => {
589            let proton_apps = get_proton_apps(&steam_apps);
590            match select_proton_with_gui(&proton_apps) {
591                Some(app) => app,
592                None => return,
593            }
594        }
595    };
596
597    if !proton_app.is_proton_ready {
598        exit_with_error("Proton installation is not ready.", no_term);
599    }
600
601    let verb_runner = Wine::new_with_arch(&proton_app, &prefix_path, saved_arch);
602    let wine_ctx =
603        crate::wine::WineContext::from_proton_with_arch(&proton_app, &prefix_path, saved_arch);
604
605    // Interactive action selection
606    loop {
607        // Show action menu
608        match select_prefix_action_gui() {
609            Some(PrefixAction::RunApplication) => {
610                if let Some(exe_path) = select_executable_gui() {
611                    println!("Running: {}", exe_path.display());
612                    // run_wine automatically changes to executable's directory
613                    match wine_ctx.run_wine(&[&exe_path.to_string_lossy()]) {
614                        Ok(_) => {}
615                        Err(e) => eprintln!("Error running application: {}", e),
616                    }
617                }
618            }
619            Some(PrefixAction::InstallComponents) => {
620                let category = match select_verb_category_gui() {
621                    Some(cat) => cat,
622                    None => continue,
623                };
624
625                let verb_list = verb_runner.list_verbs(Some(category));
626                let selected = select_verbs_with_gui(
627                    &verb_list,
628                    Some(&format!("Select {} to install", category.as_str())),
629                );
630
631                if selected.is_empty() {
632                    continue;
633                }
634
635                for verb_name in &selected {
636                    println!("Running verb: {}", verb_name);
637                    if let Err(e) = verb_runner.run_verb(verb_name) {
638                        eprintln!("Error running {}: {}", verb_name, e);
639                    }
640                }
641
642                println!("Completed running verbs.");
643            }
644            Some(PrefixAction::WineTools) => {
645                if let Some(tool) = select_wine_tool_gui() {
646                    println!("Launching: {}", tool);
647                    match wine_ctx.run_wine_no_cwd(&[&tool]) {
648                        Ok(_) => {}
649                        Err(e) => eprintln!("Error launching {}: {}", tool, e),
650                    }
651                }
652            }
653            Some(PrefixAction::Settings) => {
654                if let Some(setting) = select_prefix_setting_gui() {
655                    match setting {
656                        PrefixSetting::Dpi => {
657                            if let Some(dpi) = select_dpi_gui() {
658                                println!("Setting DPI to: {}", dpi);
659                                set_wine_dpi(&wine_ctx, dpi);
660                            }
661                        }
662                        PrefixSetting::DllOverride => {
663                            run_dll_override_gui(&wine_ctx);
664                        }
665                        PrefixSetting::WindowsVersion => {
666                            if let Some(version) = select_windows_version_gui() {
667                                println!("Setting Windows version to: {}", version);
668                                set_windows_version(&wine_ctx, &version);
669                            }
670                        }
671                        PrefixSetting::VirtualDesktop => {
672                            run_virtual_desktop_gui(&wine_ctx);
673                        }
674                        PrefixSetting::Theme => {
675                            if let Some(theme) = select_theme_gui(&wine_ctx) {
676                                println!("Setting theme to: {}", theme);
677                                set_wine_theme(&wine_ctx, &theme);
678                            }
679                        }
680                        PrefixSetting::RegistryImport => {
681                            run_registry_import_gui(&wine_ctx);
682                        }
683                        PrefixSetting::ViewLogs => {
684                            run_log_viewer_gui();
685                        }
686                    }
687                }
688            }
689            Some(PrefixAction::CreateVerb) => {
690                run_verb_creator_gui();
691            }
692            None => return,
693        }
694    }
695}
696
697/// Actions available when managing a prefix.
698enum PrefixAction {
699    RunApplication,
700    InstallComponents,
701    WineTools,
702    Settings,
703    CreateVerb,
704}
705
706/// Show GUI menu to select a prefix management action.
707fn select_prefix_action_gui() -> Option<PrefixAction> {
708    let gui_tool = crate::gui::get_gui_tool()?;
709
710    let args = vec![
711        "--list",
712        "--title",
713        "Select action",
714        "--column",
715        "Action",
716        "--column",
717        "Description",
718        "--print-column",
719        "1",
720        "--width",
721        "500",
722        "--height",
723        "350",
724        "run",
725        "Run an application",
726        "install",
727        "Install components (DLLs, fonts, etc.)",
728        "tools",
729        "Wine tools (winecfg, regedit, etc.)",
730        "settings",
731        "Prefix settings (DPI, etc.)",
732        "verb",
733        "Create custom verb",
734    ];
735
736    let output = std::process::Command::new(&gui_tool)
737        .args(&args)
738        .output()
739        .ok()?;
740
741    if !output.status.success() {
742        return None;
743    }
744
745    let selected = output_to_string(&output);
746
747    match selected.as_str() {
748        "run" => Some(PrefixAction::RunApplication),
749        "install" => Some(PrefixAction::InstallComponents),
750        "tools" => Some(PrefixAction::WineTools),
751        "settings" => Some(PrefixAction::Settings),
752        "verb" => Some(PrefixAction::CreateVerb),
753        _ => None,
754    }
755}
756
757/// Show file picker to select an executable to run.
758fn select_executable_gui() -> Option<PathBuf> {
759    let gui_tool = crate::gui::get_gui_tool()?;
760
761    let args = vec![
762        "--file-selection",
763        "--title",
764        "Select executable to run",
765        "--file-filter",
766        "Windows Executables | *.exe *.msi *.bat",
767    ];
768
769    let output = std::process::Command::new(&gui_tool)
770        .args(&args)
771        .output()
772        .ok()?;
773
774    if !output.status.success() {
775        return None;
776    }
777
778    let path = output_to_string(&output);
779    if path.is_empty() {
780        None
781    } else {
782        Some(PathBuf::from(path))
783    }
784}
785
786/// Show GUI to select prefix architecture (win32/win64).
787fn select_arch_gui() -> Option<crate::wine::WineArch> {
788    let gui_tool = crate::gui::get_gui_tool()?;
789
790    let args = vec![
791        "--list",
792        "--title",
793        "Select prefix architecture",
794        "--column",
795        "Architecture",
796        "--column",
797        "Description",
798        "--print-column",
799        "1",
800        "--width",
801        "500",
802        "--height",
803        "250",
804        "win64",
805        "64-bit Windows (recommended for modern apps)",
806        "win32",
807        "32-bit Windows (for legacy apps)",
808    ];
809
810    let output = std::process::Command::new(&gui_tool)
811        .args(&args)
812        .output()
813        .ok()?;
814
815    if !output.status.success() {
816        return None;
817    }
818
819    let selected = output_to_string(&output);
820    crate::wine::WineArch::from_str(&selected)
821}
822
823/// Show GUI to select a Wine tool (winecfg, regedit, etc.).
824fn select_wine_tool_gui() -> Option<String> {
825    let gui_tool = crate::gui::get_gui_tool()?;
826
827    let args = vec![
828        "--list",
829        "--title",
830        "Select Wine tool",
831        "--column",
832        "Tool",
833        "--column",
834        "Description",
835        "--print-column",
836        "1",
837        "--width",
838        "500",
839        "--height",
840        "350",
841        "winecfg",
842        "Wine configuration",
843        "regedit",
844        "Registry editor",
845        "taskmgr",
846        "Task manager",
847        "explorer",
848        "File explorer",
849        "control",
850        "Control panel",
851        "cmd",
852        "Command prompt",
853        "uninstaller",
854        "Wine uninstaller",
855    ];
856
857    let output = std::process::Command::new(&gui_tool)
858        .args(&args)
859        .output()
860        .ok()?;
861
862    if !output.status.success() {
863        return None;
864    }
865
866    let selected = output_to_string(&output);
867    if selected.is_empty() {
868        None
869    } else {
870        Some(selected)
871    }
872}
873
874/// Available prefix settings.
875enum PrefixSetting {
876    Dpi,
877    DllOverride,
878    WindowsVersion,
879    VirtualDesktop,
880    Theme,
881    RegistryImport,
882    ViewLogs,
883}
884
885/// Show GUI to select a prefix setting to modify.
886fn select_prefix_setting_gui() -> Option<PrefixSetting> {
887    let gui_tool = crate::gui::get_gui_tool()?;
888
889    let args = vec![
890        "--list",
891        "--title",
892        "Select setting",
893        "--column",
894        "Setting",
895        "--column",
896        "Description",
897        "--print-column",
898        "1",
899        "--width",
900        "500",
901        "--height",
902        "300",
903        "dpi",
904        "Display DPI (scaling)",
905        "dll",
906        "DLL overrides (native/builtin)",
907        "winver",
908        "Windows version",
909        "desktop",
910        "Virtual desktop",
911        "theme",
912        "Desktop theme",
913        "registry",
914        "Import registry file (.reg)",
915        "logs",
916        "View application logs",
917    ];
918
919    let output = std::process::Command::new(&gui_tool)
920        .args(&args)
921        .output()
922        .ok()?;
923
924    if !output.status.success() {
925        return None;
926    }
927
928    let selected = output_to_string(&output);
929    match selected.as_str() {
930        "dpi" => Some(PrefixSetting::Dpi),
931        "dll" => Some(PrefixSetting::DllOverride),
932        "winver" => Some(PrefixSetting::WindowsVersion),
933        "desktop" => Some(PrefixSetting::VirtualDesktop),
934        "theme" => Some(PrefixSetting::Theme),
935        "registry" => Some(PrefixSetting::RegistryImport),
936        "logs" => Some(PrefixSetting::ViewLogs),
937        _ => None,
938    }
939}
940
941/// Show GUI to select DPI value.
942fn select_dpi_gui() -> Option<u32> {
943    let gui_tool = crate::gui::get_gui_tool()?;
944
945    // DPI options in increments of 48, starting at 96
946    let args = vec![
947        "--list",
948        "--title",
949        "Select DPI",
950        "--column",
951        "DPI",
952        "--column",
953        "Scale",
954        "--print-column",
955        "1",
956        "--width",
957        "400",
958        "--height",
959        "400",
960        "96",
961        "100% (default)",
962        "144",
963        "150%",
964        "192",
965        "200%",
966        "240",
967        "250%",
968        "288",
969        "300%",
970        "336",
971        "350%",
972        "384",
973        "400%",
974    ];
975
976    let output = std::process::Command::new(&gui_tool)
977        .args(&args)
978        .output()
979        .ok()?;
980
981    if !output.status.success() {
982        return None;
983    }
984
985    let selected = output_to_string(&output);
986    selected.parse().ok()
987}
988
989/// Set Wine DPI via registry.
990fn set_wine_dpi(wine_ctx: &crate::wine::WineContext, dpi: u32) {
991    // Set DPI via registry
992    let reg_content = format!(
993        "Windows Registry Editor Version 5.00\n\n\
994         [HKEY_CURRENT_USER\\Control Panel\\Desktop]\n\
995         \"LogPixels\"=dword:{:08x}\n\n\
996         [HKEY_CURRENT_USER\\Software\\Wine\\Fonts]\n\
997         \"LogPixels\"=dword:{:08x}\n",
998        dpi, dpi
999    );
1000
1001    // Write to a temp .reg file
1002    let tmp_dir = std::env::temp_dir();
1003    let reg_file = tmp_dir.join("protontool_dpi.reg");
1004
1005    if let Err(e) = std::fs::write(&reg_file, &reg_content) {
1006        eprintln!("Failed to write registry file: {}", e);
1007        return;
1008    }
1009
1010    // Import the registry file
1011    match wine_ctx.run_wine_no_cwd(&["regedit", "/S", &reg_file.to_string_lossy()]) {
1012        Ok(_) => println!(
1013            "DPI set to {}. You may need to restart applications for changes to take effect.",
1014            dpi
1015        ),
1016        Err(e) => eprintln!("Failed to set DPI: {}", e),
1017    }
1018
1019    // Clean up
1020    std::fs::remove_file(&reg_file).ok();
1021}
1022
1023// ============================================================================
1024// DLL OVERRIDE SETTINGS
1025// ============================================================================
1026
1027/// Run the DLL override management GUI.
1028fn run_dll_override_gui(wine_ctx: &crate::wine::WineContext) {
1029    let gui_tool = match crate::gui::get_gui_tool() {
1030        Some(tool) => tool,
1031        None => return,
1032    };
1033
1034    loop {
1035        // Show action menu
1036        let args = vec![
1037            "--list",
1038            "--title",
1039            "DLL Overrides",
1040            "--column",
1041            "Action",
1042            "--column",
1043            "Description",
1044            "--print-column",
1045            "1",
1046            "--width",
1047            "500",
1048            "--height",
1049            "300",
1050            "add",
1051            "Add new DLL override",
1052            "remove",
1053            "Remove DLL override",
1054            "list",
1055            "List current overrides",
1056            "back",
1057            "Back to settings",
1058        ];
1059
1060        let output = match std::process::Command::new(&gui_tool).args(&args).output() {
1061            Ok(out) => out,
1062            Err(_) => return,
1063        };
1064
1065        if !output.status.success() {
1066            return;
1067        }
1068
1069        let selected = output_to_string(&output);
1070        match selected.as_str() {
1071            "add" => add_dll_override_gui(&gui_tool, wine_ctx),
1072            "remove" => remove_dll_override_gui(&gui_tool, wine_ctx),
1073            "list" => list_dll_overrides_gui(&gui_tool, wine_ctx),
1074            _ => return,
1075        }
1076    }
1077}
1078
1079/// Show GUI dialogs to add a new DLL override.
1080fn add_dll_override_gui(gui_tool: &std::path::Path, wine_ctx: &crate::wine::WineContext) {
1081    // Get DLL name
1082    let output = std::process::Command::new(gui_tool)
1083        .args([
1084            "--entry",
1085            "--title", "Add DLL Override",
1086            "--text", "Enter DLL name (without .dll extension):\n\nCommon examples: d3d9, d3d11, dxgi, xinput1_3, vcrun2019",
1087            "--width", "400",
1088        ])
1089        .output();
1090
1091    let dll_name = match output {
1092        Ok(out) if out.status.success() => output_to_string(&out),
1093        _ => return,
1094    };
1095
1096    if dll_name.is_empty() {
1097        return;
1098    }
1099
1100    // Get override mode
1101    let title = format!("Override mode for {}", dll_name);
1102    let args = vec![
1103        "--list",
1104        "--title",
1105        &title,
1106        "--column",
1107        "Mode",
1108        "--column",
1109        "Description",
1110        "--print-column",
1111        "1",
1112        "--width",
1113        "500",
1114        "--height",
1115        "300",
1116        "native",
1117        "Use Windows native DLL only",
1118        "builtin",
1119        "Use Wine builtin DLL only",
1120        "native,builtin",
1121        "Prefer native, fall back to builtin",
1122        "builtin,native",
1123        "Prefer builtin, fall back to native",
1124        "disabled",
1125        "Disable the DLL entirely",
1126    ];
1127
1128    let output = match std::process::Command::new(gui_tool).args(&args).output() {
1129        Ok(out) => out,
1130        Err(_) => return,
1131    };
1132
1133    if !output.status.success() {
1134        return;
1135    }
1136
1137    let mode = output_to_string(&output);
1138    if mode.is_empty() {
1139        return;
1140    }
1141
1142    // Set the override via registry
1143    let reg_content = format!(
1144        "Windows Registry Editor Version 5.00\n\n\
1145         [HKEY_CURRENT_USER\\Software\\Wine\\DllOverrides]\n\
1146         \"{}\"=\"{}\"\n",
1147        dll_name, mode
1148    );
1149
1150    let tmp_dir = std::env::temp_dir();
1151    let reg_file = tmp_dir.join("protontool_dll_override.reg");
1152
1153    if let Err(e) = std::fs::write(&reg_file, &reg_content) {
1154        eprintln!("Failed to write registry file: {}", e);
1155        return;
1156    }
1157
1158    match wine_ctx.run_wine_no_cwd(&["regedit", "/S", &reg_file.to_string_lossy()]) {
1159        Ok(_) => println!("DLL override set: {} = {}", dll_name, mode),
1160        Err(e) => eprintln!("Failed to set DLL override: {}", e),
1161    }
1162
1163    std::fs::remove_file(&reg_file).ok();
1164}
1165
1166fn remove_dll_override_gui(gui_tool: &std::path::Path, wine_ctx: &crate::wine::WineContext) {
1167    // Get DLL name to remove
1168    let output = std::process::Command::new(gui_tool)
1169        .args([
1170            "--entry",
1171            "--title",
1172            "Remove DLL Override",
1173            "--text",
1174            "Enter DLL name to remove override for:",
1175            "--width",
1176            "400",
1177        ])
1178        .output();
1179
1180    let dll_name = match output {
1181        Ok(out) if out.status.success() => output_to_string(&out),
1182        _ => return,
1183    };
1184
1185    if dll_name.is_empty() {
1186        return;
1187    }
1188
1189    // Remove override via registry (set to -)
1190    let reg_content = format!(
1191        "Windows Registry Editor Version 5.00\n\n\
1192         [HKEY_CURRENT_USER\\Software\\Wine\\DllOverrides]\n\
1193         \"{}\"=-\n",
1194        dll_name
1195    );
1196
1197    let tmp_dir = std::env::temp_dir();
1198    let reg_file = tmp_dir.join("protontool_dll_override.reg");
1199
1200    if let Err(e) = std::fs::write(&reg_file, &reg_content) {
1201        eprintln!("Failed to write registry file: {}", e);
1202        return;
1203    }
1204
1205    match wine_ctx.run_wine_no_cwd(&["regedit", "/S", &reg_file.to_string_lossy()]) {
1206        Ok(_) => println!("DLL override removed: {}", dll_name),
1207        Err(e) => eprintln!("Failed to remove DLL override: {}", e),
1208    }
1209
1210    std::fs::remove_file(&reg_file).ok();
1211}
1212
1213fn list_dll_overrides_gui(gui_tool: &std::path::Path, wine_ctx: &crate::wine::WineContext) {
1214    // Export the DLL overrides from registry
1215    let output = wine_ctx.run_wine_no_cwd(&["reg", "query", "HKCU\\Software\\Wine\\DllOverrides"]);
1216
1217    let text = match output {
1218        Ok(out) => {
1219            let stdout = String::from_utf8_lossy(&out.stdout);
1220            if stdout.trim().is_empty() || stdout.contains("ERROR") {
1221                "No DLL overrides configured.".to_string()
1222            } else {
1223                // Parse and format the output
1224                let mut overrides = Vec::new();
1225                for line in stdout.lines() {
1226                    let line = line.trim();
1227                    if line.contains("REG_SZ") {
1228                        // Format: "    dllname    REG_SZ    mode"
1229                        let parts: Vec<&str> = line.split_whitespace().collect();
1230                        if parts.len() >= 3 {
1231                            overrides.push(format!("{} = {}", parts[0], parts[2]));
1232                        }
1233                    }
1234                }
1235                if overrides.is_empty() {
1236                    "No DLL overrides configured.".to_string()
1237                } else {
1238                    overrides.join("\n")
1239                }
1240            }
1241        }
1242        Err(_) => "No DLL overrides configured.".to_string(),
1243    };
1244
1245    let _ = std::process::Command::new(gui_tool)
1246        .args([
1247            "--info",
1248            "--title",
1249            "Current DLL Overrides",
1250            "--text",
1251            &text,
1252            "--width",
1253            "400",
1254        ])
1255        .output();
1256}
1257
1258// ============================================================================
1259// WINDOWS VERSION SETTINGS
1260// ============================================================================
1261
1262fn select_windows_version_gui() -> Option<String> {
1263    let gui_tool = crate::gui::get_gui_tool()?;
1264
1265    let args = vec![
1266        "--list",
1267        "--title",
1268        "Select Windows Version",
1269        "--column",
1270        "Version",
1271        "--column",
1272        "Description",
1273        "--print-column",
1274        "1",
1275        "--width",
1276        "500",
1277        "--height",
1278        "400",
1279        "win11",
1280        "Windows 11",
1281        "win10",
1282        "Windows 10",
1283        "win81",
1284        "Windows 8.1",
1285        "win8",
1286        "Windows 8",
1287        "win7",
1288        "Windows 7",
1289        "vista",
1290        "Windows Vista",
1291        "winxp64",
1292        "Windows XP (64-bit)",
1293        "winxp",
1294        "Windows XP",
1295        "win2k",
1296        "Windows 2000",
1297        "win98",
1298        "Windows 98",
1299    ];
1300
1301    let output = std::process::Command::new(&gui_tool)
1302        .args(&args)
1303        .output()
1304        .ok()?;
1305
1306    if !output.status.success() {
1307        return None;
1308    }
1309
1310    let selected = output_to_string(&output);
1311    if selected.is_empty() {
1312        None
1313    } else {
1314        Some(selected)
1315    }
1316}
1317
1318fn set_windows_version(wine_ctx: &crate::wine::WineContext, version: &str) {
1319    // Map version string to Windows version data
1320    let (ver_str, build, sp, product) = match version {
1321        "win11" => ("win11", "10.0.22000", "", "Windows 11"),
1322        "win10" => ("win10", "10.0.19041", "", "Windows 10"),
1323        "win81" => ("win81", "6.3.9600", "", "Windows 8.1"),
1324        "win8" => ("win8", "6.2.9200", "", "Windows 8"),
1325        "win7" => ("win7", "6.1.7601", "Service Pack 1", "Windows 7"),
1326        "vista" => ("vista", "6.0.6002", "Service Pack 2", "Windows Vista"),
1327        "winxp64" => ("winxp64", "5.2.3790", "Service Pack 2", "Windows XP"),
1328        "winxp" => ("winxp", "5.1.2600", "Service Pack 3", "Windows XP"),
1329        "win2k" => ("win2k", "5.0.2195", "Service Pack 4", "Windows 2000"),
1330        "win98" => ("win98", "4.10.2222", "", "Windows 98"),
1331        _ => return,
1332    };
1333
1334    let parts: Vec<&str> = build.split('.').collect();
1335    let major = parts.get(0).unwrap_or(&"10");
1336    let minor = parts.get(1).unwrap_or(&"0");
1337    let build_num = parts.get(2).unwrap_or(&"0");
1338
1339    let reg_content = format!(
1340        "Windows Registry Editor Version 5.00\n\n\
1341         [HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion]\n\
1342         \"ProductName\"=\"{}\"\n\
1343         \"CSDVersion\"=\"{}\"\n\
1344         \"CurrentBuild\"=\"{}\"\n\
1345         \"CurrentBuildNumber\"=\"{}\"\n\
1346         \"CurrentVersion\"=\"{}.{}\"\n\n\
1347         [HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Control\\Windows]\n\
1348         \"CSDVersion\"=dword:00000000\n\n\
1349         [HKEY_CURRENT_USER\\Software\\Wine]\n\
1350         \"Version\"=\"{}\"\n",
1351        product, sp, build_num, build_num, major, minor, ver_str
1352    );
1353
1354    let tmp_dir = std::env::temp_dir();
1355    let reg_file = tmp_dir.join("protontool_winver.reg");
1356
1357    if let Err(e) = std::fs::write(&reg_file, &reg_content) {
1358        eprintln!("Failed to write registry file: {}", e);
1359        return;
1360    }
1361
1362    match wine_ctx.run_wine_no_cwd(&["regedit", "/S", &reg_file.to_string_lossy()]) {
1363        Ok(_) => println!("Windows version set to: {}", product),
1364        Err(e) => eprintln!("Failed to set Windows version: {}", e),
1365    }
1366
1367    std::fs::remove_file(&reg_file).ok();
1368}
1369
1370// ============================================================================
1371// VIRTUAL DESKTOP SETTINGS
1372// ============================================================================
1373
1374fn run_virtual_desktop_gui(wine_ctx: &crate::wine::WineContext) {
1375    let gui_tool = match crate::gui::get_gui_tool() {
1376        Some(tool) => tool,
1377        None => return,
1378    };
1379
1380    let args = vec![
1381        "--list",
1382        "--title",
1383        "Virtual Desktop",
1384        "--column",
1385        "Action",
1386        "--column",
1387        "Description",
1388        "--print-column",
1389        "1",
1390        "--width",
1391        "500",
1392        "--height",
1393        "250",
1394        "enable",
1395        "Enable virtual desktop",
1396        "disable",
1397        "Disable virtual desktop (fullscreen)",
1398    ];
1399
1400    let output = match std::process::Command::new(&gui_tool).args(&args).output() {
1401        Ok(out) => out,
1402        Err(_) => return,
1403    };
1404
1405    if !output.status.success() {
1406        return;
1407    }
1408
1409    let selected = output_to_string(&output);
1410    match selected.as_str() {
1411        "enable" => enable_virtual_desktop_gui(&gui_tool, wine_ctx),
1412        "disable" => disable_virtual_desktop(wine_ctx),
1413        _ => {}
1414    }
1415}
1416
1417fn enable_virtual_desktop_gui(gui_tool: &std::path::Path, wine_ctx: &crate::wine::WineContext) {
1418    // Get resolution
1419    let args = vec![
1420        "--list",
1421        "--title",
1422        "Virtual Desktop Resolution",
1423        "--column",
1424        "Resolution",
1425        "--column",
1426        "Aspect Ratio",
1427        "--print-column",
1428        "1",
1429        "--width",
1430        "400",
1431        "--height",
1432        "400",
1433        "1920x1080",
1434        "16:9 (Full HD)",
1435        "2560x1440",
1436        "16:9 (QHD)",
1437        "3840x2160",
1438        "16:9 (4K)",
1439        "1280x720",
1440        "16:9 (HD)",
1441        "1600x900",
1442        "16:9",
1443        "1366x768",
1444        "16:9",
1445        "1280x1024",
1446        "5:4",
1447        "1024x768",
1448        "4:3",
1449        "800x600",
1450        "4:3",
1451    ];
1452
1453    let output = match std::process::Command::new(gui_tool).args(&args).output() {
1454        Ok(out) => out,
1455        Err(_) => return,
1456    };
1457
1458    if !output.status.success() {
1459        return;
1460    }
1461
1462    let resolution = output_to_string(&output);
1463    if resolution.is_empty() {
1464        return;
1465    }
1466
1467    let reg_content = format!(
1468        "Windows Registry Editor Version 5.00\n\n\
1469         [HKEY_CURRENT_USER\\Software\\Wine\\Explorer]\n\
1470         \"Desktop\"=\"Default\"\n\n\
1471         [HKEY_CURRENT_USER\\Software\\Wine\\Explorer\\Desktops]\n\
1472         \"Default\"=\"{}\"\n",
1473        resolution
1474    );
1475
1476    let tmp_dir = std::env::temp_dir();
1477    let reg_file = tmp_dir.join("protontool_desktop.reg");
1478
1479    if let Err(e) = std::fs::write(&reg_file, &reg_content) {
1480        eprintln!("Failed to write registry file: {}", e);
1481        return;
1482    }
1483
1484    match wine_ctx.run_wine_no_cwd(&["regedit", "/S", &reg_file.to_string_lossy()]) {
1485        Ok(_) => println!("Virtual desktop enabled at {}", resolution),
1486        Err(e) => eprintln!("Failed to enable virtual desktop: {}", e),
1487    }
1488
1489    std::fs::remove_file(&reg_file).ok();
1490}
1491
1492fn disable_virtual_desktop(wine_ctx: &crate::wine::WineContext) {
1493    let reg_content = "Windows Registry Editor Version 5.00\n\n\
1494         [HKEY_CURRENT_USER\\Software\\Wine\\Explorer]\n\
1495         \"Desktop\"=-\n";
1496
1497    let tmp_dir = std::env::temp_dir();
1498    let reg_file = tmp_dir.join("protontool_desktop.reg");
1499
1500    if let Err(e) = std::fs::write(&reg_file, reg_content) {
1501        eprintln!("Failed to write registry file: {}", e);
1502        return;
1503    }
1504
1505    match wine_ctx.run_wine_no_cwd(&["regedit", "/S", &reg_file.to_string_lossy()]) {
1506        Ok(_) => println!("Virtual desktop disabled"),
1507        Err(e) => eprintln!("Failed to disable virtual desktop: {}", e),
1508    }
1509
1510    std::fs::remove_file(&reg_file).ok();
1511}
1512
1513// ============================================================================
1514// THEME SETTINGS
1515// ============================================================================
1516
1517fn select_theme_gui(wine_ctx: &crate::wine::WineContext) -> Option<String> {
1518    let gui_tool = crate::gui::get_gui_tool()?;
1519
1520    // Get available themes from the prefix
1521    let themes = get_available_themes(wine_ctx);
1522
1523    let mut args = vec![
1524        "--list".to_string(),
1525        "--title".to_string(),
1526        "Select Theme".to_string(),
1527        "--column".to_string(),
1528        "Theme".to_string(),
1529        "--column".to_string(),
1530        "Description".to_string(),
1531        "--print-column".to_string(),
1532        "1".to_string(),
1533        "--width".to_string(),
1534        "500".to_string(),
1535        "--height".to_string(),
1536        "400".to_string(),
1537        // Built-in themes
1538        "(none)".to_string(),
1539        "No theme (classic Windows look)".to_string(),
1540        "Light".to_string(),
1541        "Light theme".to_string(),
1542        "Dark".to_string(),
1543        "Dark theme".to_string(),
1544    ];
1545
1546    // Add any custom themes found in the prefix
1547    for theme in &themes {
1548        if theme != "Light" && theme != "Dark" {
1549            args.push(theme.clone());
1550            args.push("Custom theme".to_string());
1551        }
1552    }
1553
1554    let output = std::process::Command::new(&gui_tool)
1555        .args(&args)
1556        .output()
1557        .ok()?;
1558
1559    if !output.status.success() {
1560        return None;
1561    }
1562
1563    let selected = output_to_string(&output);
1564    if selected.is_empty() {
1565        None
1566    } else {
1567        Some(selected)
1568    }
1569}
1570
1571fn get_available_themes(wine_ctx: &crate::wine::WineContext) -> Vec<String> {
1572    let mut themes = Vec::new();
1573
1574    // Check for .msstyles files in the prefix's Resources/Themes directory
1575    let prefix_path = &wine_ctx.prefix_path;
1576    let themes_dir = prefix_path.join("drive_c/windows/Resources/Themes");
1577
1578    if let Ok(entries) = std::fs::read_dir(&themes_dir) {
1579        for entry in entries.flatten() {
1580            let path = entry.path();
1581            if path.is_dir() {
1582                if let Some(name) = path.file_name() {
1583                    let name_str = name.to_string_lossy().to_string();
1584                    // Check if it has a .msstyles file
1585                    let msstyles = path.join(format!("{}.msstyles", name_str));
1586                    if msstyles.exists() {
1587                        themes.push(name_str);
1588                    }
1589                }
1590            }
1591        }
1592    }
1593
1594    themes
1595}
1596
1597fn set_wine_theme(wine_ctx: &crate::wine::WineContext, theme: &str) {
1598    let prefix_path = &wine_ctx.prefix_path;
1599
1600    let (color_scheme, msstyles_path) = if theme == "(none)" {
1601        // Remove theme
1602        ("".to_string(), "".to_string())
1603    } else {
1604        // Set theme path
1605        let theme_path = format!(
1606            "C:\\\\windows\\\\Resources\\\\Themes\\\\{}\\\\{}.msstyles",
1607            theme, theme
1608        );
1609        ("NormalColor".to_string(), theme_path)
1610    };
1611
1612    // Create basic theme directories if they don't exist
1613    let themes_dir = prefix_path.join("drive_c/windows/Resources/Themes");
1614    std::fs::create_dir_all(&themes_dir).ok();
1615
1616    // Create Light theme if it doesn't exist
1617    create_builtin_theme(&themes_dir, "Light");
1618    create_builtin_theme(&themes_dir, "Dark");
1619
1620    let reg_content = if theme == "(none)" {
1621        "Windows Registry Editor Version 5.00\n\n\
1622         [HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\ThemeManager]\n\
1623         \"ColorName\"=\"\"\n\
1624         \"DllName\"=\"\"\n\
1625         \"ThemeActive\"=\"0\"\n"
1626            .to_string()
1627    } else {
1628        format!(
1629            "Windows Registry Editor Version 5.00\n\n\
1630             [HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\ThemeManager]\n\
1631             \"ColorName\"=\"{}\"\n\
1632             \"DllName\"=\"{}\"\n\
1633             \"ThemeActive\"=\"1\"\n",
1634            color_scheme, msstyles_path
1635        )
1636    };
1637
1638    let tmp_dir = std::env::temp_dir();
1639    let reg_file = tmp_dir.join("protontool_theme.reg");
1640
1641    if let Err(e) = std::fs::write(&reg_file, &reg_content) {
1642        eprintln!("Failed to write registry file: {}", e);
1643        return;
1644    }
1645
1646    match wine_ctx.run_wine_no_cwd(&["regedit", "/S", &reg_file.to_string_lossy()]) {
1647        Ok(_) => {
1648            if theme == "(none)" {
1649                println!("Theme disabled (classic Windows look)");
1650            } else {
1651                println!("Theme set to: {}", theme);
1652            }
1653        }
1654        Err(e) => eprintln!("Failed to set theme: {}", e),
1655    }
1656
1657    std::fs::remove_file(&reg_file).ok();
1658}
1659
1660fn create_builtin_theme(themes_dir: &std::path::Path, name: &str) {
1661    let theme_dir = themes_dir.join(name);
1662    let msstyles_path = theme_dir.join(format!("{}.msstyles", name));
1663
1664    // Only create if it doesn't exist
1665    if !msstyles_path.exists() {
1666        std::fs::create_dir_all(&theme_dir).ok();
1667        // Create an empty placeholder - Wine will use its builtin rendering
1668        std::fs::write(&msstyles_path, b"").ok();
1669    }
1670}
1671
1672// ============================================================================
1673// LOG VIEWER
1674// ============================================================================
1675
1676struct LogViewerState {
1677    show_error: bool,
1678    show_warning: bool,
1679    show_info: bool,
1680    show_debug: bool,
1681    search_filter: String,
1682}
1683
1684impl Default for LogViewerState {
1685    fn default() -> Self {
1686        Self {
1687            show_error: true,
1688            show_warning: true,
1689            show_info: true,
1690            show_debug: false,
1691            search_filter: String::new(),
1692        }
1693    }
1694}
1695
1696/// Run the log viewer GUI with filter controls and refresh
1697pub fn run_log_viewer_gui() {
1698    let gui_tool = match crate::gui::get_gui_tool() {
1699        Some(tool) => tool,
1700        None => {
1701            eprintln!("No GUI tool available (zenity/yad)");
1702            return;
1703        }
1704    };
1705
1706    let mut state = LogViewerState::default();
1707
1708    loop {
1709        // Step 1: Show filter/search options
1710        let filter_args = vec![
1711            "--forms",
1712            "--title",
1713            "Log Viewer - Filters",
1714            "--text",
1715            "Configure log filters:",
1716            "--add-combo",
1717            "Show Errors",
1718            "--combo-values",
1719            "Yes|No",
1720            "--add-combo",
1721            "Show Warnings",
1722            "--combo-values",
1723            "Yes|No",
1724            "--add-combo",
1725            "Show Info",
1726            "--combo-values",
1727            "Yes|No",
1728            "--add-combo",
1729            "Show Debug",
1730            "--combo-values",
1731            "Yes|No",
1732            "--add-entry",
1733            "Search",
1734            "--separator",
1735            "|",
1736            "--width",
1737            "400",
1738        ];
1739
1740        let filter_output = std::process::Command::new(&gui_tool)
1741            .args(&filter_args)
1742            .output();
1743
1744        let filters = match filter_output {
1745            Ok(out) if out.status.success() => output_to_string(&out),
1746            _ => return, // User cancelled
1747        };
1748
1749        // Parse filter selections (format: "Yes|Yes|Yes|No|searchterm")
1750        let parts: Vec<&str> = filters.split('|').collect();
1751        state.show_error = parts.first().map(|s| *s != "No").unwrap_or(true);
1752        state.show_warning = parts.get(1).map(|s| *s != "No").unwrap_or(true);
1753        state.show_info = parts.get(2).map(|s| *s != "No").unwrap_or(true);
1754        state.show_debug = parts.get(3).map(|s| *s == "Yes").unwrap_or(false);
1755        state.search_filter = parts.get(4).map(|s| s.to_string()).unwrap_or_default();
1756
1757        // Step 2: Get and display log entries
1758        loop {
1759            let search = if state.search_filter.is_empty() {
1760                None
1761            } else {
1762                Some(state.search_filter.as_str())
1763            };
1764
1765            let entries = crate::log::parse_log_deduplicated(
1766                state.show_error,
1767                state.show_warning,
1768                state.show_info,
1769                state.show_debug,
1770                search,
1771            );
1772
1773            // Build list arguments
1774            let mut list_args = vec![
1775                "--list".to_string(),
1776                "--title".to_string(),
1777                "Log Viewer".to_string(),
1778                "--column".to_string(),
1779                "Type".to_string(),
1780                "--column".to_string(),
1781                "Count".to_string(),
1782                "--column".to_string(),
1783                "Time".to_string(),
1784                "--column".to_string(),
1785                "Message".to_string(),
1786                "--width".to_string(),
1787                "900".to_string(),
1788                "--height".to_string(),
1789                "400".to_string(),
1790                "--ok-label".to_string(),
1791                "Refresh".to_string(),
1792                "--cancel-label".to_string(),
1793                "Close".to_string(),
1794                "--extra-button".to_string(),
1795                "Change Filters".to_string(),
1796            ];
1797
1798            if entries.is_empty() {
1799                list_args.push("--".to_string());
1800                list_args.push("0".to_string());
1801                list_args.push("--".to_string());
1802                list_args.push("No log entries match the current filters".to_string());
1803            } else {
1804                for entry in &entries {
1805                    list_args.push(entry.level.clone());
1806                    list_args.push(entry.count.to_string());
1807                    list_args.push(entry.timestamp.clone());
1808                    // Truncate long messages for display
1809                    let msg = if entry.message.len() > 100 {
1810                        format!("{}...", &entry.message[..100])
1811                    } else {
1812                        entry.message.clone()
1813                    };
1814                    list_args.push(msg);
1815                }
1816            }
1817
1818            let list_output = std::process::Command::new(&gui_tool)
1819                .args(&list_args)
1820                .output();
1821
1822            match list_output {
1823                Ok(out) => {
1824                    let output_str = output_to_string(&out);
1825                    if output_str.contains("Change Filters") {
1826                        // Go back to filter selection
1827                        break;
1828                    } else if out.status.success() {
1829                        // Refresh - continue loop
1830                        continue;
1831                    } else {
1832                        // Close
1833                        return;
1834                    }
1835                }
1836                Err(_) => return,
1837            }
1838        }
1839    }
1840}
1841
1842/// CLI command to view logs
1843pub fn view_logs_cli(lines: Option<usize>, level: Option<&str>, search: Option<&str>) {
1844    let show_error = level
1845        .map(|l| l.contains("error") || l == "all")
1846        .unwrap_or(true);
1847    let show_warning = level
1848        .map(|l| l.contains("warn") || l == "all")
1849        .unwrap_or(true);
1850    let show_info = level
1851        .map(|l| l.contains("info") || l == "all")
1852        .unwrap_or(true);
1853    let show_debug = level
1854        .map(|l| l.contains("debug") || l == "all")
1855        .unwrap_or(false);
1856
1857    let entries =
1858        crate::log::parse_log_deduplicated(show_error, show_warning, show_info, show_debug, search);
1859
1860    let limit = lines.unwrap_or(50);
1861
1862    println!("╔════════╦═══════╦═════════════════════╦════════════════════════════════════════════════════════════╗");
1863    println!("║ Level  ║ Count ║ Time                ║ Message                                                    ║");
1864    println!("╠════════╬═══════╬═════════════════════╬════════════════════════════════════════════════════════════╣");
1865
1866    for entry in entries.iter().take(limit) {
1867        let level_colored = match entry.level.as_str() {
1868            "ERROR" => format!("\x1b[31m{:6}\x1b[0m", entry.level),
1869            "WARN" => format!("\x1b[33m{:6}\x1b[0m", entry.level),
1870            "INFO" => format!("\x1b[32m{:6}\x1b[0m", entry.level),
1871            "DEBUG" => format!("\x1b[36m{:6}\x1b[0m", entry.level),
1872            _ => format!("{:6}", entry.level),
1873        };
1874
1875        let msg = if entry.message.len() > 58 {
1876            format!("{}...", &entry.message[..55])
1877        } else {
1878            entry.message.clone()
1879        };
1880
1881        println!(
1882            "║ {} ║ {:5} ║ {:19} ║ {:58} ║",
1883            level_colored,
1884            entry.count,
1885            &entry.timestamp[..std::cmp::min(19, entry.timestamp.len())],
1886            msg
1887        );
1888    }
1889
1890    println!("╚════════╩═══════╩═════════════════════╩════════════════════════════════════════════════════════════╝");
1891
1892    if entries.len() > limit {
1893        println!(
1894            "Showing {} of {} entries. Use --lines to see more.",
1895            limit,
1896            entries.len()
1897        );
1898    }
1899}
1900
1901// ============================================================================
1902// REGISTRY IMPORT
1903// ============================================================================
1904
1905fn run_registry_import_gui(wine_ctx: &crate::wine::WineContext) {
1906    let gui_tool = match crate::gui::get_gui_tool() {
1907        Some(tool) => tool,
1908        None => return,
1909    };
1910
1911    // Ask how to select the file
1912    let method_output = std::process::Command::new(&gui_tool)
1913        .args([
1914            "--list",
1915            "--title",
1916            "Registry Import",
1917            "--column",
1918            "Method",
1919            "--column",
1920            "Description",
1921            "--print-column",
1922            "1",
1923            "--width",
1924            "450",
1925            "--height",
1926            "200",
1927            "browse",
1928            "Browse for file",
1929            "manual",
1930            "Enter path manually",
1931        ])
1932        .output();
1933
1934    let method = match method_output {
1935        Ok(out) if out.status.success() => output_to_string(&out),
1936        _ => return,
1937    };
1938
1939    let reg_path = match method.as_str() {
1940        "browse" => {
1941            // File selection dialog for .reg files
1942            let output = std::process::Command::new(&gui_tool)
1943                .args([
1944                    "--file-selection",
1945                    "--title",
1946                    "Select Registry File to Import",
1947                    "--file-filter",
1948                    "Registry files | *.reg *.REG",
1949                ])
1950                .output();
1951
1952            match output {
1953                Ok(out) if out.status.success() => output_to_string(&out),
1954                _ => return,
1955            }
1956        }
1957        "manual" => {
1958            // Manual entry dialog
1959            let output = std::process::Command::new(&gui_tool)
1960                .args([
1961                    "--entry",
1962                    "--title",
1963                    "Enter Registry File Path",
1964                    "--text",
1965                    "Enter the full path to the .reg file:",
1966                    "--width",
1967                    "500",
1968                ])
1969                .output();
1970
1971            match output {
1972                Ok(out) if out.status.success() => output_to_string(&out),
1973                _ => return,
1974            }
1975        }
1976        _ => return,
1977    };
1978
1979    if reg_path.is_empty() {
1980        return;
1981    }
1982
1983    let path = std::path::Path::new(&reg_path);
1984    if !path.exists() {
1985        eprintln!("File not found: {}", reg_path);
1986        return;
1987    }
1988
1989    // Show confirmation with file preview
1990    if let Ok(content) = std::fs::read_to_string(path) {
1991        let preview = if content.len() > 500 {
1992            format!("{}...\n\n[truncated]", &content[..500])
1993        } else {
1994            content
1995        };
1996
1997        let confirm_output = std::process::Command::new(&gui_tool)
1998            .args([
1999                "--question",
2000                "--title",
2001                "Confirm Registry Import",
2002                "--text",
2003                &format!(
2004                    "Import this registry file?\n\nFile: {}\n\nPreview:\n{}",
2005                    path.file_name().unwrap_or_default().to_string_lossy(),
2006                    preview
2007                ),
2008                "--width",
2009                "600",
2010            ])
2011            .output();
2012
2013        match confirm_output {
2014            Ok(out) if out.status.success() => {}
2015            _ => {
2016                println!("Import cancelled.");
2017                return;
2018            }
2019        }
2020    }
2021
2022    // Import the registry file
2023    match wine_ctx.run_wine_no_cwd(&["regedit", "/S", &reg_path]) {
2024        Ok(output) => {
2025            if output.status.success() {
2026                println!("Registry file imported successfully: {}", reg_path);
2027
2028                // Show success dialog
2029                let _ = std::process::Command::new(&gui_tool)
2030                    .args([
2031                        "--info",
2032                        "--title",
2033                        "Registry Import",
2034                        "--text",
2035                        "Registry file imported successfully!",
2036                    ])
2037                    .output();
2038            } else {
2039                let stderr = String::from_utf8_lossy(&output.stderr);
2040                eprintln!("Registry import may have failed: {}", stderr);
2041
2042                let _ = std::process::Command::new(&gui_tool)
2043                    .args([
2044                        "--warning",
2045                        "--title",
2046                        "Registry Import",
2047                        "--text",
2048                        &format!("Registry import completed with warnings:\n{}", stderr),
2049                    ])
2050                    .output();
2051            }
2052        }
2053        Err(e) => {
2054            eprintln!("Failed to import registry file: {}", e);
2055
2056            let _ = std::process::Command::new(&gui_tool)
2057                .args([
2058                    "--error",
2059                    "--title",
2060                    "Registry Import Failed",
2061                    "--text",
2062                    &format!("Failed to import registry file:\n{}", e),
2063                ])
2064                .output();
2065        }
2066    }
2067}
2068
2069// ============================================================================
2070// CUSTOM VERB CREATOR GUI
2071// ============================================================================
2072
2073struct VerbData {
2074    name: String,
2075    title: String,
2076    publisher: String,
2077    year: String,
2078    category: String,
2079    action_type: String,
2080    installer_path: String,
2081    installer_args: String,
2082}
2083
2084impl Default for VerbData {
2085    fn default() -> Self {
2086        Self {
2087            name: String::new(),
2088            title: String::new(),
2089            publisher: String::new(),
2090            year: chrono_lite_now()
2091                .split('-')
2092                .next()
2093                .unwrap_or("2024")
2094                .to_string(),
2095            category: "app".to_string(),
2096            action_type: "local_installer".to_string(),
2097            installer_path: String::new(),
2098            installer_args: "/S".to_string(),
2099        }
2100    }
2101}
2102
2103impl VerbData {
2104    fn derive_name_from_title(&mut self) {
2105        self.name = self
2106            .title
2107            .to_lowercase()
2108            .chars()
2109            .filter(|c| c.is_alphanumeric() || *c == ' ')
2110            .collect::<String>()
2111            .replace(' ', "");
2112    }
2113
2114    fn to_toml(&self) -> String {
2115        let args_array = self
2116            .installer_args
2117            .split_whitespace()
2118            .map(|s| format!("\"{}\"", s))
2119            .collect::<Vec<_>>()
2120            .join(", ");
2121
2122        format!(
2123            r#"[verb]
2124name = "{}"
2125category = "{}"
2126title = "{}"
2127publisher = "{}"
2128year = "{}"
2129
2130[[actions]]
2131type = "{}"
2132path = "{}"
2133args = [{}]
2134"#,
2135            self.name,
2136            self.category,
2137            self.title,
2138            self.publisher,
2139            self.year,
2140            self.action_type,
2141            self.installer_path,
2142            args_array
2143        )
2144    }
2145
2146    fn from_toml(content: &str) -> Option<Self> {
2147        let mut data = Self::default();
2148
2149        for line in content.lines() {
2150            let line = line.trim();
2151            if line.is_empty() || line.starts_with('#') || line.starts_with('[') {
2152                continue;
2153            }
2154
2155            if let Some((key, value)) = line.split_once('=') {
2156                let key = key.trim();
2157                let value = value.trim().trim_matches('"');
2158
2159                match key {
2160                    "name" => data.name = value.to_string(),
2161                    "title" => data.title = value.to_string(),
2162                    "publisher" => data.publisher = value.to_string(),
2163                    "year" => data.year = value.to_string(),
2164                    "category" => data.category = value.to_string(),
2165                    "type" => data.action_type = value.to_string(),
2166                    "path" => data.installer_path = value.to_string(),
2167                    "args" => {
2168                        // Parse array like ["/S", "/D=path"]
2169                        let inner = value.trim_start_matches('[').trim_end_matches(']');
2170                        data.installer_args = inner
2171                            .split(',')
2172                            .map(|s| s.trim().trim_matches('"'))
2173                            .collect::<Vec<_>>()
2174                            .join(" ");
2175                    }
2176                    _ => {}
2177                }
2178            }
2179        }
2180
2181        if data.name.is_empty() && data.title.is_empty() {
2182            None
2183        } else {
2184            Some(data)
2185        }
2186    }
2187}
2188
2189fn run_verb_creator_gui() {
2190    let gui_tool = match crate::gui::get_gui_tool() {
2191        Some(tool) => tool,
2192        None => {
2193            eprintln!("No GUI tool available");
2194            return;
2195        }
2196    };
2197
2198    // Initial dialog: Import existing or create new?
2199    let output = std::process::Command::new(&gui_tool)
2200        .args([
2201            "--list",
2202            "--title",
2203            "Custom Verb Creator",
2204            "--column",
2205            "Option",
2206            "--column",
2207            "Description",
2208            "--print-column",
2209            "1",
2210            "--width",
2211            "500",
2212            "--height",
2213            "250",
2214            "new",
2215            "Create a new custom verb",
2216            "import",
2217            "Import existing TOML file",
2218        ])
2219        .output();
2220
2221    let mut verb_data = VerbData::default();
2222
2223    if let Ok(out) = output {
2224        if out.status.success() {
2225            let choice = output_to_string(&out);
2226            if choice == "import" {
2227                if let Some(data) = import_verb_toml_gui(&gui_tool) {
2228                    verb_data = data;
2229                } else {
2230                    return;
2231                }
2232            }
2233        } else {
2234            return;
2235        }
2236    } else {
2237        return;
2238    }
2239
2240    // Show advanced options checkbox
2241    let show_advanced = std::process::Command::new(&gui_tool)
2242        .args([
2243            "--question",
2244            "--title", "Verb Creator Mode",
2245            "--text", "Show advanced options?\n\nSimple mode derives some values automatically.\nAdvanced mode gives full control over all fields.",
2246            "--ok-label", "Advanced",
2247            "--cancel-label", "Simple",
2248            "--width", "400",
2249        ])
2250        .status()
2251        .map(|s| s.success())
2252        .unwrap_or(false);
2253
2254    // Run the appropriate editor
2255    let result = if show_advanced {
2256        edit_verb_advanced_gui(&gui_tool, &mut verb_data)
2257    } else {
2258        edit_verb_simple_gui(&gui_tool, &mut verb_data)
2259    };
2260
2261    if !result {
2262        return;
2263    }
2264
2265    // Save dialog
2266    save_verb_gui(&gui_tool, &verb_data);
2267}
2268
2269fn import_verb_toml_gui(gui_tool: &std::path::Path) -> Option<VerbData> {
2270    let output = std::process::Command::new(gui_tool)
2271        .args([
2272            "--file-selection",
2273            "--title",
2274            "Import TOML verb file",
2275            "--file-filter",
2276            "TOML files | *.toml",
2277        ])
2278        .output()
2279        .ok()?;
2280
2281    if !output.status.success() {
2282        return None;
2283    }
2284
2285    let path = output_to_string(&output);
2286    if path.is_empty() {
2287        return None;
2288    }
2289
2290    let content = std::fs::read_to_string(&path).ok()?;
2291    VerbData::from_toml(&content)
2292}
2293
2294fn edit_verb_simple_gui(gui_tool: &std::path::Path, data: &mut VerbData) -> bool {
2295    // Simple mode: just ask for title, publisher, and installer path
2296    // Name is derived from title, year is current year, category defaults to app
2297
2298    let output = std::process::Command::new(gui_tool)
2299        .args([
2300            "--forms",
2301            "--title",
2302            "Create Custom Verb (Simple)",
2303            "--text",
2304            "Enter verb details:\n(Name will be derived from title)",
2305            "--add-entry",
2306            "Title",
2307            "--add-entry",
2308            "Publisher",
2309            "--add-entry",
2310            "Installer Arguments",
2311            "--width",
2312            "500",
2313        ])
2314        .output();
2315
2316    if let Ok(out) = output {
2317        if !out.status.success() {
2318            return false;
2319        }
2320
2321        let output_str = output_to_string(&out);
2322        let values: Vec<String> = output_str.split('|').map(|s| s.to_string()).collect();
2323
2324        if values.len() >= 3 {
2325            data.title = values[0].clone();
2326            data.publisher = values[1].clone();
2327            data.installer_args = values[2].clone();
2328            data.derive_name_from_title();
2329        }
2330    } else {
2331        return false;
2332    }
2333
2334    // Select installer file
2335    let output = std::process::Command::new(gui_tool)
2336        .args([
2337            "--file-selection",
2338            "--title",
2339            "Select installer executable",
2340            "--file-filter",
2341            "Executables | *.exe *.msi",
2342        ])
2343        .output();
2344
2345    if let Ok(out) = output {
2346        if out.status.success() {
2347            data.installer_path = output_to_string(&out);
2348        } else {
2349            return false;
2350        }
2351    } else {
2352        return false;
2353    }
2354
2355    !data.title.is_empty() && !data.installer_path.is_empty()
2356}
2357
2358fn edit_verb_advanced_gui(gui_tool: &std::path::Path, data: &mut VerbData) -> bool {
2359    // Advanced mode: full control over all fields
2360
2361    // First, select category
2362    let output = std::process::Command::new(gui_tool)
2363        .args([
2364            "--list",
2365            "--title",
2366            "Select Category",
2367            "--column",
2368            "Category",
2369            "--column",
2370            "Description",
2371            "--print-column",
2372            "1",
2373            "--width",
2374            "400",
2375            "--height",
2376            "300",
2377            "app",
2378            "Application",
2379            "dll",
2380            "DLL/Runtime",
2381            "font",
2382            "Font",
2383            "setting",
2384            "Setting/Configuration",
2385            "custom",
2386            "Custom/Other",
2387        ])
2388        .output();
2389
2390    if let Ok(out) = output {
2391        if out.status.success() {
2392            data.category = output_to_string(&out);
2393        } else {
2394            return false;
2395        }
2396    } else {
2397        return false;
2398    }
2399
2400    // Select action type
2401    let output = std::process::Command::new(gui_tool)
2402        .args([
2403            "--list",
2404            "--title",
2405            "Select Action Type",
2406            "--column",
2407            "Type",
2408            "--column",
2409            "Description",
2410            "--print-column",
2411            "1",
2412            "--width",
2413            "500",
2414            "--height",
2415            "300",
2416            "local_installer",
2417            "Run a local installer file",
2418            "script",
2419            "Run a shell script",
2420            "override",
2421            "Set DLL override",
2422            "registry",
2423            "Import registry settings",
2424        ])
2425        .output();
2426
2427    if let Ok(out) = output {
2428        if out.status.success() {
2429            data.action_type = output_to_string(&out);
2430        } else {
2431            return false;
2432        }
2433    } else {
2434        return false;
2435    }
2436
2437    // Form for all text fields
2438    let output = std::process::Command::new(gui_tool)
2439        .args([
2440            "--forms",
2441            "--title",
2442            "Create Custom Verb (Advanced)",
2443            "--text",
2444            "Enter verb details:",
2445            "--add-entry",
2446            &format!("Name [{}]", data.name),
2447            "--add-entry",
2448            &format!("Title [{}]", data.title),
2449            "--add-entry",
2450            &format!("Publisher [{}]", data.publisher),
2451            "--add-entry",
2452            &format!("Year [{}]", data.year),
2453            "--add-entry",
2454            &format!("Arguments [{}]", data.installer_args),
2455            "--width",
2456            "500",
2457        ])
2458        .output();
2459
2460    if let Ok(out) = output {
2461        if !out.status.success() {
2462            return false;
2463        }
2464
2465        let output_str = output_to_string(&out);
2466        let values: Vec<String> = output_str.split('|').map(|s| s.to_string()).collect();
2467
2468        if values.len() >= 5 {
2469            if !values[0].is_empty() {
2470                data.name = values[0].clone();
2471            }
2472            if !values[1].is_empty() {
2473                data.title = values[1].clone();
2474            }
2475            if !values[2].is_empty() {
2476                data.publisher = values[2].clone();
2477            }
2478            if !values[3].is_empty() {
2479                data.year = values[3].clone();
2480            }
2481            if !values[4].is_empty() {
2482                data.installer_args = values[4].clone();
2483            }
2484        }
2485    } else {
2486        return false;
2487    }
2488
2489    // Select file based on action type
2490    let file_title = match data.action_type.as_str() {
2491        "local_installer" => "Select installer executable",
2492        "script" => "Select shell script",
2493        _ => "Select file",
2494    };
2495
2496    let file_filter = match data.action_type.as_str() {
2497        "local_installer" => "Executables | *.exe *.msi",
2498        "script" => "Shell scripts | *.sh",
2499        _ => "All files | *",
2500    };
2501
2502    if data.action_type == "local_installer" || data.action_type == "script" {
2503        let output = std::process::Command::new(gui_tool)
2504            .args([
2505                "--file-selection",
2506                "--title",
2507                file_title,
2508                "--file-filter",
2509                file_filter,
2510            ])
2511            .output();
2512
2513        if let Ok(out) = output {
2514            if out.status.success() {
2515                data.installer_path = output_to_string(&out);
2516            } else {
2517                return false;
2518            }
2519        } else {
2520            return false;
2521        }
2522    }
2523
2524    !data.name.is_empty() && !data.title.is_empty()
2525}
2526
2527fn save_verb_gui(gui_tool: &std::path::Path, data: &VerbData) {
2528    let toml_content = data.to_toml();
2529    let default_dir = crate::wine::custom::get_custom_verbs_dir();
2530
2531    // Ensure the directory exists
2532    std::fs::create_dir_all(&default_dir).ok();
2533
2534    // Ask Save or Save As
2535    let output = std::process::Command::new(gui_tool)
2536        .args([
2537            "--list",
2538            "--title",
2539            "Save Verb",
2540            "--column",
2541            "Option",
2542            "--column",
2543            "Description",
2544            "--print-column",
2545            "1",
2546            "--width",
2547            "500",
2548            "--height",
2549            "200",
2550            "save",
2551            &format!(
2552                "Save to default location (~/.config/protontool/verbs/{}.toml)",
2553                data.name
2554            ),
2555            "saveas",
2556            "Save As... (choose location)",
2557        ])
2558        .output();
2559
2560    let save_path = if let Ok(out) = output {
2561        if !out.status.success() {
2562            return;
2563        }
2564
2565        let choice = output_to_string(&out);
2566
2567        if choice == "saveas" {
2568            // Let user choose location
2569            let output = std::process::Command::new(gui_tool)
2570                .args([
2571                    "--file-selection",
2572                    "--save",
2573                    "--title",
2574                    "Save verb as...",
2575                    "--filename",
2576                    &format!("{}.toml", data.name),
2577                    "--file-filter",
2578                    "TOML files | *.toml",
2579                ])
2580                .output();
2581
2582            if let Ok(out) = output {
2583                if out.status.success() {
2584                    let path = output_to_string(&out);
2585                    if !path.is_empty() {
2586                        PathBuf::from(path)
2587                    } else {
2588                        return;
2589                    }
2590                } else {
2591                    return;
2592                }
2593            } else {
2594                return;
2595            }
2596        } else {
2597            // Save to default location
2598            default_dir.join(format!("{}.toml", data.name))
2599        }
2600    } else {
2601        return;
2602    };
2603
2604    // Write the file
2605    match std::fs::write(&save_path, &toml_content) {
2606        Ok(_) => {
2607            println!("Verb saved to: {}", save_path.display());
2608            let _ = std::process::Command::new(gui_tool)
2609                .args([
2610                    "--info",
2611                    "--title", "Verb Saved",
2612                    "--text", &format!("Custom verb '{}' saved successfully!\n\nLocation: {}\n\nRestart protontool to use the new verb.", data.name, save_path.display()),
2613                    "--width", "500",
2614                ])
2615                .status();
2616        }
2617        Err(e) => {
2618            eprintln!("Failed to save verb: {}", e);
2619            let _ = std::process::Command::new(gui_tool)
2620                .args([
2621                    "--error",
2622                    "--title",
2623                    "Save Failed",
2624                    "--text",
2625                    &format!("Failed to save verb: {}", e),
2626                    "--width",
2627                    "400",
2628                ])
2629                .status();
2630        }
2631    }
2632}
2633
2634fn run_list_mode(parsed: &util::ParsedArgs, no_term: bool) {
2635    let extra_libs = parsed.get_multi_option("steam_library").to_vec();
2636    let verbose = parsed.get_count("verbose") > 0;
2637
2638    let (steam_path, steam_root, steam_lib_paths) = match get_steam_context(no_term, &extra_libs) {
2639        Some(ctx) => ctx,
2640        None => {
2641            exit_with_error("No Steam installation was selected.", no_term);
2642        }
2643    };
2644
2645    if verbose {
2646        println!("Steam path: {}", steam_path.display());
2647        println!("Steam root: {}", steam_root.display());
2648        println!("Library paths searched:");
2649        for lib in &steam_lib_paths {
2650            println!("  - {}", lib.display());
2651        }
2652        println!();
2653    }
2654
2655    let steam_apps = get_steam_apps(&steam_root, &steam_path, &steam_lib_paths);
2656
2657    if verbose {
2658        println!("Total apps found: {}", steam_apps.len());
2659        println!(
2660            "Apps with Proton prefix (Windows apps): {}",
2661            steam_apps.iter().filter(|a| a.is_windows_app()).count()
2662        );
2663        println!(
2664            "Proton installations: {}",
2665            steam_apps.iter().filter(|a| a.is_proton).count()
2666        );
2667        println!();
2668
2669        if steam_apps.iter().filter(|a| a.is_windows_app()).count() == 0 {
2670            println!("No Windows apps found. Showing all detected apps:");
2671            for app in &steam_apps {
2672                println!(
2673                    "  {} ({}) - proton: {}, has_prefix: {}",
2674                    app.name,
2675                    app.appid,
2676                    app.is_proton,
2677                    app.prefix_path.is_some()
2678                );
2679            }
2680            println!();
2681        }
2682    }
2683
2684    let matching_apps: Vec<_> = if parsed.get_flag("list") {
2685        steam_apps
2686            .iter()
2687            .filter(|app| app.is_windows_app())
2688            .collect()
2689    } else if let Some(search) = parsed.get_option("search") {
2690        steam_apps
2691            .iter()
2692            .filter(|app| app.is_windows_app() && app.name_contains(search))
2693            .collect()
2694    } else {
2695        vec![]
2696    };
2697
2698    if !matching_apps.is_empty() {
2699        println!("Found the following games:");
2700        for app in &matching_apps {
2701            println!("{} ({})", app.name, app.appid);
2702        }
2703        println!("\nTo run protontool for the chosen game, run:");
2704        println!("$ protontool APPID COMMAND");
2705    } else {
2706        println!("Found no games.");
2707    }
2708
2709    println!("\nNOTE: A game must be launched at least once before protontool can find the game.");
2710}
2711
2712fn run_verb_mode(appid: u32, verbs: &[String], parsed: &util::ParsedArgs, no_term: bool) {
2713    let extra_libs = parsed.get_multi_option("steam_library").to_vec();
2714    let (steam_path, steam_root, steam_lib_paths) = match get_steam_context(no_term, &extra_libs) {
2715        Some(ctx) => ctx,
2716        None => {
2717            exit_with_error("No Steam installation was selected.", no_term);
2718        }
2719    };
2720
2721    let steam_apps = get_steam_apps(&steam_root, &steam_path, &steam_lib_paths);
2722
2723    let steam_app = match steam_apps
2724        .iter()
2725        .find(|app| app.appid == appid && app.is_windows_app())
2726    {
2727        Some(app) => app.clone(),
2728        None => {
2729            exit_with_error(
2730                "Steam app with the given app ID could not be found. Is it installed and have you launched it at least once?",
2731                no_term
2732            );
2733        }
2734    };
2735
2736    let proton_app = match find_proton_app(&steam_path, &steam_apps, appid) {
2737        Some(app) => app,
2738        None => {
2739            exit_with_error("Proton installation could not be found!", no_term);
2740        }
2741    };
2742
2743    if !proton_app.is_proton_ready {
2744        exit_with_error(
2745            "Proton installation is incomplete. Have you launched a Steam app using this Proton version at least once?",
2746            no_term
2747        );
2748    }
2749
2750    let prefix_path = steam_app.prefix_path.as_ref().unwrap();
2751    let verb_runner = Wine::new(&proton_app, prefix_path);
2752
2753    // Run each specified verb
2754    let mut success = true;
2755    for verb_name in verbs {
2756        // Skip if it looks like a flag (starts with -)
2757        if verb_name.starts_with('-') {
2758            continue;
2759        }
2760
2761        println!("Running verb: {}", verb_name);
2762        match verb_runner.run_verb(verb_name) {
2763            Ok(()) => println!("Successfully completed: {}", verb_name),
2764            Err(e) => {
2765                eprintln!("Error running {}: {}", verb_name, e);
2766                success = false;
2767            }
2768        }
2769    }
2770
2771    if success {
2772        process::exit(0);
2773    } else {
2774        process::exit(1);
2775    }
2776}
2777
2778fn run_command_mode(appid: Option<u32>, command: &str, parsed: &util::ParsedArgs, no_term: bool) {
2779    let extra_libs = parsed.get_multi_option("steam_library").to_vec();
2780    let (steam_path, steam_root, steam_lib_paths) = match get_steam_context(no_term, &extra_libs) {
2781        Some(ctx) => ctx,
2782        None => {
2783            exit_with_error("No Steam installation was selected.", no_term);
2784        }
2785    };
2786
2787    let steam_apps = get_steam_apps(&steam_root, &steam_path, &steam_lib_paths);
2788
2789    let appid = match appid {
2790        Some(id) => id,
2791        None => {
2792            exit_with_error("APPID is required for -c/--command mode", no_term);
2793        }
2794    };
2795
2796    let steam_app = match steam_apps
2797        .iter()
2798        .find(|app| app.appid == appid && app.is_windows_app())
2799    {
2800        Some(app) => app.clone(),
2801        None => {
2802            exit_with_error(
2803                "Steam app with the given app ID could not be found.",
2804                no_term,
2805            );
2806        }
2807    };
2808
2809    let proton_app = match find_proton_app(&steam_path, &steam_apps, appid) {
2810        Some(app) => app,
2811        None => {
2812            exit_with_error("Proton installation could not be found!", no_term);
2813        }
2814    };
2815
2816    // Use built-in wine context to run the command
2817    let prefix_path = steam_app.prefix_path.as_ref().unwrap();
2818    let wine_ctx = crate::wine::WineContext::from_proton(&proton_app, prefix_path);
2819
2820    let cwd_app = parsed.get_flag("cwd_app");
2821    let _cwd = if cwd_app {
2822        Some(steam_app.install_path.to_string_lossy().to_string())
2823    } else {
2824        None
2825    };
2826
2827    // Start background wineserver if requested
2828    if parsed.get_flag("background_wineserver") {
2829        if let Err(e) = wine_ctx.start_wineserver() {
2830            eprintln!("Warning: Failed to start background wineserver: {}", e);
2831        }
2832    }
2833
2834    // Run the command with wine
2835    match wine_ctx.run_wine(&[command]) {
2836        Ok(output) => {
2837            if !output.stdout.is_empty() {
2838                println!("{}", String::from_utf8_lossy(&output.stdout));
2839            }
2840            if !output.stderr.is_empty() {
2841                eprintln!("{}", String::from_utf8_lossy(&output.stderr));
2842            }
2843            process::exit(output.status.code().unwrap_or(0));
2844        }
2845        Err(e) => {
2846            exit_with_error(&format!("Failed to run command: {}", e), no_term);
2847        }
2848    }
2849}
2850
2851fn run_prefix_command_mode(
2852    prefix_path: &str,
2853    command: &str,
2854    parsed: &util::ParsedArgs,
2855    no_term: bool,
2856) {
2857    let prefix_path = PathBuf::from(prefix_path);
2858
2859    if !prefix_path.exists() {
2860        exit_with_error(
2861            &format!("Prefix path does not exist: {}", prefix_path.display()),
2862            no_term,
2863        );
2864    }
2865
2866    let extra_libs = parsed.get_multi_option("steam_library").to_vec();
2867    let (steam_path, steam_root, steam_lib_paths) = match get_steam_context(no_term, &extra_libs) {
2868        Some(ctx) => ctx,
2869        None => {
2870            exit_with_error("No Steam installation was selected.", no_term);
2871        }
2872    };
2873
2874    let steam_apps = get_steam_apps(&steam_root, &steam_path, &steam_lib_paths);
2875
2876    // Try to read saved Proton and arch info from prefix metadata
2877    let metadata_path = prefix_path.join(".protontool");
2878    let metadata_content = std::fs::read_to_string(&metadata_path).ok();
2879
2880    let proton_app = if let Some(ref metadata) = metadata_content {
2881        let proton_name = metadata
2882            .lines()
2883            .find(|l| l.starts_with("proton_name="))
2884            .and_then(|l| l.strip_prefix("proton_name="));
2885
2886        if let Some(name) = proton_name {
2887            find_proton_by_name(&steam_apps, name)
2888        } else {
2889            None
2890        }
2891    } else {
2892        None
2893    };
2894
2895    // Read saved architecture (default to win64)
2896    let saved_arch = metadata_content
2897        .as_ref()
2898        .and_then(|m| m.lines().find(|l| l.starts_with("arch=")))
2899        .and_then(|l| l.strip_prefix("arch="))
2900        .and_then(crate::wine::WineArch::from_str)
2901        .unwrap_or(crate::wine::WineArch::Win64);
2902
2903    // If no saved Proton or --proton flag specified, select one
2904    let proton_app = if let Some(proton_name) = parsed.get_option("proton") {
2905        match find_proton_by_name(&steam_apps, proton_name) {
2906            Some(app) => app,
2907            None => {
2908                exit_with_error(
2909                    &format!("Proton version '{}' not found.", proton_name),
2910                    no_term,
2911                );
2912            }
2913        }
2914    } else if let Some(app) = proton_app {
2915        println!("Using saved Proton version: {}", app.name);
2916        app
2917    } else {
2918        match select_proton_with_gui(&get_proton_apps(&steam_apps)) {
2919            Some(app) => app,
2920            None => {
2921                exit_with_error("No Proton version selected.", no_term);
2922            }
2923        }
2924    };
2925
2926    if !proton_app.is_proton_ready {
2927        exit_with_error("Proton installation is not ready.", no_term);
2928    }
2929
2930    let wine_ctx =
2931        crate::wine::WineContext::from_proton_with_arch(&proton_app, &prefix_path, saved_arch);
2932
2933    // Start background wineserver if requested
2934    if parsed.get_flag("background_wineserver") {
2935        if let Err(e) = wine_ctx.start_wineserver() {
2936            eprintln!("Warning: Failed to start background wineserver: {}", e);
2937        }
2938    }
2939
2940    // Run the command with wine
2941    match wine_ctx.run_wine(&[command]) {
2942        Ok(output) => {
2943            if !output.stdout.is_empty() {
2944                println!("{}", String::from_utf8_lossy(&output.stdout));
2945            }
2946            if !output.stderr.is_empty() {
2947                eprintln!("{}", String::from_utf8_lossy(&output.stderr));
2948            }
2949            process::exit(output.status.code().unwrap_or(0));
2950        }
2951        Err(e) => {
2952            exit_with_error(&format!("Failed to run command: {}", e), no_term);
2953        }
2954    }
2955}
2956
2957fn run_create_prefix_mode(prefix_path: &str, parsed: &util::ParsedArgs, no_term: bool) {
2958    let extra_libs = parsed.get_multi_option("steam_library").to_vec();
2959    let (steam_path, steam_root, steam_lib_paths) = match get_steam_context(no_term, &extra_libs) {
2960        Some(ctx) => ctx,
2961        None => {
2962            exit_with_error("No Steam installation was selected.", no_term);
2963        }
2964    };
2965
2966    let steam_apps = get_steam_apps(&steam_root, &steam_path, &steam_lib_paths);
2967    let proton_apps = get_proton_apps(&steam_apps);
2968
2969    if proton_apps.is_empty() {
2970        exit_with_error(
2971            "No Proton installations found. Please install Proton through Steam first.",
2972            no_term,
2973        );
2974    }
2975
2976    // Find Proton version - either from --proton flag or let user select
2977    let proton_app = if let Some(proton_name) = parsed.get_option("proton") {
2978        match find_proton_by_name(&steam_apps, proton_name) {
2979            Some(app) => app,
2980            None => {
2981                eprintln!("Available Proton versions:");
2982                for app in &proton_apps {
2983                    eprintln!("  - {}", app.name);
2984                }
2985                exit_with_error(
2986                    &format!("Proton version '{}' not found.", proton_name),
2987                    no_term,
2988                );
2989            }
2990        }
2991    } else {
2992        match select_proton_with_gui(&proton_apps) {
2993            Some(app) => app,
2994            None => {
2995                exit_with_error("No Proton version selected.", no_term);
2996            }
2997        }
2998    };
2999
3000    if !proton_app.is_proton_ready {
3001        exit_with_error(
3002            "Selected Proton installation is not ready. Please launch a game with this Proton version first to initialize it.",
3003            no_term
3004        );
3005    }
3006
3007    let prefix_path = PathBuf::from(prefix_path);
3008
3009    // Parse architecture option (default to win64)
3010    let arch = parsed
3011        .get_option("arch")
3012        .and_then(|s| crate::wine::WineArch::from_str(s))
3013        .unwrap_or(crate::wine::WineArch::Win64);
3014
3015    // Create the prefix directory structure
3016    println!("Creating Wine prefix at: {}", prefix_path.display());
3017    println!("Using Proton: {}", proton_app.name);
3018    println!("Architecture: {}", arch.as_str());
3019
3020    if let Err(e) = std::fs::create_dir_all(&prefix_path) {
3021        exit_with_error(
3022            &format!("Failed to create prefix directory: {}", e),
3023            no_term,
3024        );
3025    }
3026
3027    // Initialize the prefix with Proton's wine
3028    let wine_ctx = crate::wine::WineContext::from_proton_with_arch(&proton_app, &prefix_path, arch);
3029    // Proton uses "files" subdirectory, older versions may use "dist"
3030    let dist_dir = {
3031        let files_dir = proton_app.install_path.join("files");
3032        let dist_dir = proton_app.install_path.join("dist");
3033        if files_dir.exists() {
3034            files_dir
3035        } else {
3036            dist_dir
3037        }
3038    };
3039
3040    println!("Initializing prefix...");
3041    if let Err(e) = crate::wine::prefix::init_prefix(&prefix_path, &dist_dir, true, Some(&wine_ctx))
3042    {
3043        exit_with_error(&format!("Failed to initialize prefix: {}", e), no_term);
3044    }
3045
3046    // Save prefix metadata for future use
3047    let metadata_path = prefix_path.join(".protontool");
3048    let metadata = format!(
3049        "proton_name={}\nproton_path={}\narch={}\ncreated={}\n",
3050        proton_app.name,
3051        proton_app.install_path.display(),
3052        arch.as_str(),
3053        chrono_lite_now()
3054    );
3055    std::fs::write(&metadata_path, metadata).ok();
3056
3057    println!("\nPrefix created successfully!");
3058    println!("\nTo use this prefix:");
3059    println!("  protontool --prefix '{}' <verbs>", prefix_path.display());
3060    println!(
3061        "  protontool --prefix '{}' -c <command>",
3062        prefix_path.display()
3063    );
3064}
3065
3066fn run_delete_prefix_mode(prefix_path: &str, no_term: bool) {
3067    let prefix_path = PathBuf::from(prefix_path);
3068
3069    if !prefix_path.exists() {
3070        exit_with_error(
3071            &format!("Prefix path does not exist: {}", prefix_path.display()),
3072            no_term,
3073        );
3074    }
3075
3076    let prefix_name = prefix_path
3077        .file_name()
3078        .and_then(|n| n.to_str())
3079        .unwrap_or("Unknown");
3080
3081    // Confirm deletion
3082    println!(
3083        "Are you sure you want to delete the prefix '{}'?",
3084        prefix_name
3085    );
3086    println!("Path: {}", prefix_path.display());
3087    println!();
3088    print!("Type 'yes' to confirm: ");
3089    std::io::Write::flush(&mut std::io::stdout()).ok();
3090
3091    let mut input = String::new();
3092    if std::io::stdin().read_line(&mut input).is_err() {
3093        exit_with_error("Failed to read input.", no_term);
3094    }
3095
3096    if input.trim().to_lowercase() != "yes" {
3097        println!("Deletion cancelled.");
3098        return;
3099    }
3100
3101    // Delete the prefix directory
3102    match std::fs::remove_dir_all(&prefix_path) {
3103        Ok(()) => {
3104            println!("Prefix '{}' deleted successfully.", prefix_name);
3105        }
3106        Err(e) => {
3107            exit_with_error(&format!("Failed to delete prefix: {}", e), no_term);
3108        }
3109    }
3110}
3111
3112fn run_custom_prefix_mode(
3113    prefix_path: &str,
3114    verbs: &[String],
3115    parsed: &util::ParsedArgs,
3116    no_term: bool,
3117) {
3118    let prefix_path = PathBuf::from(prefix_path);
3119
3120    if !prefix_path.exists() {
3121        exit_with_error(
3122            &format!("Prefix path does not exist: {}", prefix_path.display()),
3123            no_term,
3124        );
3125    }
3126
3127    let extra_libs = parsed.get_multi_option("steam_library").to_vec();
3128    let (steam_path, steam_root, steam_lib_paths) = match get_steam_context(no_term, &extra_libs) {
3129        Some(ctx) => ctx,
3130        None => {
3131            exit_with_error("No Steam installation was selected.", no_term);
3132        }
3133    };
3134
3135    let steam_apps = get_steam_apps(&steam_root, &steam_path, &steam_lib_paths);
3136    let proton_apps = get_proton_apps(&steam_apps);
3137
3138    // Try to read saved Proton and arch info from prefix metadata
3139    let metadata_path = prefix_path.join(".protontool");
3140    let metadata_content = std::fs::read_to_string(&metadata_path).ok();
3141
3142    let proton_app = if let Some(ref metadata) = metadata_content {
3143        let proton_name = metadata
3144            .lines()
3145            .find(|l| l.starts_with("proton_name="))
3146            .and_then(|l| l.strip_prefix("proton_name="));
3147
3148        if let Some(name) = proton_name {
3149            find_proton_by_name(&steam_apps, name)
3150        } else {
3151            None
3152        }
3153    } else {
3154        None
3155    };
3156
3157    // Read saved architecture (default to win64)
3158    let saved_arch = metadata_content
3159        .as_ref()
3160        .and_then(|m| m.lines().find(|l| l.starts_with("arch=")))
3161        .and_then(|l| l.strip_prefix("arch="))
3162        .and_then(crate::wine::WineArch::from_str)
3163        .unwrap_or(crate::wine::WineArch::Win64);
3164
3165    // If no saved Proton or --proton flag specified, select one
3166    let proton_app = if let Some(proton_name) = parsed.get_option("proton") {
3167        match find_proton_by_name(&steam_apps, proton_name) {
3168            Some(app) => app,
3169            None => {
3170                exit_with_error(
3171                    &format!("Proton version '{}' not found.", proton_name),
3172                    no_term,
3173                );
3174            }
3175        }
3176    } else if let Some(app) = proton_app {
3177        println!("Using saved Proton version: {}", app.name);
3178        app
3179    } else {
3180        match select_proton_with_gui(&proton_apps) {
3181            Some(app) => app,
3182            None => {
3183                exit_with_error("No Proton version selected.", no_term);
3184            }
3185        }
3186    };
3187
3188    if !proton_app.is_proton_ready {
3189        exit_with_error("Proton installation is not ready.", no_term);
3190    }
3191
3192    let verb_runner = Wine::new_with_arch(&proton_app, &prefix_path, saved_arch);
3193
3194    if verbs.is_empty() {
3195        // Interactive mode - show verb selection
3196        loop {
3197            let category = match select_verb_category_gui() {
3198                Some(cat) => cat,
3199                None => return,
3200            };
3201
3202            let verb_list = verb_runner.list_verbs(Some(category));
3203            let selected = select_verbs_with_gui(
3204                &verb_list,
3205                Some(&format!("Select {} to install", category.as_str())),
3206            );
3207
3208            if selected.is_empty() {
3209                continue;
3210            }
3211
3212            for verb_name in &selected {
3213                println!("Running verb: {}", verb_name);
3214                if let Err(e) = verb_runner.run_verb(verb_name) {
3215                    eprintln!("Error running {}: {}", verb_name, e);
3216                }
3217            }
3218
3219            println!("Completed running verbs.");
3220        }
3221    } else {
3222        // Run specified verbs
3223        for verb_name in verbs {
3224            if verb_name.starts_with('-') {
3225                continue;
3226            }
3227            println!("Running verb: {}", verb_name);
3228            match verb_runner.run_verb(verb_name) {
3229                Ok(()) => println!("Successfully completed: {}", verb_name),
3230                Err(e) => eprintln!("Error running {}: {}", verb_name, e),
3231            }
3232        }
3233    }
3234}
3235
3236fn chrono_lite_now() -> String {
3237    use std::time::{SystemTime, UNIX_EPOCH};
3238    let duration = SystemTime::now()
3239        .duration_since(UNIX_EPOCH)
3240        .unwrap_or_default();
3241    format!("{}", duration.as_secs())
3242}