202 lines
5.4 KiB
Go
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
|
|
}
|
|
}
|
|
}
|