Add global Wine preferences tab
Phase 3 of the Wine/Proton stage (decisions 6, 9, 13, 19 in docs/testing/wine-proton.md): a global-only Wine tab modeled on the Pronunciation page, with the controller-only checkbox and a typed editable program list. Wine values flow through the preferences protocol so a global Apply reaches the live Wine settings from Phase 2 with no reader restart. Entries are validated as edited and named in-tab, blocking Apply until fixed; case-insensitive duplicates collapse. Per-application preferences expose no Wine editing and an app-scoped Apply cannot change the governing list.
This commit is contained in:
1 parent
dc6a961032
commit
75f34ea767
15 files changed
+543
-5
No files matched your search
@@ -0,0 +1,112 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package preferences
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
#include "native.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/config"
|
||||
)
|
||||
|
||||
func cloneWineSettings(wine config.WineSettings) config.WineSettings {
|
||||
wine.ExcludedPrograms = append([]string(nil), wine.ExcludedPrograms...)
|
||||
return wine
|
||||
}
|
||||
|
||||
// validateWinePrograms reports the first entry the Wine tab cannot accept,
|
||||
// naming it so the user can fix or delete exactly that entry. Config
|
||||
// validation remains the backstop for these rules.
|
||||
func validateWinePrograms(programs []string) error {
|
||||
if len(programs) > 256 {
|
||||
return fmt.Errorf("Wine program \"%s\" is entry %d. At most 256 programs can be listed; delete an entry.", programs[256], 257)
|
||||
}
|
||||
for index, name := range programs {
|
||||
switch {
|
||||
case strings.TrimSpace(name) == "":
|
||||
return fmt.Errorf("Wine program entry %d is blank. Type an executable name such as Game.exe, or delete the entry.", index+1)
|
||||
case strings.ContainsAny(name, `/\`):
|
||||
return fmt.Errorf("Wine program \"%s\" is a path. Type only the executable name, such as Game.exe, or delete the entry.", name)
|
||||
case len(name) > 255:
|
||||
return fmt.Errorf("Wine program \"%s\" is too long. Executable names may be at most 255 characters; fix or delete the entry.", name)
|
||||
case strings.TrimSpace(name) != name || strings.ContainsAny(name, "\x00\r\n"):
|
||||
return fmt.Errorf("Wine program \"%s\" is not a valid executable name. Remove the surrounding spaces or control characters, or delete the entry.", name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// collapseWinePrograms keeps the first spelling of each program and drops
|
||||
// later case-insensitive duplicates. An empty list stays nil so merged
|
||||
// settings keep their existing shape.
|
||||
func collapseWinePrograms(programs []string) []string {
|
||||
var collapsed []string
|
||||
seen := make(map[string]bool, len(programs))
|
||||
for _, name := range programs {
|
||||
key := strings.ToLower(name)
|
||||
if seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
collapsed = append(collapsed, name)
|
||||
}
|
||||
return collapsed
|
||||
}
|
||||
|
||||
func normalizedWineSettings(wine config.WineSettings) (config.WineSettings, error) {
|
||||
if err := validateWinePrograms(wine.ExcludedPrograms); err != nil {
|
||||
return config.WineSettings{}, err
|
||||
}
|
||||
wine.ExcludedPrograms = collapseWinePrograms(wine.ExcludedPrograms)
|
||||
return wine, nil
|
||||
}
|
||||
|
||||
func setWineValidation(message string) {
|
||||
text := C.CString(message)
|
||||
defer C.free(unsafe.Pointer(text))
|
||||
C.prefs_wine_validation(text)
|
||||
}
|
||||
|
||||
func setWineValues(wine config.WineSettings) {
|
||||
setCheck("wineControllerOnlyCheckButton", wine.ControllerOnly)
|
||||
C.prefs_wine_clear()
|
||||
for _, program := range wine.ExcludedPrograms {
|
||||
name := C.CString(program)
|
||||
C.prefs_wine_append(name)
|
||||
C.free(unsafe.Pointer(name))
|
||||
}
|
||||
setWineValidation("")
|
||||
}
|
||||
|
||||
func getWinePrograms() []string {
|
||||
var programs []string
|
||||
for row := 0; row < int(C.prefs_wine_count()); row++ {
|
||||
name := C.prefs_wine_value(C.int(row))
|
||||
programs = append(programs, C.GoString(name))
|
||||
C.g_free(C.gpointer(name))
|
||||
}
|
||||
return programs
|
||||
}
|
||||
|
||||
func (session *uiSession) refreshWineValidation() {
|
||||
message := ""
|
||||
if err := validateWinePrograms(getWinePrograms()); err != nil {
|
||||
message = err.Error()
|
||||
}
|
||||
setWineValidation(message)
|
||||
}
|
||||
|
||||
func getWineValues() (config.WineSettings, error) {
|
||||
wine := config.WineSettings{ControllerOnly: getCheck("wineControllerOnlyCheckButton"),
|
||||
ExcludedPrograms: getWinePrograms()}
|
||||
return normalizedWineSettings(wine)
|
||||
}
|
||||
Reference in new issue
Block a user