Files
EpochSilicon/pkg/utils/wine_registry.go
2025-07-23 08:59:16 -07:00

441 lines
12 KiB
Go

package utils
import (
"epochsilicon/pkg/log"
"fmt"
"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.Infof("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.Infof("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.Infof("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.Infof("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.Infof("Failed to get Wine registry path: %v", err)
return false
}
if !PathExists(regPath) {
log.Infof("Wine user.reg file not found at: %s", regPath)
return false
}
content, err := os.ReadFile(regPath)
if err != nil {
log.Infof("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.Infof("Wine registry disable failed: %v", err)
}
err2 := setRegistryValuesFast(false)
if err2 != nil {
log.Infof("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.Infof("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.Infof("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.Infof("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
}