package ui import ( "epochsilicon/pkg/log" "epochsilicon/pkg/patching" "errors" "fmt" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/widget" "os" "os/exec" "path/filepath" "strings" "howett.net/plist" "epochsilicon/pkg/paths" "epochsilicon/pkg/utils" ) // showOptionsPopup creates and shows an integrated popup window for options func showOptionsPopup() { if currentWindow == nil { return } // Create General tab content generalTitle := widget.NewLabel("General Settings") generalTitle.TextStyle = fyne.TextStyle{Bold: true} generalContainer := container.NewVBox( generalTitle, widget.NewSeparator(), advancedLoggingCheckbox, metalHudCheckbox, showTerminalCheckbox, autoDeleteWdbCheckbox, widget.NewSeparator(), container.NewBorder(nil, nil, nil, container.NewHBox(enableOptionAsAltButton, disableOptionAsAltButton), optionAsAltStatusLabel), ) // Create Environment Variables tab content envVarsTitle := widget.NewLabel("Environment Variables") envVarsTitle.TextStyle = fyne.TextStyle{Bold: true} envVarsContainer := container.NewVBox( envVarsTitle, widget.NewSeparator(), envVarsEntry, ) // Create tabs tabs := container.NewAppTabs( container.NewTabItem("General", container.NewScroll(generalContainer)), container.NewTabItem("Environment", container.NewScroll(envVarsContainer)), ) // Set tab location to top tabs.SetTabLocation(container.TabLocationTop) // Create close button closeButton := widget.NewButton("Close", func() { // This will be set when the popup is created }) // Create the popup content with close button popupContent := container.NewBorder( nil, // top container.NewCenter(closeButton), // bottom nil, // left nil, // right container.NewPadded(tabs), // center ) // Get the window size and calculate 2/3 size windowSize := currentWindow.Content().Size() popupWidth := windowSize.Width * 5 / 6 popupHeight := windowSize.Height * 9 / 10 // Create a modal popup popup := widget.NewModalPopUp(popupContent, currentWindow.Canvas()) popup.Resize(fyne.NewSize(popupWidth, popupHeight)) // Set the close button action to hide the popup closeButton.OnTapped = func() { if remapOperationInProgress { // Show warning popup instead of closing showRemapWarningPopup() } else { popup.Hide() } } popup.Show() } // showRemapWarningPopup shows a warning popup when user tries to close options during remap operation func showRemapWarningPopup() { if currentWindow == nil { return } // Create warning content warningTitle := widget.NewRichTextFromMarkdown("# ⚠️ Please Wait") warningMessage := widget.NewRichTextFromMarkdown("**Remap operation is in progress.**\n\nThe wine registry is being modified. This will take a moment.\n\nPlease wait for the operation to complete before closing the options.") // Create OK button okButton := widget.NewButton("OK", func() { // This will be set when the popup is created }) okButton.Importance = widget.HighImportance // Create warning content container warningContent := container.NewVBox( container.NewCenter(warningTitle), widget.NewSeparator(), warningMessage, widget.NewSeparator(), container.NewCenter(okButton), ) // Calculate smaller popup size windowSize := currentWindow.Content().Size() popupWidth := windowSize.Width * 2 / 3 popupHeight := windowSize.Height / 2 // Create the warning popup warningPopup := widget.NewModalPopUp(container.NewPadded(warningContent), currentWindow.Canvas()) warningPopup.Resize(fyne.NewSize(popupWidth, popupHeight)) // Set the OK button action to hide the warning popup okButton.OnTapped = func() { warningPopup.Hide() } warningPopup.Show() } // showTroubleshootingPopup creates and shows a popup window for troubleshooting actions func showTroubleshootingPopup() { if currentWindow == nil { return } // --- CrossOver Version Check --- crossoverVersion := getCrossoverVersion(paths.CrossoverPath) var crossoverStatusShort *widget.Label var crossoverStatusDetail *widget.Label if crossoverVersion == "" { crossoverStatusShort = widget.NewLabel("Not found") crossoverStatusDetail = widget.NewLabel("") } else if isCrossoverVersionRecommended(crossoverVersion) { crossoverStatusShort = widget.NewLabelWithStyle("✔ "+crossoverVersion, fyne.TextAlignTrailing, fyne.TextStyle{Bold: true}) crossoverStatusDetail = widget.NewLabelWithStyle("✔ Recommended version of CrossOver installed", fyne.TextAlignLeading, fyne.TextStyle{Italic: true}) } else { crossoverStatusShort = widget.NewLabelWithStyle("⚠️ "+crossoverVersion, fyne.TextAlignTrailing, fyne.TextStyle{Italic: true}) crossoverStatusDetail = widget.NewLabelWithStyle("⚠️ Please update to CrossOver 25.0.1 or later!", fyne.TextAlignLeading, fyne.TextStyle{Italic: true}) } crossoverStatusDetail.Wrapping = fyne.TextWrapWord // --- Delete WDB Directory --- wdbDeleteButton = widget.NewButton("Delete", func() { wdbPath := filepath.Join(paths.EpochPath, "WDB") if !utils.DirExists(wdbPath) { dialog.ShowInformation("WDB Not Found", "No WDB directory found in your Epoch folder.", currentWindow) return } dialog.NewConfirm("Delete WDB Directory", "Are you sure you want to delete the WDB directory? This will remove all cached data. No important data will be lost.", func(confirm bool) { if confirm { err := os.RemoveAll(wdbPath) if err != nil { dialog.ShowError(fmt.Errorf("Failed to delete WDB: %v", err), currentWindow) } else { dialog.ShowInformation("WDB Deleted", "WDB directory deleted successfully.", currentWindow) } } }, currentWindow).Show() }) // --- Delete Wine Prefixes --- wineDeleteButton = widget.NewButton("Delete", func() { homeDir, _ := os.UserHomeDir() userWine := filepath.Join(homeDir, ".wine") turtleWine := filepath.Join(paths.EpochPath, ".wine") msg := "Are you sure you want to delete the following Wine prefixes?\n\n- " + userWine + "\n- " + turtleWine + "\n\nThis cannot be undone." dialog.NewConfirm("Delete Wine Prefixes", msg, func(confirm bool) { if confirm { err1 := os.RemoveAll(userWine) err2 := os.RemoveAll(turtleWine) if err1 != nil && !os.IsNotExist(err1) { dialog.ShowError(fmt.Errorf("Failed to delete ~/.wine: %v", err1), currentWindow) return } if err2 != nil && !os.IsNotExist(err2) { dialog.ShowError(fmt.Errorf("Failed to delete Epoch/.wine: %v", err2), currentWindow) return } dialog.ShowInformation("Wine Prefixes Deleted", "Wine prefixes deleted successfully.", currentWindow) } }, currentWindow).Show() }) // --- Build Rosettax87 Locally --- buildRosettaButton = widget.NewButton("Build", func() { msg := "Building rosettax87 on your computer may speed up launch times. This requires xcode-commandline-tools and Cmake to be installed. See the instructions at https://git.burkey.co/eburk/epochsilicon/README.md" dialog.NewConfirm("Build rosettax87", msg, func(confirm bool) { if confirm { // Check for dependencies if _, err := exec.LookPath("clang"); err != nil { m := fmt.Errorf("xcode command line tools are not installed on your computer. Click the Website button in the app and read the instructions on building rosettax87 before trying again") log.Error(m.Error()) dialog.ShowError(m, currentWindow) return } if _, err := exec.LookPath("cmake"); err != nil { m := fmt.Errorf("Cmake is not installed on your computer. Click the Website button in the app and read the instructions on building rosettax87 before trying again") log.Error(m.Error()) dialog.ShowError(m, currentWindow) return } xPath, lPath, err := patching.BuildRosetta() if err != nil { m := fmt.Errorf("Error building rosettax87: %v\nClick on File Issue to upload your logs and file a support issue", err) log.Error(m.Error()) dialog.ShowError(m, currentWindow) return } d := filepath.Join(paths.EpochPath, "rosettax87") if err = os.RemoveAll(d); err != nil { m := fmt.Errorf("Error removing existing rosettax87 directory: %v", err) log.Error(m.Error()) dialog.ShowError(m, currentWindow) return } if err = os.MkdirAll(d, 0755); err != nil { m := fmt.Errorf("Error creating existing rosettax87 directory: %v", err) log.Error(m.Error()) dialog.ShowError(m, currentWindow) return } pathMap := map[string]string{ xPath: filepath.Join(paths.EpochPath, "rosettax87", "rosettax87"), lPath: filepath.Join(paths.EpochPath, "rosettax87", "libRuntimeRosettax87"), } for srcPath, destPath := range pathMap { fBytes, err := os.ReadFile(srcPath) if err != nil { errMsg := fmt.Sprintf("failed to read source file %s: %v", srcPath, err) dialog.ShowError(errors.New(errMsg), currentWindow) log.Debug(errMsg) return } err = os.WriteFile(destPath, fBytes, 0755) if err != nil { errMsg := fmt.Sprintf("failed to write file %s: %v", destPath, err) dialog.ShowError(errors.New(errMsg), currentWindow) log.Debug(errMsg) return } log.Debugf("Successfully copied %s to %s", srcPath, destPath) } dialog.ShowInformation("Build Successful", "Rosettax87 installed successfully.", currentWindow) } }, currentWindow).Show() }) troubleshootingTitle := widget.NewLabel("Troubleshooting") troubleshootingTitle.TextStyle = fyne.TextStyle{Bold: true} rowCrossover := container.NewBorder(nil, nil, widget.NewLabel("CrossOver version:"), crossoverStatusShort, nil) rowWDB := container.NewBorder(nil, nil, widget.NewLabel("Delete WDB directory (cache):"), wdbDeleteButton, nil) rowWine := container.NewBorder(nil, nil, widget.NewLabel("Delete Wine prefixes (~/.wine & Epoch/.wine):"), wineDeleteButton, nil) rowBuildRosetta := container.NewBorder(nil, nil, widget.NewLabel("Build Rosettax87 locally):"), buildRosettaButton, nil) appMgmtNote := widget.NewLabel("Please ensure EpochSilicon is enabled in System Settings > Privacy & Security > App Management.") appMgmtNote.Wrapping = fyne.TextWrapWord appMgmtNote.TextStyle = fyne.TextStyle{Italic: true} content := container.NewVBox( troubleshootingTitle, widget.NewSeparator(), rowCrossover, crossoverStatusDetail, rowWDB, rowWine, rowBuildRosetta, appMgmtNote, ) scrollContainer := container.NewScroll(content) troubleshootingCloseButton = widget.NewButton("Close", func() {}) popupContent := container.NewBorder( nil, // top container.NewCenter(troubleshootingCloseButton), // bottom nil, // left nil, // right container.NewPadded(scrollContainer), // center ) windowSize := currentWindow.Content().Size() popupWidth := windowSize.Width * 5 / 6 popupHeight := windowSize.Height * 5 / 6 popup := widget.NewModalPopUp(popupContent, currentWindow.Canvas()) popup.Resize(fyne.NewSize(popupWidth, popupHeight)) troubleshootingCloseButton.OnTapped = func() { popup.Hide() } popup.Show() } // getCrossoverVersion reads the Info.plist and returns the version string, or "" if not found func getCrossoverVersion(appPath string) string { if appPath == "" { return "" } plistPath := filepath.Join(appPath, "Contents", "Info.plist") f, err := os.Open(plistPath) if err != nil { return "" } defer f.Close() var data struct { Version string `plist:"CFBundleShortVersionString"` } decoder := plist.NewDecoder(f) if err := decoder.Decode(&data); err != nil { return "" } return data.Version } // isCrossoverVersionRecommended returns true if version >= 25.0.1 func isCrossoverVersionRecommended(version string) bool { parts := strings.Split(version, ".") if len(parts) < 3 { return false } major := parts[0] minor := parts[1] patch := parts[2] if major > "25" { return true } if major == "25" && minor >= "0" && patch >= "1" { return true } return false }