Files

202 lines
5.4 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
#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, soundThemes: []string{"default", "none"}}
session.setValues(initial.Values)
tentacles := &tentaclePage{}
tentacles.populate(initial.Tentacles)
var tentaclePending *tentacle.ManagementRequest
C.prefs_voice_changed()
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.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)
C.prefs_voice_changed()
}
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 >= 9 && action <= 20 && !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)
}
}
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 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
}
if err := encoder.Encode(Request{Values: values, Save: canSave, Close: action == 2}); err != nil {
return err
}
busy = true
previewDirty = false
C.prefs_busy(1, boolean(canSave))
continue
}
if !previewDirty || previewPending || busy {
continue
}
values, err := session.getValues()
if err != nil {
status(err.Error(), true)
previewDirty = false
continue
}
if err := encoder.Encode(Request{Values: values, Preview: true}); err != nil {
return err
}
previewPending = true
previewDirty = false
}
}
}