diff --git a/main_test.go b/main_test.go deleted file mode 100644 index 64e753e..0000000 --- a/main_test.go +++ /dev/null @@ -1,10 +0,0 @@ -package main - -import ( - "testing" -) - -func TestMain(t *testing.T) { - // Basic test to ensure the main package can be imported and tested - t.Log("TurtleSilicon main package test passed") -} diff --git a/pkg/ui/components.go b/pkg/ui/components.go index d3f0a8f..77517fa 100644 --- a/pkg/ui/components.go +++ b/pkg/ui/components.go @@ -3,6 +3,8 @@ package ui import ( "log" "net/url" + "strings" + "time" "turtlesilicon/pkg/launcher" "turtlesilicon/pkg/patching" @@ -45,6 +47,9 @@ func createOptionsComponents() { vanillaTweaksCheckbox.SetChecked(prefs.EnableVanillaTweaks) launcher.EnableVanillaTweaks = prefs.EnableVanillaTweaks + // Create Wine registry Option-as-Alt buttons and status + createWineRegistryComponents() + // Load environment variables from preferences if prefs.EnvironmentVariables != "" { launcher.CustomEnvVars = prefs.EnvironmentVariables @@ -158,3 +163,182 @@ func createBottomBar(myWindow fyne.Window) fyne.CanvasObject { return container.NewPadded(bottomContainer) } + +// createWineRegistryComponents creates Wine registry Option-as-Alt buttons and status +func createWineRegistryComponents() { + // Create status label to show current state + optionAsAltStatusLabel = widget.NewRichText() + + // Create enable button + enableOptionAsAltButton = widget.NewButton("Enable", func() { + enableOptionAsAltButton.Disable() + disableOptionAsAltButton.Disable() + + // Show loading state in status label + fyne.Do(func() { + optionAsAltStatusLabel.ParseMarkdown("**Remap Option key as Alt key:** Enabling...") + startPulsingEffect() + }) + + // Run in goroutine to avoid blocking UI + go func() { + if err := utils.SetOptionAsAltEnabled(true); err != nil { + log.Printf("Failed to enable Option-as-Alt mapping: %v", err) + // Update UI on main thread + fyne.Do(func() { + stopPulsingEffect() + optionAsAltStatusLabel.ParseMarkdown("**Remap Option key as Alt key:** Enable Failed") + }) + time.Sleep(2 * time.Second) // Show error briefly + } else { + log.Printf("Successfully enabled Option-as-Alt mapping") + // Update preferences + prefs, _ := utils.LoadPrefs() + prefs.RemapOptionAsAlt = true + utils.SavePrefs(prefs) + } + + // Update UI on main thread + fyne.Do(func() { + stopPulsingEffect() + updateWineRegistryStatusWithMethod(true) // Use Wine command for accurate check after modifications + }) + }() + }) + + // Create disable button + disableOptionAsAltButton = widget.NewButton("Disable", func() { + enableOptionAsAltButton.Disable() + disableOptionAsAltButton.Disable() + + // Show loading state in status label + fyne.Do(func() { + optionAsAltStatusLabel.ParseMarkdown("**Remap Option key as Alt key:** Disabling...") + startPulsingEffect() + }) + + // Run in goroutine to avoid blocking UI + go func() { + if err := utils.SetOptionAsAltEnabled(false); err != nil { + log.Printf("Failed to disable Option-as-Alt mapping: %v", err) + // Update UI on main thread + fyne.Do(func() { + stopPulsingEffect() + optionAsAltStatusLabel.ParseMarkdown("**Remap Option key as Alt key:** Disable Failed") + }) + time.Sleep(2 * time.Second) // Show error briefly + } else { + log.Printf("Successfully disabled Option-as-Alt mapping") + // Update preferences + prefs, _ := utils.LoadPrefs() + prefs.RemapOptionAsAlt = false + utils.SavePrefs(prefs) + } + + // Update UI on main thread + fyne.Do(func() { + stopPulsingEffect() + updateWineRegistryStatusWithMethod(true) // Use Wine command for accurate check after modifications + }) + }() + }) + + // Style the buttons similar to other action buttons + enableOptionAsAltButton.Importance = widget.MediumImportance + disableOptionAsAltButton.Importance = widget.MediumImportance + + // Initialize status and button states + updateWineRegistryStatus() +} + +// updateWineRegistryStatus updates the Wine registry status label and button states +func updateWineRegistryStatus() { + updateWineRegistryStatusWithMethod(false) +} + +// updateWineRegistryStatusWithMethod updates status with choice of checking method +func updateWineRegistryStatusWithMethod(useWineCommand bool) { + if useWineCommand { + // Use Wine command for accurate check after modifications + currentWineRegistryEnabled = utils.CheckOptionAsAltEnabled() + } else { + // Use fast file-based check for regular status updates + currentWineRegistryEnabled = utils.CheckOptionAsAltEnabledFast() + } + + // Update UI with simple white text + if currentWineRegistryEnabled { + optionAsAltStatusLabel.ParseMarkdown("**Remap Option key as Alt key:** Enabled") + } else { + optionAsAltStatusLabel.ParseMarkdown("**Remap Option key as Alt key:** Disabled") + } + + // Update button states based on current status + if currentWineRegistryEnabled { + // If enabled, only show disable button as clickable + enableOptionAsAltButton.Disable() + disableOptionAsAltButton.Enable() + } else { + // If disabled, only show enable button as clickable + enableOptionAsAltButton.Enable() + disableOptionAsAltButton.Disable() + } +} + +// startPulsingEffect starts a pulsing animation for the status label during loading +func startPulsingEffect() { + if pulsingActive { + return // Already pulsing + } + + pulsingActive = true + pulsingTicker = time.NewTicker(500 * time.Millisecond) + + go func() { + dots := "" + + for pulsingActive { + select { + case <-pulsingTicker.C: + if pulsingActive { + // Cycle through different dot patterns for visual effect + switch len(dots) { + case 0: + dots = "." + case 1: + dots = ".." + case 2: + dots = "..." + default: + dots = "" + } + + // Update the label with pulsing dots + fyne.Do(func() { + if pulsingActive && optionAsAltStatusLabel != nil { + // Use the dots directly in the status text + if strings.Contains(optionAsAltStatusLabel.String(), "Enabling") { + optionAsAltStatusLabel.ParseMarkdown("**Remap Option key as Alt key:** Enabling" + dots) + } else if strings.Contains(optionAsAltStatusLabel.String(), "Disabling") { + optionAsAltStatusLabel.ParseMarkdown("**Remap Option key as Alt key:** Disabling" + dots) + } + } + }) + } + } + } + }() +} + +// stopPulsingEffect stops the pulsing animation +func stopPulsingEffect() { + if !pulsingActive { + return + } + + pulsingActive = false + if pulsingTicker != nil { + pulsingTicker.Stop() + pulsingTicker = nil + } +} diff --git a/pkg/ui/popup.go b/pkg/ui/popup.go index 18f7e2e..513bb95 100644 --- a/pkg/ui/popup.go +++ b/pkg/ui/popup.go @@ -21,6 +21,8 @@ func showOptionsPopup() { metalHudCheckbox, showTerminalCheckbox, vanillaTweaksCheckbox, + widget.NewSeparator(), + container.NewBorder(nil, nil, nil, container.NewHBox(enableOptionAsAltButton, disableOptionAsAltButton), optionAsAltStatusLabel), ) envVarsTitle := widget.NewLabel("Environment Variables") @@ -55,8 +57,8 @@ func showOptionsPopup() { // Get the window size and calculate 2/3 size windowSize := currentWindow.Content().Size() - popupWidth := windowSize.Width * 2 / 3 - popupHeight := windowSize.Height * 2 / 3 + popupWidth := windowSize.Width * 5 / 6 + popupHeight := windowSize.Height * 5 / 6 // Create a modal popup popup := widget.NewModalPopUp(popupContent, currentWindow.Canvas()) diff --git a/pkg/ui/status.go b/pkg/ui/status.go index 6dd6e16..8741443 100644 --- a/pkg/ui/status.go +++ b/pkg/ui/status.go @@ -25,6 +25,11 @@ func UpdateAllStatuses() { updateTurtleWoWStatus() updatePlayButtonState() updateServiceStatus() + + // Update Wine registry status if components are initialized + if optionAsAltStatusLabel != nil { + updateWineRegistryStatus() + } } // updateCrossoverStatus updates CrossOver path and patch status diff --git a/pkg/ui/variables.go b/pkg/ui/variables.go index f22c469..01c987d 100644 --- a/pkg/ui/variables.go +++ b/pkg/ui/variables.go @@ -1,6 +1,8 @@ package ui import ( + "time" + "fyne.io/fyne/v2" "fyne.io/fyne/v2/widget" ) @@ -30,9 +32,20 @@ var ( showTerminalCheckbox *widget.Check vanillaTweaksCheckbox *widget.Check + // Wine registry buttons and status + enableOptionAsAltButton *widget.Button + disableOptionAsAltButton *widget.Button + optionAsAltStatusLabel *widget.RichText + // Environment variables entry envVarsEntry *widget.Entry // Window reference for popup functionality currentWindow fyne.Window + + // State variables + currentWineRegistryEnabled bool + + // Pulsing effect variables (pulsingActive is in status.go) + pulsingTicker *time.Ticker ) diff --git a/pkg/utils/prefs.go b/pkg/utils/prefs.go index 63f0da3..5bd0f4b 100644 --- a/pkg/utils/prefs.go +++ b/pkg/utils/prefs.go @@ -14,6 +14,7 @@ type UserPrefs struct { SaveSudoPassword bool `json:"save_sudo_password"` ShowTerminalNormally bool `json:"show_terminal_normally"` EnableVanillaTweaks bool `json:"enable_vanilla_tweaks"` + RemapOptionAsAlt bool `json:"remap_option_as_alt"` } func getPrefsPath() (string, error) { diff --git a/pkg/utils/wine_registry.go b/pkg/utils/wine_registry.go new file mode 100644 index 0000000..0943489 --- /dev/null +++ b/pkg/utils/wine_registry.go @@ -0,0 +1,440 @@ +package utils + +import ( + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strings" +) + +const ( + wineRegistrySection = "[Software\\Wine\\Mac Driver]" + leftOptionKey = "\"LeftOptionIsAlt\"=\"Y\"" + rightOptionKey = "\"RightOptionIsAlt\"=\"Y\"" + wineLoaderPath = "/Applications/CrossOver.app/Contents/SharedSupport/CrossOver/CrossOver-Hosted Application/wineloader2" + registryKeyPath = "HKEY_CURRENT_USER\\Software\\Wine\\Mac Driver" +) + +// GetWineUserRegPath returns the path to the Wine user.reg file +func GetWineUserRegPath() (string, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("failed to get user home directory: %v", err) + } + return filepath.Join(homeDir, ".wine", "user.reg"), nil +} + +// queryRegistryValue queries a specific registry value using Wine's reg command +func queryRegistryValue(winePrefix, valueName string) bool { + cmd := exec.Command(wineLoaderPath, "reg", "query", registryKeyPath, "/v", valueName) + cmd.Env = append(os.Environ(), fmt.Sprintf("WINEPREFIX=%s", winePrefix)) + + output, err := cmd.Output() + if err != nil { + // Value doesn't exist or other error + return false + } + + // Check if the output contains the value set to "Y" + outputStr := string(output) + return strings.Contains(outputStr, valueName) && strings.Contains(outputStr, "Y") +} + +func setRegistryValuesOptimized(winePrefix string, enabled bool) error { + if enabled { + return addBothRegistryValues(winePrefix) + } else { + return deleteBothRegistryValues(winePrefix) + } +} + +func addBothRegistryValues(winePrefix string) error { + batchContent := fmt.Sprintf(`@echo off +reg add "%s" /v "LeftOptionIsAlt" /t REG_SZ /d "Y" /f +reg add "%s" /v "RightOptionIsAlt" /t REG_SZ /d "Y" /f +`, registryKeyPath, registryKeyPath) + + // Create temporary batch file + tempDir := os.TempDir() + batchFile := filepath.Join(tempDir, "wine_registry_add.bat") + + if err := os.WriteFile(batchFile, []byte(batchContent), 0644); err != nil { + return fmt.Errorf("failed to create batch file: %v", err) + } + defer os.Remove(batchFile) + + // Run the batch file with Wine + cmd := exec.Command(wineLoaderPath, "cmd", "/c", batchFile) + cmd.Env = append(os.Environ(), fmt.Sprintf("WINEPREFIX=%s", winePrefix)) + + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("batch registry add failed: %v, output: %s", err, string(output)) + } + + log.Printf("Successfully enabled Option-as-Alt mapping in Wine registry (optimized)") + return nil +} + +func deleteBothRegistryValues(winePrefix string) error { + batchContent := fmt.Sprintf(`@echo off +reg delete "%s" /v "LeftOptionIsAlt" /f 2>nul +reg delete "%s" /v "RightOptionIsAlt" /f 2>nul +`, registryKeyPath, registryKeyPath) + + // Create temporary batch file + tempDir := os.TempDir() + batchFile := filepath.Join(tempDir, "wine_registry_delete.bat") + + if err := os.WriteFile(batchFile, []byte(batchContent), 0644); err != nil { + return fmt.Errorf("failed to create batch file: %v", err) + } + defer os.Remove(batchFile) // Clean up + + // Run the batch file with Wine + cmd := exec.Command(wineLoaderPath, "cmd", "/c", batchFile) + cmd.Env = append(os.Environ(), fmt.Sprintf("WINEPREFIX=%s", winePrefix)) + + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("batch registry delete failed: %v, output: %s", err, string(output)) + } + + log.Printf("Successfully disabled Option-as-Alt mapping in Wine registry (optimized)") + return nil +} + +// CheckOptionAsAltEnabled checks if Option keys are remapped as Alt keys in Wine registry +func CheckOptionAsAltEnabled() bool { + homeDir, err := os.UserHomeDir() + if err != nil { + log.Printf("Failed to get user home directory: %v", err) + return false + } + + winePrefix := filepath.Join(homeDir, ".wine") + + // Check if CrossOver wine loader exists + if !PathExists(wineLoaderPath) { + log.Printf("CrossOver wine loader not found at: %s", wineLoaderPath) + return false + } + + // Query both registry values + leftEnabled := queryRegistryValue(winePrefix, "LeftOptionIsAlt") + rightEnabled := queryRegistryValue(winePrefix, "RightOptionIsAlt") + + return leftEnabled && rightEnabled +} + +// CheckOptionAsAltEnabledFast checks status by reading user.reg file directly +func CheckOptionAsAltEnabledFast() bool { + regPath, err := GetWineUserRegPath() + if err != nil { + log.Printf("Failed to get Wine registry path: %v", err) + return false + } + + if !PathExists(regPath) { + log.Printf("Wine user.reg file not found at: %s", regPath) + return false + } + + content, err := os.ReadFile(regPath) + if err != nil { + log.Printf("Failed to read Wine registry file: %v", err) + return false + } + + contentStr := string(content) + + // Check for the Mac Driver section in different possible formats + macDriverSectionFound := strings.Contains(contentStr, wineRegistrySection) || + strings.Contains(contentStr, "[SoftwareWineMac Driver]") + + if macDriverSectionFound { + // Look for the registry values in the proper format or any format + leftOptionFound := strings.Contains(contentStr, leftOptionKey) || + strings.Contains(contentStr, "\"LeftOptionIsAlt\"=\"Y\"") + rightOptionFound := strings.Contains(contentStr, rightOptionKey) || + strings.Contains(contentStr, "\"RightOptionIsAlt\"=\"Y\"") + + return leftOptionFound && rightOptionFound + } + + return false +} + +// SetOptionAsAltEnabled enables or disables Option key remapping in Wine registry +func SetOptionAsAltEnabled(enabled bool) error { + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get user home directory: %v", err) + } + + winePrefix := filepath.Join(homeDir, ".wine") + + // Check if CrossOver wine loader exists + if !PathExists(wineLoaderPath) { + return fmt.Errorf("CrossOver wine loader not found at: %s", wineLoaderPath) + } + + // Ensure the .wine directory exists + if err := os.MkdirAll(winePrefix, 0755); err != nil { + return fmt.Errorf("failed to create .wine directory: %v", err) + } + + if enabled { + return setRegistryValuesOptimized(winePrefix, true) + } else { + err := setRegistryValuesOptimized(winePrefix, false) + if err != nil { + log.Printf("Wine registry disable failed: %v", err) + } + + err2 := setRegistryValuesFast(false) + if err2 != nil { + log.Printf("File-based cleanup failed: %v", err2) + } + + if err != nil { + return err + } + return err2 + } +} + +// setRegistryValuesFast directly modifies the user.reg file (much faster) +func setRegistryValuesFast(enabled bool) error { + regPath, err := GetWineUserRegPath() + if err != nil { + return fmt.Errorf("failed to get Wine registry path: %v", err) + } + + // Ensure the .wine directory exists + wineDir := filepath.Dir(regPath) + if err := os.MkdirAll(wineDir, 0755); err != nil { + return fmt.Errorf("failed to create .wine directory: %v", err) + } + + var content string + var lines []string + + // Read existing content if file exists + if PathExists(regPath) { + contentBytes, err := os.ReadFile(regPath) + if err != nil { + return fmt.Errorf("failed to read existing registry file: %v", err) + } + content = string(contentBytes) + lines = strings.Split(content, "\n") + } else { + // Create basic registry structure if file doesn't exist + content = "WINE REGISTRY Version 2\n;; All keys relative to \\User\n\n" + lines = strings.Split(content, "\n") + } + + if enabled { + return addOptionAsAltSettingsFast(regPath, lines) + } else { + return removeOptionAsAltSettingsFast(regPath, lines) + } +} + +// addOptionAsAltSettingsFast adds the Option-as-Alt registry settings directly to the file +func addOptionAsAltSettingsFast(regPath string, lines []string) error { + var newLines []string + sectionFound := false + sectionIndex := -1 + leftOptionFound := false + rightOptionFound := false + + // Find the Mac Driver section + for i, line := range lines { + if strings.TrimSpace(line) == wineRegistrySection { + sectionFound = true + sectionIndex = i + break + } + } + + if !sectionFound { + // Add the section at the end + newLines = append(lines, "") + newLines = append(newLines, wineRegistrySection) + newLines = append(newLines, "#time=1dbd859c084de18") + newLines = append(newLines, leftOptionKey) + newLines = append(newLines, rightOptionKey) + } else { + // Section exists, check if keys are already present + newLines = make([]string, len(lines)) + copy(newLines, lines) + + // Look for existing keys in the section + for i := sectionIndex + 1; i < len(lines); i++ { + line := strings.TrimSpace(lines[i]) + if line == "" { + continue + } + if strings.HasPrefix(line, "[") && line != wineRegistrySection { + // Found start of another section, stop looking + break + } + if strings.Contains(line, "LeftOptionIsAlt") { + leftOptionFound = true + if !strings.Contains(line, "\"Y\"") { + newLines[i] = leftOptionKey + } + } + if strings.Contains(line, "RightOptionIsAlt") { + rightOptionFound = true + if !strings.Contains(line, "\"Y\"") { + newLines[i] = rightOptionKey + } + } + } + + // Add missing keys + if !leftOptionFound || !rightOptionFound { + insertIndex := sectionIndex + 1 + + // Add timestamp if it doesn't exist + timestampExists := false + for i := sectionIndex + 1; i < len(newLines); i++ { + if strings.HasPrefix(strings.TrimSpace(newLines[i]), "#time=") { + timestampExists = true + break + } + if strings.HasPrefix(strings.TrimSpace(newLines[i]), "[") && newLines[i] != wineRegistrySection { + break + } + } + + if !timestampExists { + timestampLine := "#time=1dbd859c084de18" + newLines = insertLine(newLines, insertIndex, timestampLine) + insertIndex++ + } + + if !leftOptionFound { + newLines = insertLine(newLines, insertIndex, leftOptionKey) + insertIndex++ + } + if !rightOptionFound { + newLines = insertLine(newLines, insertIndex, rightOptionKey) + } + } + } + + // Write the updated content + newContent := strings.Join(newLines, "\n") + if err := os.WriteFile(regPath, []byte(newContent), 0644); err != nil { + return fmt.Errorf("failed to write registry file: %v", err) + } + + log.Printf("Successfully enabled Option-as-Alt mapping in Wine registry (fast method)") + return nil +} + +// removeOptionAsAltSettingsFast removes the Option-as-Alt registry settings directly from the file +func removeOptionAsAltSettingsFast(regPath string, lines []string) error { + if !PathExists(regPath) { + // File doesn't exist, nothing to remove + log.Printf("Successfully disabled Option-as-Alt mapping in Wine registry (no file to modify)") + return nil + } + + var newLines []string + + // Remove lines that contain our option key settings from any section + for _, line := range lines { + trimmedLine := strings.TrimSpace(line) + + // Skip lines that contain our option key settings + if strings.Contains(trimmedLine, "LeftOptionIsAlt") || strings.Contains(trimmedLine, "RightOptionIsAlt") { + continue + } + + newLines = append(newLines, line) + } + + // Check if any Mac Driver sections are now empty and remove them + newLines = removeEmptyMacDriverSections(newLines) + + // Write the updated content + newContent := strings.Join(newLines, "\n") + if err := os.WriteFile(regPath, []byte(newContent), 0644); err != nil { + return fmt.Errorf("failed to write registry file: %v", err) + } + + log.Printf("Successfully disabled Option-as-Alt mapping in Wine registry (fast method)") + return nil +} + +// removeEmptyMacDriverSections removes empty Mac Driver sections from the registry +func removeEmptyMacDriverSections(lines []string) []string { + var finalLines []string + i := 0 + + for i < len(lines) { + line := strings.TrimSpace(lines[i]) + + // Check if this is a Mac Driver section + if line == wineRegistrySection || line == "[SoftwareWineMac Driver]" { + // Check if the section is empty (only contains timestamp or nothing) + sectionStart := i + sectionEnd := i + 1 + sectionEmpty := true + + // Find the end of this section + for sectionEnd < len(lines) { + nextLine := strings.TrimSpace(lines[sectionEnd]) + if nextLine == "" { + sectionEnd++ + continue + } + if strings.HasPrefix(nextLine, "[") { + // Start of new section + break + } + if !strings.HasPrefix(nextLine, "#time=") { + // Found non-timestamp content in section + sectionEmpty = false + } + sectionEnd++ + } + + if sectionEmpty { + // Skip the entire empty section + i = sectionEnd + continue + } else { + // Keep the section + for j := sectionStart; j < sectionEnd; j++ { + finalLines = append(finalLines, lines[j]) + } + i = sectionEnd + continue + } + } + + finalLines = append(finalLines, lines[i]) + i++ + } + + return finalLines +} + +// insertLine inserts a line at the specified index +func insertLine(lines []string, index int, newLine string) []string { + if index >= len(lines) { + return append(lines, newLine) + } + + lines = append(lines, "") + copy(lines[index+1:], lines[index:]) + lines[index] = newLine + return lines +} diff --git a/winerosetta/libSiliconPatch.dll b/winerosetta/libSiliconPatch.dll index 1b980e4..3f9ae85 100644 Binary files a/winerosetta/libSiliconPatch.dll and b/winerosetta/libSiliconPatch.dll differ