Files
hjozwiak 3026bc55c2 Bring in goodies from testing.
# Conflicts:
#	docs/testing/wine-proton.md
2026-11-02 11:30:32 -05:00

245 lines
6.9 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package preferences
/*
#cgo pkg-config: gtk+-3.0 xkbcommon
#include <stdlib.h>
#include "native.h"
*/
import "C"
import (
"context"
_ "embed"
"encoding/json"
"errors"
"io"
"runtime"
"time"
"unsafe"
"git.stormux.org/storm/cthulhu/internal/config"
"git.stormux.org/storm/cthulhu/internal/tentacle"
)
//go:embed preferences.ui
var layout string
// Run owns all GTK calls on one OS thread. Its caller supplies private pipes;
// closing the parent pipe closes the window. No widget invokes speech directly.
func Run(ctx context.Context, input io.Reader, output io.Writer) error {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
updates := make(chan Response, 1)
errorsIn := make(chan error, 1)
go read_responses(ctx, input, updates, errorsIn)
var initial Response
select {
case initial = <-updates:
case err := <-errorsIn:
return err
case <-ctx.Done():
return ctx.Err()
}
if initial.ChooseApplication {
response, chosen, err := choose_application(ctx, updates, errorsIn, output)
if err != nil || !chosen {
return err
}
initial = response
}
xml := C.CString(layout)
defer C.free(unsafe.Pointer(xml))
if C.prefs_init(xml) == 0 {
return errors.New("cannot open GTK preferences on this display")
}
defer C.prefs_close()
if initial.Application != "" {
name := C.CString(initial.Application)
C.prefs_set_application(name)
C.free(unsafe.Pointer(name))
}
session := &uiSession{catalog: initial.Catalog, speechDefaults: initial.SpeechDefaults, soundThemes: []string{"default", "none"},
wineEditable: initial.Application == ""}
session.setValues(initial.Values)
if err := session.set_extension_pages(initial.ExtensionPages); err != nil {
return err
}
tentacles := &tentaclePage{}
tentacles.populate(initial.Tentacles)
var tentaclePending *tentacle.ManagementRequest
C.prefs_voice_changed()
C.prefs_echo_preview(nil)
status(initial.Message, false)
canSave, busy := initial.CanSave, false
previewPending, previewDirty := false, false
C.prefs_busy(0, boolean(canSave))
encoder := json.NewEncoder(output)
// AT-SPI calls into this GTK process are served by the same event pump.
// Focus snapshots make several synchronous calls, so a 10 ms poll adds
// noticeable delay to every Tab stop in the dialog.
ticker := time.NewTicker(time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case err := <-errorsIn:
return err
case response := <-updates:
if response.ExtensionsOnly {
if err := session.set_extension_pages(response.ExtensionPages); err != nil {
return err
}
continue
}
if response.Present {
C.prefs_present()
continue
}
if response.Preview {
previewPending = false
if !response.Accepted {
status(response.Message, true)
}
continue
}
if response.Close {
return nil
}
if tentaclePending != nil {
settingsOpened := tentaclePending.Action == "settings" && response.Accepted
// Management commits independently of the ordinary settings draft.
// Its response does not contain Values or CanSave.
if response.Tentacles != nil {
tentacles.populate(response.Tentacles)
if response.Accepted {
tentacles.accepted(tentaclePending)
}
}
tentaclePending, busy = nil, false
C.prefs_busy(0, boolean(canSave))
if !settingsOpened {
status(response.Message, true)
}
continue
}
busy, canSave = false, response.CanSave
if response.Accepted {
session.setValues(response.Values)
if err := session.set_extension_pages(response.ExtensionPages); err != nil {
return err
}
C.prefs_voice_changed()
C.prefs_echo_preview(nil)
}
C.prefs_busy(0, boolean(canSave))
status(response.Message, !response.Accepted)
case <-ticker.C:
C.prefs_iterate()
session.refreshEchoVoices()
session.refreshSpeechVoices()
session.refreshKeyBindingLayout()
action := C.prefs_action()
if action >= C.PREFS_TENTACLE_REPOSITORY_ADD && action <= C.PREFS_TENTACLE_UPDATE && !busy {
request, err := tentacles.action(int(action))
if err != nil {
status(err.Error(), true)
continue
}
if request != nil {
if err := encoder.Encode(Request{Tentacle: request}); err != nil {
return err
}
tentaclePending, busy = request, true
// Keep the focused Settings button enabled while the internal
// form takes keyboard ownership without moving native focus.
if request.Action != "settings" {
C.prefs_busy(1, boolean(canSave))
}
if request.Action == "install" || request.Action == "install-enable" {
status("Downloading and checking the tentacle package. Close preferences to cancel before it is saved.", true)
}
if request.Action == "upgrade" {
status("Downloading and checking the tentacle update. Close preferences to cancel.", true)
}
}
continue
}
if action == 8 {
setTextAttributes(config.DefaultTextAttributes())
continue
}
if action >= 4 && action <= 7 {
session.editKeyBinding(int(action))
continue
}
if action == 3 {
// The parent restores any active preview when this helper exits.
return nil
}
if session.wineEditable && C.prefs_wine_changed() != 0 {
session.refreshWineValidation()
}
if C.prefs_voice_changed() != 0 {
previewDirty = true
}
if (action == 1 || action == 2) && !busy {
values, err := session.getValues()
if err != nil {
status(err.Error(), true)
continue
}
drafts, err := session.extension_drafts()
if err != nil {
status(err.Error(), true)
continue
}
request := Request{Values: values, ExtensionPages: drafts, Save: canSave, Close: action == 2}
data, err := json.Marshal(request)
if err != nil {
return err
}
if len(data)+1 > initialMessageLimit {
status("Preferences changes exceed the message limit. Shorten the edited values.", true)
continue
}
if err := encoder.Encode(json.RawMessage(data)); err != nil {
return err
}
busy = true
previewDirty = false
C.prefs_busy(1, boolean(canSave))
continue
}
if previewPending || busy {
continue
}
echoPreview := ""
var echoFocus C.int
if control := int(C.prefs_echo_preview(&echoFocus)); control != 0 {
echoPreview = []string{"rate", "pitch", "volume", "synthesizer", "person"}[control-1]
}
if !previewDirty && echoPreview == "" {
continue
}
values, err := session.getValues()
if err != nil {
status(err.Error(), true)
previewDirty = false
continue
}
if err := encoder.Encode(Request{Values: values, Preview: true, EchoPreview: echoPreview, EchoPreviewFocus: echoFocus != 0}); err != nil {
return err
}
previewPending = true
previewDirty = false
}
}
}