Files

146 lines
5.0 KiB
Go

// Cthulhu screen reader.
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
package config
import (
"bytes"
"errors"
"fmt"
"sort"
"strings"
"github.com/pelletier/go-toml/v2"
)
func defaultDocument() writableDocument {
return writableDocument{
SchemaVersion: CurrentSchemaVersion,
ActiveProfile: "default",
Profiles: map[string]writableProfile{
"default": profileFromSettings(DefaultSettings()),
},
Applications: make(map[string]string),
}
}
func decodeWritable(contents []byte) (Settings, error) {
_, settings, err := decodeDocument(contents)
return settings, err
}
func decodeDocument(contents []byte) (writableDocument, Settings, error) {
var envelope struct {
SchemaVersion int `toml:"schema-version"`
}
if err := toml.Unmarshal(contents, &envelope); err != nil {
return writableDocument{}, Settings{}, fmt.Errorf("parse settings: %w", err)
}
if envelope.SchemaVersion != CurrentSchemaVersion {
return writableDocument{}, Settings{}, fmt.Errorf("unsupported settings schema version %d; expected %d", envelope.SchemaVersion, CurrentSchemaVersion)
}
var document writableDocument
decoder := toml.NewDecoder(bytes.NewReader(contents))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&document); err != nil {
var strictError *toml.StrictMissingError
if errors.As(err, &strictError) {
return writableDocument{}, Settings{}, fmt.Errorf("parse settings schema %d: %s", CurrentSchemaVersion, strictError.String())
}
return writableDocument{}, Settings{}, fmt.Errorf("parse settings schema %d: %w", CurrentSchemaVersion, err)
}
settings, err := effectiveSettings(document)
if err != nil {
return writableDocument{}, Settings{}, err
}
return document, settings, nil
}
func effectiveSettings(document writableDocument) (Settings, error) {
if strings.TrimSpace(document.ActiveProfile) == "" {
return Settings{}, errors.New("validate configuration: active-profile is empty")
}
profile, exists := document.Profiles[document.ActiveProfile]
if !exists {
return Settings{}, fmt.Errorf("validate configuration: active profile %q does not exist", document.ActiveProfile)
}
for _, name := range sortedKeys(document.Profiles) {
candidate := document.Profiles[name]
if strings.TrimSpace(name) != name || name == "" {
return Settings{}, fmt.Errorf("validate configuration: invalid profile name %q", name)
}
if err := validateCommandValue("profile name", name); err != nil {
return Settings{}, err
}
candidateSettings := DefaultSettings()
applyProfile(&candidateSettings, candidate)
if err := validate(candidateSettings); err != nil {
return Settings{}, fmt.Errorf("profile %q: %w", name, err)
}
}
for _, application := range sortedKeys(document.Applications) {
profileName := document.Applications[application]
if strings.TrimSpace(application) != application || application == "" {
return Settings{}, fmt.Errorf("validate configuration: invalid application identifier %q", application)
}
if err := validateCommandValue("application identifier", application); err != nil {
return Settings{}, err
}
if _, exists := document.Profiles[profileName]; !exists {
return Settings{}, fmt.Errorf("validate configuration: application %q selects missing profile %q", application, profileName)
}
}
settings := DefaultSettings()
applyProfile(&settings, profile)
settings.ActiveProfile = document.ActiveProfile
return settings, nil
}
func sortedKeys[Value any](values map[string]Value) []string {
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func encodeWritable(settings Settings) ([]byte, error) {
document := defaultDocument()
profileName := settings.ActiveProfile
if profileName == "" {
profileName = document.ActiveProfile
}
document.ActiveProfile = profileName
document.Profiles = map[string]writableProfile{profileName: profileFromSettings(settings)}
return encodeDocument(document)
}
func encodeDocument(document writableDocument) ([]byte, error) {
if document.SchemaVersion != CurrentSchemaVersion {
return nil, fmt.Errorf("encode settings schema version %d; expected %d", document.SchemaVersion, CurrentSchemaVersion)
}
if _, err := effectiveSettings(document); err != nil {
return nil, err
}
contents, err := toml.Marshal(document)
if err != nil {
return nil, fmt.Errorf("encode settings: %w", err)
}
return contents, nil
}