Files
cthulhu/internal/app/preferences_apply.go

224 lines
8.1 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"context"
"errors"
"reflect"
"time"
"git.stormux.org/storm/cthulhu/internal/config"
"git.stormux.org/storm/cthulhu/internal/input"
"git.stormux.org/storm/cthulhu/internal/preferences"
)
func (worker *preferencesWorker) response(message string, accepted bool) preferences.Response {
settings := worker.settings
if worker.editingActive {
settings = worker.editing
}
store, err := config.NewStore(settings.Path)
if err == nil {
_, err = store.Load()
}
return preferences.Response{Values: preferences.FromSettings(settings), CanSave: err == nil,
Message: message, Accepted: accepted}
}
func (worker *preferencesWorker) apply(ctx context.Context, request preferences.Request) preferences.Response {
base := worker.settings
if worker.editingActive {
base = worker.editing
}
next, err := request.Values.Merge(base)
if err != nil {
return worker.response("Settings were not applied: "+err.Error(), false)
}
return worker.applyNext(ctx, next, request.Save, request.Close)
}
func (worker *preferencesWorker) applyNext(ctx context.Context, next config.Settings, save, close bool) preferences.Response {
scopeApp := worker.activeApp
if worker.editingActive {
scopeApp = worker.editApp
}
if err := config.Validate(next); err != nil {
return worker.response("Settings were not applied: "+err.Error(), false)
}
if save && !worker.response("", false).CanSave {
return worker.response("This settings file cannot be saved by the Go preview. Use Apply for this session.", false)
}
if err := worker.applyRuntime(ctx, next); err != nil {
return worker.response(err.Error(), false)
}
if worker.editingActive {
worker.editing = next
}
if scopeApp != "" {
worker.appSettings[scopeApp] = next
} else {
oldGlobal := worker.globalSettings
for application, settings := range worker.appSettings {
worker.appSettings[application] = config.RebaseApplicationSettings(oldGlobal, next, settings)
}
worker.globalSettings = next
}
message := "Settings applied for this session."
if save {
store, err := config.NewStore(next.Path)
if err == nil {
if scopeApp != "" {
err = store.SaveForApplication(scopeApp, next)
} else {
err = store.Save(next)
}
}
if err != nil {
message = "Applied for this session, but could not save: " + err.Error()
} else {
message = "Settings applied and saved."
}
}
response := worker.response(message, true)
response.Close = close && save && message == "Settings applied and saved."
if worker.editingActive && worker.editApp != worker.activeApp {
previousApp := worker.activeApp
worker.activeApp = ""
if previousApp != "" {
worker.switchApplication(ctx, previousApp)
}
}
return response
}
func (worker *preferencesWorker) applyRuntime(ctx context.Context, next config.Settings) error {
keyboardChanged := next.KeyboardLayout != worker.settings.KeyboardLayout || !reflect.DeepEqual(next.CthulhuModifierKeys, worker.settings.CthulhuModifierKeys)
bindingsChanged := keyboardChanged || !reflect.DeepEqual(next.KeyBindings, worker.settings.KeyBindings)
bindingsCommitted := false
if bindingsChanged {
if worker.bindings == nil {
return errors.New("Key bindings cannot be changed by this input backend.")
}
if err := worker.apply_keyboard(ctx, next, keyboardChanged); err != nil {
return errors.New("Key bindings were not applied: " + err.Error())
}
defer func() {
if !bindingsCommitted {
rollbackCtx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if err := worker.apply_keyboard(rollbackCtx, worker.settings, keyboardChanged); err != nil {
worker.logger.Error("restore key bindings after failed preferences apply", "error", err)
}
}
}()
}
speechChanged := next.SpeechServerFactory != worker.settings.SpeechServerFactory ||
!reflect.DeepEqual(next.UppercaseVoice, worker.settings.UppercaseVoice) ||
!reflect.DeepEqual(next.HyperlinkVoice, worker.settings.HyperlinkVoice) ||
!reflect.DeepEqual(next.SystemVoice, worker.settings.SystemVoice) ||
next.HardwareSpeechDevice != worker.settings.HardwareSpeechDevice ||
next.HardwareSpeechBaudRate != worker.settings.HardwareSpeechBaudRate || !reflect.DeepEqual(speechSettingsFromConfig(next), speechSettingsFromConfig(worker.settings)) ||
next.UseCustomEchoVoice != worker.settings.UseCustomEchoVoice ||
(next.UseCustomEchoVoice && !reflect.DeepEqual(echoSpeechSettings(next), echoSpeechSettings(worker.settings)))
if next.EnableSpeech && (speechChanged || !worker.settings.EnableSpeech) {
if message := worker.apply_speech_settings(ctx, next); message != "" {
return errors.New(message)
}
}
if next.EnableBraille != worker.settings.EnableBraille {
if worker.braille == nil {
return errors.New("Braille is unavailable in this build.")
}
if err := worker.braille.Configure(ctx, next.EnableBraille); err != nil {
return errors.New("Braille settings were not applied: " + err.Error())
}
}
if next.EnableContractedBraille != worker.settings.EnableContractedBraille ||
next.BrailleContractionTable != worker.settings.BrailleContractionTable {
if updater, ok := worker.braille.(interface{ SetContraction(bool, string) }); ok {
updater.SetContraction(next.EnableContractedBraille, next.BrailleContractionTable)
}
}
bindingsCommitted = true
worker.commit_settings(next)
return nil
}
func (worker *preferencesWorker) apply_keyboard(ctx context.Context, settings config.Settings, keyboardChanged bool) error {
if updater, ok := worker.bindings.(interface {
SetKeyboardConfiguration(context.Context, input.KeyboardLayout, []string, []input.BindingOverride) error
}); ok {
return updater.SetKeyboardConfiguration(ctx, input.KeyboardLayout(settings.KeyboardLayout), settings.CthulhuModifierKeys, settings.KeyBindings)
}
if keyboardChanged {
return errors.New("this input backend cannot change Cthulhu keys or keyboard layout")
}
return worker.bindings.SetKeyBindings(ctx, settings.KeyBindings)
}
func (worker *preferencesWorker) apply_speech_settings(ctx context.Context, next config.Settings) string {
var err error
if worker.updater == nil {
return "Speech is disabled or this backend cannot change settings."
}
if updater, ok := worker.updater.(interface {
UpdateConfiguration(context.Context, config.Settings) error
}); ok {
err = updater.UpdateConfiguration(ctx, next)
} else if updater, ok := worker.updater.(speechSettingsUpdater); ok {
if next.UseCustomEchoVoice {
return "This speech backend cannot use a custom echo voice."
}
err = updater.UpdateSettings(ctx, speechSettingsFromConfig(next))
} else {
return "This backend cannot change speech settings."
}
if err != nil {
return "Settings were not applied: " + err.Error()
}
return ""
}
// Commit follows the existing backend updates and keyboard rollback boundary.
// It publishes settings to runtime consumers without changing their ownership.
func (worker *preferencesWorker) commit_settings(next config.Settings) {
soundChanged := soundConfiguration(next) != soundConfiguration(worker.settings)
worker.settings = next
if updater, ok := worker.braille.(interface{ SetWordWrap(bool) }); ok {
updater.SetWordWrap(next.Braille.WordWrap)
}
if worker.speechWorker != nil && !next.EnableSpeech {
worker.speechWorker.SetEnabled(false)
}
if worker.history != nil {
worker.history.setLimit(next.SpeechHistoryLimit)
if worker.refreshHistory != nil {
worker.refreshHistory()
}
}
if worker.documentReview != nil {
worker.documentReview.SetDocumentBrailleEnabled(next.EnableBraille)
}
// Configure cancels playback. An application switch or unrelated setting
// must leave the startup cue running when its sound policy is unchanged.
if worker.sound != nil && soundChanged {
worker.sound.Configure(soundConfiguration(next))
}
if worker.geometry != nil {
worker.geometry.SetCaptureSoundGeometry(captureSoundGeometry(next))
}
if worker.textAttributes != nil {
worker.textAttributes.SetCaptureTextAttributes(captureTextAttributes(next))
}
if worker.live != nil {
worker.live.Store(next)
}
if worker.speechWorker != nil && next.EnableSpeech {
worker.speechWorker.SetEnabled(true)
}
}