Adjusted starting speech rate for new users. Fixed error that could happen when figuring out where to write changes.

This commit is contained in:
Storm Dragon committed 2026-10-28 15:04:41 -04:00
1 parent f989628a5a
commit 3b7a44c224
19 files changed
+163 -49

No files matched your search

+4 -2
View File
@@ -181,8 +181,10 @@ Default settings locations:
- Root: `/etc/fenrirscreenreader/settings/settings.toml`.
Fenrir loads the first existing file, or built-in defaults if neither exists.
Use `--config PATH` to select a specific file. To create a personal settings
file without opening devices:
Python `settings.conf` files are ignored. Regular users save to their personal
TOML file even when startup loaded system defaults; root saves to the system
file. `--config PATH` explicitly selects both the load and save path.
To create a personal settings file without opening devices:
```bash
./fenrir --write-config ~/.local/share/stormux/fenrirscreenreader/settings/settings.toml
+2 -10
View File
@@ -65,7 +65,7 @@ func run_with_ready(args []string, stdout, stderr io.Writer, ready func() error)
checkConfig := flags.Bool("check-config", false, "validate settings and exit without opening devices")
writeConfig := flags.String("write-config", "", "atomically write effective settings to PATH and exit")
driver := flags.String("speech-driver", "speech-dispatcher", "speech driver ID (overrides presentation.speech.driver)")
rate := flags.Int("speech-rate", 80, "Speech Dispatcher rate (-100 to 100)")
rate := flags.Int("speech-rate", config.Defaults().Presentation.Speech.Rate, "Speech Dispatcher rate (-100 to 100)")
module := flags.String("speech-module", "", "Speech Dispatcher output module (empty uses server default)")
debugFile := flags.String("debug-file", "", "append timing and reader-decision diagnostics without text or key values")
var devices []string
@@ -160,14 +160,6 @@ func run_with_ready(args []string, stdout, stderr io.Writer, ready func() error)
slog.Info("reader starting", "pid", os.Getpid(), "terminal_mode", *terminalMode, "at", time.Now())
ctx, stop := termination_context()
defer stop()
if path == "" {
home, err := os.UserHomeDir()
if err != nil {
fmt.Fprintln(stderr, "fenrir:", err)
return 1
}
path = config.Paths(home, os.Geteuid())[0]
}
if *terminalMode {
err = app.Start_terminal(ctx, settings, path, bindings)
} else {
@@ -227,7 +219,7 @@ func load_settings(flags *flag.FlagSet, requested string, rate int, module strin
if err != nil {
return config.Settings{}, "", err
}
settings, path, err := config.Load(requested, config.Paths(home, os.Geteuid()))
settings, path, err := config.Load_startup(requested, config.Paths(home, os.Geteuid()))
if err != nil {
return config.Settings{}, path, err
}
+12 -1
View File
@@ -19,6 +19,7 @@ package main
import (
"bytes"
"flag"
"os"
"path/filepath"
"strings"
@@ -28,6 +29,16 @@ import (
"git.stormux.org/storm/fenrir/internal/version"
)
func TestStartupUsesAccountSaveDestination(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
flags := flag.NewFlagSet("fenrir", flag.ContinueOnError)
_, path, err := load_settings(flags, "", config.Defaults().Presentation.Speech.Rate, "", nil, "", "speech-dispatcher", "")
if expected := config.Paths(home, os.Geteuid())[0]; err != nil || path != expected {
t.Fatalf("save destination=%q, want %q: %v", path, expected, err)
}
}
func TestCLIWithoutOpeningDevices(t *testing.T) {
for _, testCase := range []struct {
args []string
@@ -35,7 +46,7 @@ func TestCLIWithoutOpeningDevices(t *testing.T) {
text string
}{
{[]string{"--help"}, 0, "keypad 7/8/9"},
{[]string{"--help"}, 0, "(default 80)"},
{[]string{"--help"}, 0, "(default -20)"},
{[]string{"-f", "--help"}, 0, "stay in the foreground"},
{[]string{"-f", "--version"}, 0, "fenrir " + version.Label()},
{[]string{"--version"}, 0, "fenrir " + version.Label()},
+2 -1
View File
@@ -23,7 +23,8 @@ hardware_device = ""
hardware_baud_rate = 0
enabled = true
# Shared -100 to 100 scale; hardware drivers map it to native ranges.
rate = 80
# -20 corresponds to 0.4 on Python Fenrir's 0.0 to 1.0 scale.
rate = -20
# Empty choices use the server default. Voice names are module-specific.
module = ""
language = ""
+5 -3
View File
@@ -448,9 +448,11 @@ Root: \f[CR]/etc/fenrirscreenreader/settings/settings.toml\f[R].
\f[CR]\-\-config PATH\f[R]: use that file explicitly.
.PP
If default files are absent, Fenrir uses built\-in defaults.
An explicitly selected file must exist.
Saving targets the selected file; if startup used built\-in defaults,
saving creates the first default path for that account.
Python \f[CR]settings.conf\f[R] files are ignored.
Regular users save to their personal TOML file, including when startup
loaded system defaults; root saves to the system file.
\f[CR]\-\-config PATH\f[R] explicitly selects both the load and save path,
and that file must exist at startup.
.PP
Create a personal settings file and validate it without opening devices:
.IP
+5 -3
View File
@@ -346,9 +346,11 @@ Fenrir loads one settings file rather than merging files.
- Root: `/etc/fenrirscreenreader/settings/settings.toml`.
- `--config PATH`: use that file explicitly.
If default files are absent, Fenrir uses built-in defaults. An explicitly
selected file must exist. Saving targets the selected file; if startup used
built-in defaults, saving creates the first default path for that account.
If default files are absent, Fenrir uses built-in defaults. Python
`settings.conf` files are ignored. Regular users save to their personal TOML
file, including when startup loaded system defaults; root saves to the system
file. `--config PATH` explicitly selects both the load and save path, and that
file must exist at startup.
Create a personal settings file and validate it without opening devices:
+5 -4
View File
@@ -272,10 +272,11 @@ options. Fenrir loads one settings file rather than merging files.
- Root: /etc/fenrirscreenreader/settings/settings.toml.
- --config PATH: use that file explicitly.
If default files are absent, Fenrir uses built-in defaults. An
explicitly selected file must exist. Saving targets the selected file;
if startup used built-in defaults, saving creates the first default path
for that account.
If default files are absent, Fenrir uses built-in defaults. Python
settings.conf files are ignored. Regular users save to their personal TOML
file, including when startup loaded system defaults; root saves to the system
file. --config PATH explicitly selects both the load and save path, and that
file must exist at startup.
Create a personal settings file and validate it without opening devices:
+1 -1
View File
@@ -271,7 +271,7 @@ func TestAttributeSessionToggleQuickSaveAndFreshStartup(t *testing.T) {
t.Fatal(err)
}
saved, _, err := config.Load(path, nil)
if err != nil || saved.Reading.Attributes != mode || saved.Reading.AttributeFormat != "fenrirBold" || saved.Presentation.Speech.Rate != 90 {
if err != nil || saved.Reading.Attributes != mode || saved.Reading.AttributeFormat != "fenrirBold" || saved.Presentation.Speech.Rate != -10 {
t.Fatal("temporary toggle persisted", saved, err)
}
settings = saved
+1 -1
View File
@@ -95,7 +95,7 @@ func (a *application) handle_command(id command.ID) {
case command.Help:
a.ui.open_tutorial(a.options.Bindings, a.options.Scripts, a.settings.live.General.ScriptKeys)
case command.Version:
a.present(i18n.Format("Fenrir screen reader version {0}-{1}", version.Build, version.Channel), "")
a.present(i18n.Format("Fenrir screen reader version {0}-{1}", version.Version, version.Channel), "")
case command.Time:
text, _ := datetime.Format_time(a.settings.live.Presentation.DateTime.TimeFormat, time.Now())
a.present(text, "")
+8 -7
View File
@@ -36,6 +36,7 @@ import (
func TestQuickSpeechSelectionAdjustmentAndBounds(t *testing.T) {
settings := config.Defaults()
settings.Presentation.Speech.Rate = 80 // Exercise the upper rate bound.
voice := &fakeVoice{}
menu := new_quick_settings("speech-dispatcher")
for _, testCase := range []struct {
@@ -89,14 +90,14 @@ func TestHardwareQuickSettingsOnlyOfferSupportedControls(t *testing.T) {
menu.selectSpeech = func(config.Speech, func(error, bool)) {
t.Fatal("hardware numeric adjustment tried reopening the device")
}
for _, want := range []string{"speech pitch 0", "speech volume 100", "speech rate 80"} {
for _, want := range []string{"speech pitch 0", "speech volume 100", "speech rate -20"} {
menu.command(command.NextQuickEntry, &settings, voice)
if got := voice.messages[len(voice.messages)-1]; got != want {
t.Fatal(driver, got, want)
}
}
menu.command(command.NextQuickValue, &settings, voice)
if settings.Presentation.Speech.Rate != 90 || voice.parameters[len(voice.parameters)-1].Rate != 90 {
if settings.Presentation.Speech.Rate != -10 || voice.parameters[len(voice.parameters)-1].Rate != -10 {
t.Fatal(settings, voice.parameters)
}
}
@@ -124,7 +125,7 @@ func TestQuickChangesSurviveMenuCancelAndExplicitSaveThenQuit(t *testing.T) {
t.Fatal(err)
}
saved, _, err := config.Load(path, nil)
if err != nil || saved.Presentation.Speech.Rate != 90 || saved.Presentation.Speech.Pitch != -10 || !saved.Reading.Automatic || saved.Reading.Mode != "table" || !reflect.DeepEqual(saved.General, base.General) {
if err != nil || saved.Presentation.Speech.Rate != -10 || saved.Presentation.Speech.Pitch != -10 || !saved.Reading.Automatic || saved.Reading.Mode != "table" || !reflect.DeepEqual(saved.General, base.General) {
t.Fatal(saved, err)
}
if !strings.Contains(strings.Join(voice.messages, "|"), "Settings saved.") {
@@ -208,7 +209,7 @@ func TestSessionMuteDoesNotPersistAcrossRestart(t *testing.T) {
if err != nil || settings.Presentation.Speech.Enabled != trial.enabled {
t.Fatal("session mute changed saved speech enablement", settings.Presentation.Speech, err)
}
if trial.save && settings.Presentation.Speech.Rate != 90 {
if trial.save && settings.Presentation.Speech.Rate != -10 {
t.Fatal("quick speech adjustment was not saved", settings.Presentation.Speech)
}
}
@@ -277,7 +278,7 @@ func TestSessionAutomaticReadingDoesNotPersistAcrossRestart(t *testing.T) {
if err != nil || settings.Reading.Automatic != trial.automatic || !settings.Presentation.Speech.Enabled {
t.Fatal("session reading toggle changed saved startup behavior", settings, err)
}
if trial.save && settings.Presentation.Speech.Rate != 90 {
if trial.save && settings.Presentation.Speech.Rate != -10 {
t.Fatal("quick adjustment was not saved", settings.Presentation.Speech)
}
}
@@ -305,7 +306,7 @@ func TestSaveFailureKeepsRunningQuickSettings(t *testing.T) {
case <-time.After(time.Second):
t.Fatal("save hung")
}
if saver.busy || settings.Presentation.Speech.Rate != 90 || voice.messages[len(voice.messages)-1] != "Failed to save settings." {
if saver.busy || settings.Presentation.Speech.Rate != -10 || voice.messages[len(voice.messages)-1] != "Failed to save settings." {
t.Fatal(settings, voice.messages)
}
}
@@ -341,7 +342,7 @@ func TestQuickCategoriesRememberSelectionAndWrap(t *testing.T) {
t.Fatalf("%s: got %q, want %q", trial.id, got, trial.want)
}
}
if menu := new_quick_settings("speech-dispatcher"); !menu.command(command.CurrentQuickEntry, &settings, voice) || voice.messages[len(voice.messages)-1] != "speech rate 80" {
if menu := new_quick_settings("speech-dispatcher"); !menu.command(command.CurrentQuickEntry, &settings, voice) || voice.messages[len(voice.messages)-1] != "speech rate -20" {
t.Fatal("new session did not reset selection", voice.messages)
}
}
+1 -1
View File
@@ -199,7 +199,7 @@ func TestMenuSelectionFailurePreservesDiskAndCommittedSaveSurvivesClose(t *testi
t.Fatal("failure changed disk/live", outcome)
}
} else {
if outcome != nil || a.settings.live.Presentation.Speech.Voice != "Base Voice" || a.settings.live.Presentation.Speech.Rate != 90 || a.settings.resetBase.Presentation.Speech.Rate != 80 || a.settings.resetBase.Presentation.Speech.Voice != "Base Voice" {
if outcome != nil || a.settings.live.Presentation.Speech.Voice != "Base Voice" || a.settings.live.Presentation.Speech.Rate != -10 || a.settings.resetBase.Presentation.Speech.Rate != -20 || a.settings.resetBase.Presentation.Speech.Voice != "Base Voice" {
t.Fatal("committed selection or newer numeric override lost", outcome, a.settings.live, a.settings.resetBase)
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"git.stormux.org/storm/fenrir/internal/version"
)
func TestVersionUsesBuildMetadataInBothModes(t *testing.T) {
func TestVersionUsesReleaseMetadataInBothModes(t *testing.T) {
for _, terminalMode := range []bool{false, true} {
voice := &fakeVoice{}
a, err := new_application(context.Background(), &fakeSource{}, &fakeKeys{}, voice, RunOptions{Settings: config.Defaults(), TerminalMode: terminalMode})
+14 -4
View File
@@ -163,7 +163,7 @@ type Reading struct {
}
func Defaults() Settings {
return Settings{Screen: Screen{IgnoredVTs: []int{}}, Review: Review{LineBreak: true, EndOfScreen: true, LeaveReviewOnCursorChange: true, LeaveReviewOnScreenChange: true}, Prompt: Prompt{ExactMatches: []string{}, CustomPatterns: []string{}}, Version: 3, Presentation: Presentation{DateTime: DateTime{TimeFormat: "locale", DateFormat: "locale"}, Progress: Progress{Enabled: true}, Sound: Sound{Enabled: true, Volume: 0.7, Driver: "pulse", Theme: "default", Device: "default"}, Speech: Speech{Enabled: true, Rate: 80, Volume: 100, Driver: "speech-dispatcher"}}, Keyboard: Keyboard{MultiPressTimeoutMS: int(input.DefaultMultiPressTimeout / time.Millisecond), Layout: "desktop", CharacterEcho: true}, Reading: Reading{Emoticons: true, RepeatedSymbolsMin: 4, Spellcheck: true, SpellcheckSound: true, Attributes: "none", AttributeFormat: presentation.Default_attribute_format, Indentation: "none", RaisePitchForCapitals: true, Automatic: true, CursorEcho: true, Mode: "cursor", PunctuationProfile: "default", PunctuationLevel: "some"}, Clipboard: Clipboard{NumberOfClipboards: 200, ExportPath: "/tmp/fenrirClipboard"}, General: General{ScriptPath: "/usr/share/fenrirscreenreader/scripts", ScriptKeys: []string{"KEY_COMPOSE"}}, Scripts: Scripts{Workers: 2, QueueLimit: 16, OutputBytes: 1 << 20}, Remote: Remote{Enabled: true, EnableCommandRemote: true, EnableSettingsRemote: true}}
return Settings{Screen: Screen{IgnoredVTs: []int{}}, Review: Review{LineBreak: true, EndOfScreen: true, LeaveReviewOnCursorChange: true, LeaveReviewOnScreenChange: true}, Prompt: Prompt{ExactMatches: []string{}, CustomPatterns: []string{}}, Version: 3, Presentation: Presentation{DateTime: DateTime{TimeFormat: "locale", DateFormat: "locale"}, Progress: Progress{Enabled: true}, Sound: Sound{Enabled: true, Volume: 0.7, Driver: "pulse", Theme: "default", Device: "default"}, Speech: Speech{Enabled: true, Rate: -20, Volume: 100, Driver: "speech-dispatcher"}}, Keyboard: Keyboard{MultiPressTimeoutMS: int(input.DefaultMultiPressTimeout / time.Millisecond), Layout: "desktop", CharacterEcho: true}, Reading: Reading{Emoticons: true, RepeatedSymbolsMin: 4, Spellcheck: true, SpellcheckSound: true, Attributes: "none", AttributeFormat: presentation.Default_attribute_format, Indentation: "none", RaisePitchForCapitals: true, Automatic: true, CursorEcho: true, Mode: "cursor", PunctuationProfile: "default", PunctuationLevel: "some"}, Clipboard: Clipboard{NumberOfClipboards: 200, ExportPath: "/tmp/fenrirClipboard"}, General: General{ScriptPath: "/usr/share/fenrirscreenreader/scripts", ScriptKeys: []string{"KEY_COMPOSE"}}, Scripts: Scripts{Workers: 2, QueueLimit: 16, OutputBytes: 1 << 20}, Remote: Remote{Enabled: true, EnableCommandRemote: true, EnableSettingsRemote: true}}
}
func (s Settings) Validate() error {
@@ -234,9 +234,8 @@ func (s Settings) Validate() error {
return nil
}
// Paths follows Python Fenrir's selection: root uses the system file; a normal
// user first checks their personal file, then the system file. Sudo also uses
// the system settings, just as Python does.
// Paths lists TOML settings in load order. The first path is also the default
// save destination: personal settings for a normal user, system settings for root.
func Paths(home string, uid int) []string {
if uid == 0 {
return []string{SystemPath}
@@ -293,6 +292,17 @@ func Load(requested string, paths []string) (Settings, string, error) {
return Defaults(), "", nil
}
// Load_startup loads settings and returns their save destination. System
// fallback settings seed a user's first save without making /etc the destination.
// An explicit --config path remains the destination; errors name the source.
func Load_startup(requested string, paths []string) (Settings, string, error) {
settings, path, err := Load(requested, paths)
if err == nil && requested == "" && len(paths) != 0 {
path = paths[0]
}
return settings, path, err
}
// Save validates before touching disk and replaces the file atomically. The
// caller retains its last good in-memory settings on a load/save error.
func Save(path string, settings Settings) error {
+2 -2
View File
@@ -79,7 +79,7 @@ func TestSettingsSelectionDoesNotHideInvalidOrExplicitMissingFiles(t *testing.T)
t.Fatal(err)
}
settings, selected, err = Load("", []string{userPath, systemPath})
if err != nil || selected != userPath || settings.Presentation.Sound.Enabled || settings.Presentation.Speech.Rate != 80 {
if err != nil || selected != userPath || settings.Presentation.Sound.Enabled || settings.Presentation.Speech.Rate != -20 {
t.Fatalf("user selection merged system values: %+v %s %v", settings, selected, err)
}
if err := os.WriteFile(userPath, []byte("invalid = true"), 0600); err != nil {
@@ -151,7 +151,7 @@ func TestShippedExampleMatchesDefaults(t *testing.T) {
func TestSpeechParameterDefaultsAndBounds(t *testing.T) {
for _, data := range []string{"version=1\n", "version=2\n", "version=3\n"} {
settings, err := Decode([]byte(data))
if err != nil || settings.Presentation.Speech.Pitch != 0 || settings.Presentation.Speech.Volume != 100 {
if err != nil || settings.Presentation.Speech.Rate != -20 || settings.Presentation.Speech.Pitch != 0 || settings.Presentation.Speech.Volume != 100 {
t.Fatal(settings, err)
}
}
+1 -1
View File
@@ -45,7 +45,7 @@ func decode_v1(data []byte) (Settings, error) {
General General `toml:"general"`
Scripts Scripts `toml:"scripts"`
Remote Remote `toml:"remote"`
}{Review: defaults.Review, Version: 1, Speech: oldSpeech{Enabled: true, Rate: 80}, Sound: defaults.Presentation.Sound, Keyboard: defaults.Keyboard, Reading: defaults.Reading, Clipboard: defaults.Clipboard, General: defaults.General, Scripts: defaults.Scripts, Remote: defaults.Remote}
}{Review: defaults.Review, Version: 1, Speech: oldSpeech{Enabled: true, Rate: defaults.Presentation.Speech.Rate}, Sound: defaults.Presentation.Sound, Keyboard: defaults.Keyboard, Reading: defaults.Reading, Clipboard: defaults.Clipboard, General: defaults.General, Scripts: defaults.Scripts, Remote: defaults.Remote}
if err := toml.NewDecoder(bytes.NewReader(data)).DisallowUnknownFields().Decode(&old); err != nil {
return Settings{}, fmt.Errorf("parse version 1 settings: %w", err)
}
+95
View File
@@ -0,0 +1,95 @@
// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package config
import (
"bytes"
"os"
"path/filepath"
"testing"
)
func TestStartupSaveDestinationAndLegacySettingsIgnored(t *testing.T) {
for _, scenario := range []string{"defaults", "system fallback", "personal", "explicit", "root"} {
t.Run(scenario, func(t *testing.T) {
root := t.TempDir()
paths := Paths(root, 1000)
userPath := paths[0]
// Substitute an isolated system directory without touching /etc.
systemPath := filepath.Join(root, "etc", "settings.toml")
paths[1] = systemPath
legacyPath := filepath.Join(filepath.Dir(userPath), "settings.conf")
legacy := []byte("[speech]\nrate = 19\n")
if err := os.MkdirAll(filepath.Dir(userPath), 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(legacyPath, legacy, 0600); err != nil {
t.Fatal(err)
}
requested, destination, rate := "", userPath, -20
if scenario != "defaults" {
system := Defaults()
system.Presentation.Speech.Rate = 30
if err := Save(systemPath, system); err != nil {
t.Fatal(err)
}
rate = 30
}
switch scenario {
case "personal":
personal := Defaults()
personal.Presentation.Speech.Rate = 40
if err := Save(userPath, personal); err != nil {
t.Fatal(err)
}
rate = 40
case "explicit":
requested, destination = systemPath, systemPath
case "root":
paths, destination = []string{systemPath}, systemPath
}
systemBefore, _ := os.ReadFile(systemPath)
settings, path, err := Load_startup(requested, paths)
if err != nil || path != destination || settings.Presentation.Speech.Rate != rate {
t.Fatalf("startup: destination=%q rate=%d err=%v", path, settings.Presentation.Speech.Rate, err)
}
// Follow the settings menu's document save workflow, then restart.
doc, err := Open_document(path, settings)
if err != nil || doc.Settings.Presentation.Speech.Rate != rate {
t.Fatalf("open settings: %+v %v", doc, err)
}
settings.Presentation.Speech.Rate = 55
if err := doc.Save(settings); err != nil {
t.Fatal(err)
}
restarted, path, err := Load_startup(requested, paths)
if err != nil || path != destination || restarted.Presentation.Speech.Rate != 55 {
t.Fatalf("restart: destination=%q rate=%d err=%v", path, restarted.Presentation.Speech.Rate, err)
}
if destination != systemPath {
systemAfter, _ := os.ReadFile(systemPath)
if !bytes.Equal(systemBefore, systemAfter) {
t.Fatal("personal save modified the system file")
}
}
if data, err := os.ReadFile(legacyPath); err != nil || !bytes.Equal(data, legacy) {
t.Fatal("legacy settings changed", err)
}
})
}
}
func TestStartupErrorsIdentifySource(t *testing.T) {
root := t.TempDir()
userPath, systemPath := filepath.Join(root, "user.toml"), filepath.Join(root, "system.toml")
if err := os.WriteFile(systemPath, []byte("invalid=true"), 0600); err != nil {
t.Fatal(err)
}
if _, path, err := Load_startup("", []string{userPath, systemPath}); err == nil || path != systemPath {
t.Fatal("invalid fallback did not identify source", path, err)
}
if _, path, err := Load_startup(userPath, []string{systemPath}); err == nil || path != userPath {
t.Fatal("explicit missing file did not identify source", path, err)
}
}
+1 -1
View File
@@ -52,7 +52,7 @@ func TestSettingsFormCoversSchemaAndReturnsIsolatedDraft(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if draft.Presentation.Speech.Enabled || draft.Presentation.Speech.Rate != 81 || !base.Presentation.Speech.Enabled || base.Presentation.Speech.Rate != 80 {
if draft.Presentation.Speech.Enabled || draft.Presentation.Speech.Rate != -19 || !base.Presentation.Speech.Enabled || base.Presentation.Speech.Rate != -20 {
t.Fatal("draft isolation", draft, base)
}
}
+2 -5
View File
@@ -5,12 +5,9 @@
package version
// Keep editable release metadata here, following Cthulhu's version package.
const Version = "2026.10.27"
const Version = "2026.10.28"
const Channel = "testing"
// Build defaults to Version; release builders can override it using -ldflags -X.
var Build = Version
func Label() string {
return Build + "-" + Channel
return Version + "-" + Channel
}
+1 -1
View File
@@ -330,7 +330,7 @@ def main():
server = SpeechServer(root / "speech.sock")
(root / "settings.toml").write_text(
'version=3\n[presentation.sound]\nenabled=false\n'
'[presentation.speech]\n'
'[presentation.speech]\nrate=80\n'
'[presentation.time_and_date]\ntime_format="24-hour-words"\ndate_format="numbers-ymd"\n'
'[screen]\nignored_vts=[1,7,8,10,63]\n'
f'[remote]\nsocket_file="{root / "remote.sock"}"\n'