added option key remap

This commit is contained in:
aomizu
2025-06-08 21:21:33 +09:00
parent fec5577d57
commit ac893a1c19
8 changed files with 647 additions and 12 deletions

View File

@@ -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")
}

View File

@@ -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
}
}

View File

@@ -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())

View File

@@ -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

View File

@@ -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
)

View File

@@ -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) {

440
pkg/utils/wine_registry.go Normal file
View File

@@ -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
}

Binary file not shown.