1pub 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
29pub 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 run_gui_mode(no_term);
169 return;
170 }
171 println!("{}", parser.help());
172 return;
173 }
174
175 let do_prefix_command = do_command && do_use_prefix;
177
178 let action_count = if do_prefix_command {
179 1 } 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
226fn 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
250fn run_gui_mode(no_term: bool) {
252 loop {
254 let action = match show_main_menu_gui() {
255 Some(a) => a,
256 None => return, };
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
268fn run_gui_manage_game(no_term: bool) {
270 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 loop {
322 let category = match select_verb_category_gui() {
323 Some(cat) => cat,
324 None => return, };
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; }
336
337 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
349fn run_gui_create_prefix(no_term: bool) {
351 let prefix_name = match get_prefix_name_gui() {
353 Some(name) => name,
354 None => return,
355 };
356
357 let prefix_path = match select_prefix_location_gui(&prefix_name) {
359 Some(path) => path,
360 None => return,
361 };
362
363 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 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 arch = match select_arch_gui() {
396 Some(a) => a,
397 None => return,
398 };
399
400 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 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 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
444fn run_gui_delete_prefix(no_term: bool) {
446 let prefixes_dir = crate::config::get_prefixes_dir();
447
448 std::fs::create_dir_all(&prefixes_dir).ok();
450
451 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 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 match std::fs::remove_dir_all(&prefix_path) {
497 Ok(()) => {
498 println!("Prefix '{}' deleted successfully.", prefix_name);
499
500 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
532fn run_gui_manage_prefix(no_term: bool) {
534 let prefixes_dir = crate::config::get_prefixes_dir();
536
537 std::fs::create_dir_all(&prefixes_dir).ok();
539
540 let prefix_path = match select_custom_prefix_gui(&prefixes_dir) {
542 Some(path) => path,
543 None => return,
544 };
545
546 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 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 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 loop {
607 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 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
697enum PrefixAction {
699 RunApplication,
700 InstallComponents,
701 WineTools,
702 Settings,
703 CreateVerb,
704}
705
706fn 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
757fn 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
786fn 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
823fn 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
874enum PrefixSetting {
876 Dpi,
877 DllOverride,
878 WindowsVersion,
879 VirtualDesktop,
880 Theme,
881 RegistryImport,
882 ViewLogs,
883}
884
885fn 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
941fn select_dpi_gui() -> Option<u32> {
943 let gui_tool = crate::gui::get_gui_tool()?;
944
945 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
989fn set_wine_dpi(wine_ctx: &crate::wine::WineContext, dpi: u32) {
991 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 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(®_file, ®_content) {
1006 eprintln!("Failed to write registry file: {}", e);
1007 return;
1008 }
1009
1010 match wine_ctx.run_wine_no_cwd(&["regedit", "/S", ®_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 std::fs::remove_file(®_file).ok();
1021}
1022
1023fn 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 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
1079fn add_dll_override_gui(gui_tool: &std::path::Path, wine_ctx: &crate::wine::WineContext) {
1081 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 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 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(®_file, ®_content) {
1154 eprintln!("Failed to write registry file: {}", e);
1155 return;
1156 }
1157
1158 match wine_ctx.run_wine_no_cwd(&["regedit", "/S", ®_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(®_file).ok();
1164}
1165
1166fn remove_dll_override_gui(gui_tool: &std::path::Path, wine_ctx: &crate::wine::WineContext) {
1167 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 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(®_file, ®_content) {
1201 eprintln!("Failed to write registry file: {}", e);
1202 return;
1203 }
1204
1205 match wine_ctx.run_wine_no_cwd(&["regedit", "/S", ®_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(®_file).ok();
1211}
1212
1213fn list_dll_overrides_gui(gui_tool: &std::path::Path, wine_ctx: &crate::wine::WineContext) {
1214 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 let mut overrides = Vec::new();
1225 for line in stdout.lines() {
1226 let line = line.trim();
1227 if line.contains("REG_SZ") {
1228 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
1258fn 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 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(®_file, ®_content) {
1358 eprintln!("Failed to write registry file: {}", e);
1359 return;
1360 }
1361
1362 match wine_ctx.run_wine_no_cwd(&["regedit", "/S", ®_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(®_file).ok();
1368}
1369
1370fn 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 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(®_file, ®_content) {
1480 eprintln!("Failed to write registry file: {}", e);
1481 return;
1482 }
1483
1484 match wine_ctx.run_wine_no_cwd(&["regedit", "/S", ®_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(®_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(®_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", ®_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(®_file).ok();
1511}
1512
1513fn select_theme_gui(wine_ctx: &crate::wine::WineContext) -> Option<String> {
1518 let gui_tool = crate::gui::get_gui_tool()?;
1519
1520 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 "(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 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 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 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 ("".to_string(), "".to_string())
1603 } else {
1604 let theme_path = format!(
1606 "C:\\\\windows\\\\Resources\\\\Themes\\\\{}\\\\{}.msstyles",
1607 theme, theme
1608 );
1609 ("NormalColor".to_string(), theme_path)
1610 };
1611
1612 let themes_dir = prefix_path.join("drive_c/windows/Resources/Themes");
1614 std::fs::create_dir_all(&themes_dir).ok();
1615
1616 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(®_file, ®_content) {
1642 eprintln!("Failed to write registry file: {}", e);
1643 return;
1644 }
1645
1646 match wine_ctx.run_wine_no_cwd(&["regedit", "/S", ®_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(®_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 if !msstyles_path.exists() {
1666 std::fs::create_dir_all(&theme_dir).ok();
1667 std::fs::write(&msstyles_path, b"").ok();
1669 }
1670}
1671
1672struct 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
1696pub 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 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, };
1748
1749 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 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 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 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 break;
1828 } else if out.status.success() {
1829 continue;
1831 } else {
1832 return;
1834 }
1835 }
1836 Err(_) => return,
1837 }
1838 }
1839 }
1840}
1841
1842pub 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
1901fn 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 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 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 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(®_path);
1984 if !path.exists() {
1985 eprintln!("File not found: {}", reg_path);
1986 return;
1987 }
1988
1989 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 match wine_ctx.run_wine_no_cwd(&["regedit", "/S", ®_path]) {
2024 Ok(output) => {
2025 if output.status.success() {
2026 println!("Registry file imported successfully: {}", reg_path);
2027
2028 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
2069struct 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 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 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 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 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_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 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 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 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 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 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 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 std::fs::create_dir_all(&default_dir).ok();
2533
2534 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 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 default_dir.join(format!("{}.toml", data.name))
2599 }
2600 } else {
2601 return;
2602 };
2603
2604 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 let mut success = true;
2755 for verb_name in verbs {
2756 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 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 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 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 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 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 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 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 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 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 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 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 let wine_ctx = crate::wine::WineContext::from_proton_with_arch(&proton_app, &prefix_path, arch);
3029 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 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 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 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 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 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 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 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 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}