Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,201 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user