// 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 }