224 lines
8.1 KiB
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)
|
|
}
|
|
}
|