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:
1 parent
c018f5fd29
commit
0e50de4c76
1047 files changed
+58364
-63747
No files matched your search
@@ -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())
|
||||
}
|
||||
Reference in new issue
Block a user