// 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 . package config import ( "errors" "fmt" "io" "os" "path/filepath" "github.com/pelletier/go-toml/v2" ) const maxConfigSize = 1024 * 1024 func DefaultPath() (string, error) { if dataHome := os.Getenv("XDG_DATA_HOME"); dataHome != "" { return filepath.Join(dataHome, "cthulhu", "user-settings-go.toml"), nil } home, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("resolve home directory: %w", err) } return filepath.Join(home, ".local", "share", "cthulhu", "user-settings-go.toml"), nil } // Load reads legacy Python settings or the current versioned schema without // modifying the file. A present schema marker always selects strict decoding. func Load(path string) (Settings, error) { settings := DefaultSettings() if path == "" { var err error path, err = DefaultPath() if err != nil { return Settings{}, err } } settings.Path = path file, err := os.Open(path) if errors.Is(err, os.ErrNotExist) { return settings, nil } if err != nil { return Settings{}, fmt.Errorf("open configuration: %w", err) } defer file.Close() contents, err := io.ReadAll(io.LimitReader(file, maxConfigSize+1)) if err != nil { return Settings{}, fmt.Errorf("read configuration: %w", err) } if len(contents) > maxConfigSize { return Settings{}, fmt.Errorf("configuration exceeds %d bytes", maxConfigSize) } var envelope struct { SchemaVersion *int `toml:"schema-version"` } if err := toml.Unmarshal(contents, &envelope); err != nil { return Settings{}, fmt.Errorf("parse configuration: %w", err) } if envelope.SchemaVersion != nil { settings, err := decodeWritable(contents) if err != nil { return Settings{}, err } settings.Path = path settings.Loaded = true return settings, nil } var parsed document if err := toml.Unmarshal(contents, &parsed); err != nil { return Settings{}, fmt.Errorf("parse configuration: %w", err) } apply(&settings, parsed.General) profileName := selectedProfile(parsed.General, parsed.Profiles) if profileName != "" { profile, exists := parsed.Profiles[profileName] if !exists { return Settings{}, fmt.Errorf("configured profile %q does not exist", profileName) } apply(&settings, profile) } settings.ActiveProfile = profileName settings.Loaded = true if err := validate(settings); err != nil { return Settings{}, err } return settings, nil }