141 lines
5.1 KiB
Go
141 lines
5.1 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package config
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/pelletier/go-toml/v2"
|
|
)
|
|
|
|
// Application_setting admits only settings which can be applied and restored
|
|
// without changing devices, routing, storage, or other process-wide services.
|
|
func Application_setting(path string) bool {
|
|
switch path {
|
|
case "presentation.speech.enabled", "presentation.speech.rate", "presentation.speech.pitch", "presentation.speech.volume", "presentation.speech.module", "presentation.speech.language", "presentation.speech.voice",
|
|
"presentation.sound.enabled", "presentation.sound.volume", "presentation.progress.enabled",
|
|
"presentation.time_and_date.time_format", "presentation.time_and_date.date_format",
|
|
"keyboard.character_echo", "keyboard.word_echo", "keyboard.echo_on_capslock":
|
|
return true
|
|
case "reading.automatic", "reading.cursor_echo", "reading.mode", "reading.punctuation_level",
|
|
"reading.raise_pitch_for_capitals", "reading.beep_for_capitals", "reading.say_cap_for_capitals",
|
|
"reading.indentation", "reading.attributes", "reading.attribute_format", "reading.spellcheck",
|
|
"reading.spellcheck_read_word", "reading.spellcheck_say_misspelled", "reading.spellcheck_sound",
|
|
"reading.emoticons", "reading.repeated_symbols", "reading.repeated_symbols_min":
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Open_application_document overlays a sparse, current-schema profile on the
|
|
// application's inherited settings. Missing files inherit everything.
|
|
func Open_application_document(path string, base Settings) (*Document, error) {
|
|
doc := &Document{Settings: base, path: path, applicationBase: &base, applicationFields: make(map[string]bool)}
|
|
data, err := read_document(path)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return doc, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var fields map[string]any
|
|
if err := toml.Unmarshal(data, &fields); err != nil {
|
|
return nil, err
|
|
}
|
|
var collect func(map[string]any, string) error
|
|
collect = func(fields map[string]any, prefix string) error {
|
|
for name, value := range fields {
|
|
path := prefix + name
|
|
if table, ok := value.(map[string]any); ok {
|
|
if err := collect(table, path+"."); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
if !Application_setting(path) {
|
|
return fmt.Errorf("setting %s is not supported in application profiles", path)
|
|
}
|
|
doc.applicationFields[path] = true
|
|
}
|
|
return nil
|
|
}
|
|
if err := collect(fields, ""); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := toml.NewDecoder(bytes.NewReader(data)).DisallowUnknownFields().Decode(&doc.Settings); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := doc.Settings.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
doc.original, doc.existed = data, true
|
|
return doc, nil
|
|
}
|
|
|
|
// Apply_application preserves inherited session state for every omitted field.
|
|
func (d *Document) Apply_application(base Settings) Settings {
|
|
for path := range d.applicationFields {
|
|
setting_value(reflect.ValueOf(&base).Elem(), path).Set(setting_value(reflect.ValueOf(d.Settings), path))
|
|
}
|
|
return base
|
|
}
|
|
|
|
func (d *Document) save_application(settings Settings) error {
|
|
fields := make(map[string]any)
|
|
selectionChanged := false
|
|
for _, path := range []string{"presentation.speech.module", "presentation.speech.language", "presentation.speech.voice"} {
|
|
selectionChanged = selectionChanged || d.applicationFields[path] || !reflect.DeepEqual(setting_value(reflect.ValueOf(settings), path).Interface(), setting_value(reflect.ValueOf(*d.applicationBase), path).Interface())
|
|
}
|
|
var visit func(reflect.Value, reflect.Value, string)
|
|
visit = func(value, base reflect.Value, prefix string) {
|
|
for index := range value.NumField() {
|
|
path := prefix + value.Type().Field(index).Tag.Get("toml")
|
|
child, inherited := value.Field(index), base.Field(index)
|
|
if child.Kind() == reflect.Struct {
|
|
visit(child, inherited, path+".")
|
|
continue
|
|
}
|
|
speechSelection := selectionChanged && (path == "presentation.speech.module" || path == "presentation.speech.language" || path == "presentation.speech.voice")
|
|
if !Application_setting(path) || (!speechSelection && !d.applicationFields[path] && reflect.DeepEqual(child.Interface(), inherited.Interface())) {
|
|
continue
|
|
}
|
|
parts := strings.Split(path, ".")
|
|
table := fields
|
|
for _, part := range parts[:len(parts)-1] {
|
|
if table[part] == nil {
|
|
table[part] = make(map[string]any)
|
|
}
|
|
table = table[part].(map[string]any)
|
|
}
|
|
table[parts[len(parts)-1]] = child.Interface()
|
|
}
|
|
}
|
|
visit(reflect.ValueOf(settings), reflect.ValueOf(*d.applicationBase), "")
|
|
data, err := toml.Marshal(fields)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return save_data(d.path, data)
|
|
}
|
|
|
|
// Apply_application_settings copies the supported live settings only. Inherited
|
|
// next-launch choices must never replace the running backend or layout.
|
|
func Apply_application_settings(base Settings, source Settings) Settings {
|
|
for _, definition := range Definitions() {
|
|
if Application_setting(definition.Path) {
|
|
if err := definition.Set(&base, definition.Value(source)); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
base.Presentation.Progress = source.Presentation.Progress
|
|
return base
|
|
}
|