After commit c018f5fd29 Fenrir is being switched over to go. If you do not want to be part of the testing process, please switch to master.

This commit is contained in:
Storm Dragon committed 2026-10-27 20:30:55 -04:00
1 parent c018f5fd29
commit 0e50de4c76
1047 files changed
+58364 -63747

No files matched your search

+190
View File
@@ -0,0 +1,190 @@
// Fenrir console screen reader.
// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
//
// 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"
"io"
"os"
"reflect"
"strings"
"syscall"
"github.com/pelletier/go-toml/v2"
"github.com/pelletier/go-toml/v2/unstable"
)
// Document retains file identity and table order for an isolated edit. Version
// is metadata, not a UI tab. Omitted tables follow in schema order.
type Document struct {
Settings Settings
Order []string
path string
original []byte
existed bool
}
func read_document(path string) ([]byte, error) {
// Remote SAVEAS can name an arbitrary destination. A FIFO must not
// block opening before we can establish that it is a regular file.
file, err := os.OpenFile(path, os.O_RDONLY|syscall.O_NONBLOCK, 0)
if err != nil {
return nil, err
}
defer file.Close()
info, err := file.Stat()
if err != nil {
return nil, err
}
if !info.Mode().IsRegular() {
return nil, fmt.Errorf("settings destination is not a regular file")
}
data, err := io.ReadAll(io.LimitReader(file, (1<<20)+1))
if err == nil && len(data) > 1<<20 {
err = fmt.Errorf("settings file exceeds 1 MiB")
}
return data, err
}
func Open_document(path string, fallback Settings) (*Document, error) {
doc := &Document{Settings: fallback, path: path}
data, err := read_document(path)
if !errors.Is(err, os.ErrNotExist) {
if err != nil {
return nil, err
}
doc.Settings, err = Decode(data)
if err != nil {
return nil, err
}
doc.original, doc.existed = data, true
var parser unstable.Parser
parser.Reset(data)
seen := make(map[string]bool)
sectionIDs := make(map[string]bool)
var collect func([]Section)
collect = func(sections []Section) {
for _, section := range sections {
sectionIDs[section.ID] = true
collect(section.Sections)
}
}
collect(Sections())
record := func(parts []string) {
// Map old table order to the migrated hierarchy, preserving sibling order.
if len(parts) > 0 && (parts[0] == "speech" || parts[0] == "sound") {
parts = append([]string{"presentation"}, parts...)
}
for count := 1; count <= len(parts); count++ {
name := strings.Join(parts[:count], ".")
if sectionIDs[name] && !seen[name] {
doc.Order = append(doc.Order, name)
seen[name] = true
}
}
}
var visit func(*unstable.Node, []string)
visit = func(node *unstable.Node, prefix []string) {
parts := append([]string(nil), prefix...)
keys := node.Key()
for keys.Next() {
parts = append(parts, string(keys.Node().Data))
}
record(parts)
if node.Kind == unstable.KeyValue && node.Value().Kind == unstable.InlineTable {
children := node.Value().Children()
for children.Next() {
visit(children.Node(), parts)
}
}
}
var tablePath []string
for parser.NextExpression() {
node := parser.Expression()
switch node.Kind {
case unstable.Table:
tablePath = nil
keys := node.Key()
for keys.Next() {
tablePath = append(tablePath, string(keys.Node().Data))
}
record(tablePath)
case unstable.KeyValue:
visit(node, tablePath)
}
}
if err := parser.Error(); err != nil {
return nil, err
}
}
return doc, nil
}
// Save checks for external edits made since the menu opened before writing.
// Atomic replacement still uses the same private file permissions as Save.
func (d *Document) Save(settings Settings) error {
data, err := read_document(d.path)
if d.existed {
if err != nil {
return err
}
if !bytes.Equal(data, d.original) {
return fmt.Errorf("settings changed since the menu opened")
}
} else if err == nil {
return fmt.Errorf("settings created since the menu opened")
} else if !errors.Is(err, os.ErrNotExist) {
return err
}
if err := settings.Validate(); err != nil {
return err
}
// The same schema drives persistence and the form, including nested groups.
var output bytes.Buffer
fmt.Fprintf(&output, "version = %d\n", settings.Version)
var encode func([]Section) error
encode = func(sections []Section) error {
for _, section := range Ordered_sections(sections, d.Order) {
value := setting_value(reflect.ValueOf(settings), section.ID)
fields := make(map[string]any)
for index := range value.NumField() {
if value.Field(index).Kind() != reflect.Struct {
fields[value.Type().Field(index).Tag.Get("toml")] = value.Field(index).Interface()
}
}
if len(fields) > 0 {
encoded, err := toml.Marshal(fields)
if err != nil {
return err
}
fmt.Fprintf(&output, "\n[%s]\n", section.ID)
output.Write(encoded)
}
if err := encode(section.Sections); err != nil {
return err
}
}
return nil
}
if err := encode(Sections()); err != nil {
return err
}
return save_data(d.path, output.Bytes())
}