337 lines
10 KiB
Go
337 lines
10 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"math"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/config"
|
|
"git.stormux.org/storm/cthulhu/internal/input"
|
|
"github.com/godbus/dbus/v5"
|
|
)
|
|
|
|
// The names are the Python service's public names. Each mapped field is read
|
|
// from the live snapshot and applied by the preferences worker.
|
|
var remoteFields = map[string]string{
|
|
"SpeechManager.CapitalizationStyle": "CapitalizationStyle",
|
|
"SpeechManager.SpeakDescription": "SpeakDescription",
|
|
"SpeechManager.SpeakRowInGuiTable": "Reading.ReadFullRowInGUITable",
|
|
"TypingEchoManager.KeyEchoEnabled": "EnableKeyEcho",
|
|
"TypingEchoManager.CharacterEchoEnabled": "EnableEchoByCharacter",
|
|
"TypingEchoManager.WordEchoEnabled": "EnableEchoByWord",
|
|
"TypingEchoManager.SentenceEchoEnabled": "EnableEchoBySentence",
|
|
"TypingEchoManager.AlphabeticKeysEnabled": "EnableAlphabeticKeys",
|
|
"TypingEchoManager.NumericKeysEnabled": "EnableNumericKeys",
|
|
"TypingEchoManager.PunctuationKeysEnabled": "EnablePunctuationKeys",
|
|
"TypingEchoManager.SpaceEnabled": "EnableSpace",
|
|
"TypingEchoManager.ModifierKeysEnabled": "EnableModifierKeys",
|
|
"TypingEchoManager.FunctionKeysEnabled": "EnableFunctionKeys",
|
|
"TypingEchoManager.ActionKeysEnabled": "EnableActionKeys",
|
|
"TypingEchoManager.NavigationKeysEnabled": "EnableNavigationKeys",
|
|
"TypingEchoManager.DiacriticalKeysEnabled": "EnableDiacriticalKeys",
|
|
}
|
|
|
|
func remoteField(settings *config.Settings, path string) reflect.Value {
|
|
value := reflect.ValueOf(settings).Elem()
|
|
for _, part := range strings.Split(path, ".") {
|
|
if value.Kind() != reflect.Struct {
|
|
return reflect.Value{}
|
|
}
|
|
value = value.FieldByName(part)
|
|
if !value.IsValid() {
|
|
return value
|
|
}
|
|
}
|
|
return value
|
|
}
|
|
|
|
func remoteValue(settings config.Settings, module, property string) (dbus.Variant, bool) {
|
|
if module == "SpeechManager" {
|
|
switch property {
|
|
case "CurrentServer":
|
|
if !settings.EnableSpeech {
|
|
return dbus.MakeVariant(""), true
|
|
}
|
|
return dbus.MakeVariant("speechdispatcher"), true
|
|
case "AvailableServers":
|
|
return dbus.MakeVariant([]string{"speechdispatcher"}), true
|
|
case "CurrentSynthesizer":
|
|
return dbus.MakeVariant(settings.SpeechOutputModule), true
|
|
case "CurrentVoice":
|
|
if settings.DefaultVoice.Family != nil {
|
|
return dbus.MakeVariant(settings.DefaultVoice.Family.Name), true
|
|
}
|
|
return dbus.MakeVariant(""), true
|
|
case "Rate":
|
|
if settings.DefaultVoice.Rate != nil {
|
|
return dbus.MakeVariant(int32(*settings.DefaultVoice.Rate)), true
|
|
}
|
|
return dbus.MakeVariant(int32(50)), true
|
|
case "Pitch":
|
|
if settings.DefaultVoice.AveragePitch != nil {
|
|
return dbus.MakeVariant(*settings.DefaultVoice.AveragePitch), true
|
|
}
|
|
return dbus.MakeVariant(float64(5)), true
|
|
case "Volume":
|
|
if settings.DefaultVoice.Gain != nil {
|
|
return dbus.MakeVariant(*settings.DefaultVoice.Gain), true
|
|
}
|
|
return dbus.MakeVariant(float64(10)), true
|
|
case "SpeechIsEnabled":
|
|
return dbus.MakeVariant(settings.EnableSpeech), true
|
|
case "PunctuationLevel":
|
|
return dbus.MakeVariant([]string{"all", "most", "some", "none"}[settings.VerbalizePunctuationStyle]), true
|
|
}
|
|
}
|
|
path, ok := remoteFields[module+"."+property]
|
|
if !ok {
|
|
return dbus.Variant{}, false
|
|
}
|
|
field := remoteField(&settings, path)
|
|
if !field.IsValid() {
|
|
return dbus.Variant{}, false
|
|
}
|
|
return dbus.MakeVariant(field.Interface()), true
|
|
}
|
|
|
|
type remoteSettingsChange struct {
|
|
module, property string
|
|
command string
|
|
value dbus.Variant
|
|
result chan bool
|
|
expires time.Time
|
|
}
|
|
|
|
func (worker *preferencesWorker) RunRemoteCommand(command string) bool {
|
|
change := remoteSettingsChange{module: "SpeechManager", command: command, result: make(chan bool, 1), expires: time.Now().Add(2 * time.Second)}
|
|
select {
|
|
case worker.remoteChanges <- change:
|
|
default:
|
|
return false
|
|
}
|
|
select {
|
|
case accepted := <-change.result:
|
|
return accepted
|
|
case <-worker.done:
|
|
return false
|
|
case <-time.After(2 * time.Second):
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (worker *preferencesWorker) SetRemote(module, property string, value dbus.Variant) bool {
|
|
change := remoteSettingsChange{module: module, property: property, value: value, result: make(chan bool, 1), expires: time.Now().Add(2 * time.Second)}
|
|
select {
|
|
case worker.remoteChanges <- change:
|
|
default:
|
|
return false
|
|
}
|
|
select {
|
|
case accepted := <-change.result:
|
|
return accepted
|
|
case <-worker.done:
|
|
return false
|
|
case <-time.After(2 * time.Second):
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (worker *preferencesWorker) applyRemoteChange(ctx context.Context, change remoteSettingsChange) bool {
|
|
if time.Now().After(change.expires) {
|
|
return false
|
|
}
|
|
next := worker.settings
|
|
if change.command != "" {
|
|
if !adjustRemoteSpeech(&next, change.command) {
|
|
return false
|
|
}
|
|
} else {
|
|
if !setRemoteValue(&next, change.module, change.property, change.value) {
|
|
return false
|
|
}
|
|
}
|
|
applyCtx, cancel := context.WithDeadline(ctx, change.expires)
|
|
defer cancel()
|
|
return worker.applyNext(applyCtx, next, false, false).Accepted
|
|
}
|
|
|
|
func adjustRemoteSpeech(settings *config.Settings, command string) bool {
|
|
var field **float64
|
|
var defaultValue, step, upper float64
|
|
delta := float64(1)
|
|
switch command {
|
|
case "DecreaseRate", "IncreaseRate":
|
|
field = &settings.DefaultVoice.Rate
|
|
defaultValue, step, upper = 50, 1, 99
|
|
case "DecreasePitch", "IncreasePitch":
|
|
field = &settings.DefaultVoice.AveragePitch
|
|
defaultValue, step, upper = 5, 0.1, 9
|
|
case "DecreaseVolume", "IncreaseVolume":
|
|
field = &settings.DefaultVoice.Gain
|
|
defaultValue, step, upper = 9, 0.1, 9
|
|
default:
|
|
return false
|
|
}
|
|
if strings.HasPrefix(command, "Decrease") {
|
|
delta = -1
|
|
}
|
|
value := defaultValue
|
|
if *field != nil {
|
|
value = **field
|
|
}
|
|
value = math.Round((value+delta*step)*10) / 10
|
|
value = math.Max(0, math.Min(upper, value))
|
|
*field = &value
|
|
return true
|
|
}
|
|
|
|
func setRemoteValue(settings *config.Settings, module, property string, value dbus.Variant) bool {
|
|
key := module + "." + property
|
|
if module == "SpeechManager" {
|
|
switch property {
|
|
case "Rate", "Pitch", "Volume":
|
|
var number float64
|
|
if property == "Rate" {
|
|
v, ok := value.Value().(int32)
|
|
if !ok || v < 0 || v > 99 {
|
|
return false
|
|
}
|
|
number = float64(v)
|
|
} else {
|
|
v, ok := value.Value().(float64)
|
|
if !ok {
|
|
return false
|
|
}
|
|
number = v
|
|
}
|
|
switch property {
|
|
case "Rate":
|
|
settings.DefaultVoice.Rate = &number
|
|
case "Pitch":
|
|
settings.DefaultVoice.AveragePitch = &number
|
|
case "Volume":
|
|
settings.DefaultVoice.Gain = &number
|
|
}
|
|
return true
|
|
case "CurrentSynthesizer":
|
|
v, ok := value.Value().(string)
|
|
if !ok || len(v) > 256 {
|
|
return false
|
|
}
|
|
settings.SpeechOutputModule = v
|
|
settings.DefaultVoice.Family = nil
|
|
return true
|
|
case "CurrentVoice":
|
|
v, ok := value.Value().(string)
|
|
if !ok || len(v) > 256 {
|
|
return false
|
|
}
|
|
if settings.DefaultVoice.Family == nil {
|
|
settings.DefaultVoice.Family = &config.VoiceFamily{}
|
|
} else {
|
|
family := *settings.DefaultVoice.Family
|
|
settings.DefaultVoice.Family = &family
|
|
}
|
|
settings.DefaultVoice.Family.Name = v
|
|
return true
|
|
case "PunctuationLevel":
|
|
v, ok := value.Value().(string)
|
|
if !ok {
|
|
return false
|
|
}
|
|
for i, name := range []string{"all", "most", "some", "none"} {
|
|
if strings.EqualFold(v, name) {
|
|
settings.VerbalizePunctuationStyle = i
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
path, ok := remoteFields[key]
|
|
if !ok {
|
|
return false
|
|
}
|
|
field := remoteField(settings, path)
|
|
if !field.IsValid() || !field.CanSet() {
|
|
return false
|
|
}
|
|
newValue := reflect.ValueOf(value.Value())
|
|
if !newValue.IsValid() || newValue.Type() != field.Type() {
|
|
return false
|
|
}
|
|
field.Set(newValue)
|
|
return true
|
|
}
|
|
|
|
func (presenter remoteSpeech) RemoteGet(module, property string) (dbus.Variant, bool) {
|
|
if presenter.live == nil {
|
|
return dbus.Variant{}, false
|
|
}
|
|
if module == "SpeechManager" && property == "SpeechIsEnabled" {
|
|
return dbus.MakeVariant(presenter.speech != nil && presenter.live.Load().EnableSpeech), true
|
|
}
|
|
if module == "SpeechManager" && presenter.speech == nil && (property == "CurrentServer" || property == "CurrentSynthesizer" || property == "CurrentVoice") {
|
|
return dbus.MakeVariant(""), true
|
|
}
|
|
return remoteValue(presenter.live.Load(), module, property)
|
|
}
|
|
|
|
func (presenter remoteSpeech) RemoteModulesAvailable() bool {
|
|
return presenter.live != nil && presenter.preferences != nil
|
|
}
|
|
|
|
func (presenter remoteSpeech) RemoteSet(module, property string, value dbus.Variant) bool {
|
|
return presenter.preferences != nil && presenter.preferences.SetRemote(module, property, value)
|
|
}
|
|
|
|
var remoteSpeechCommands = map[string]input.Command{
|
|
"SelectPreviousSpeechSetting": input.SelectPreviousSpeechSetting,
|
|
"SelectNextSpeechSetting": input.SelectNextSpeechSetting,
|
|
"DecreaseCurrentSpeechSetting": input.DecreaseSpeechSetting,
|
|
"IncreaseCurrentSpeechSetting": input.IncreaseSpeechSetting,
|
|
"SaveSpeechSettings": input.SaveSpeechSettings,
|
|
}
|
|
|
|
func (presenter remoteSpeech) RemoteCommand(module, command string, notify bool) bool {
|
|
if module == "SpeechManager" {
|
|
if command == "InterruptSpeech" {
|
|
return presenter.CancelSpeech()
|
|
}
|
|
if strings.HasPrefix(command, "Decrease") || strings.HasPrefix(command, "Increase") {
|
|
if presenter.preferences == nil {
|
|
return false
|
|
}
|
|
accepted := presenter.preferences.RunRemoteCommand(command)
|
|
if accepted && notify {
|
|
if message, ok := map[string]string{"DecreaseRate": "Speech slower", "IncreaseRate": "Speech faster", "DecreasePitch": "Speech lower", "IncreasePitch": "Speech higher", "DecreaseVolume": "Speech softer", "IncreaseVolume": "Speech louder"}[command]; ok {
|
|
presenter.PresentMessage(message)
|
|
}
|
|
}
|
|
return accepted
|
|
}
|
|
if mapped, ok := remoteSpeechCommands[command]; ok && presenter.preferences != nil {
|
|
return presenter.preferences.TrySpeechCommand(mapped)
|
|
}
|
|
}
|
|
if module == "TypingEchoManager" && command == "CycleKeyEcho" && presenter.live != nil {
|
|
current := presenter.live.Load().EnableKeyEcho
|
|
accepted := presenter.RemoteSet(module, "KeyEchoEnabled", dbus.MakeVariant(!current))
|
|
if accepted && notify {
|
|
if current {
|
|
presenter.PresentMessage("Key echo disabled")
|
|
} else {
|
|
presenter.PresentMessage("Key echo enabled")
|
|
}
|
|
}
|
|
return accepted
|
|
}
|
|
return false
|
|
}
|