diff --git a/README.md b/README.md index 833f246f..2c77068b 100644 --- a/README.md +++ b/README.md @@ -153,9 +153,13 @@ Quick settings are available without opening the menu: Quick settings take effect during the session; save them to keep them for the next launch. -Application overrides are selected manually and never saved. While an override -is selected, saving reports **Save unavailable.** A detected return to a shell -prompt, selecting Default, or restarting Fenrir clears its temporary changes. +Application overrides are selected manually and never selected automatically +at startup. While an application override is active, the settings menu and +save shortcut save its preferences to `applications/.toml` inside the +ordinary settings save directory. For example, Ninjam uses +`applications/ninjam.toml`. Only customized values are stored; other values +inherit ordinary settings. Returning to a shell prompt or selecting Default +restores your ordinary settings, including previous unsaved adjustments. See [application overrides](docs/user.md#application-overrides) for details and Ninjam's temporary region shortcuts. In the laptop layout, reading the current attributes now uses **Fenrir+Shift+A**. diff --git a/docs/user.md b/docs/user.md index f12b0863..4629e65b 100644 --- a/docs/user.md +++ b/docs/user.md @@ -26,12 +26,47 @@ launch programs. Choose **Default** to restore the settings and reading regions that were in effect before the override. Overrides are built into Fenrir and are never selected automatically or remembered across restarts. -All settings adjustments made while an override is running are temporary, -including quick settings and remote changes. Saving from the settings menu, -the save shortcut, or remote SAVE/SAVEAS reports **Save unavailable.** and writes -nothing. Menu OK remains unavailable; use quick settings for temporary changes -and Cancel to leave the menu. Saving remains blocked while an override is -selected, including while it is suspended or selected on another console. +While an override is active, **Fenrir+F** opens its application settings, +with the application name in the title. **OK**, **Fenrir+Control+S**, and remote +`SETTING SAVE` save that application's preferences. Quick and remote changes +remain temporary until saved. Default settings are kept separately, including +any unsaved adjustments made before activating the override. + +Profiles live in an `applications/` subdirectory of the ordinary settings save +directory. For example: + +- System settings: `/etc/fenrirscreenreader/settings/applications/ninjam.toml`. +- Personal settings: `~/.local/share/stormux/fenrirscreenreader/settings/applications/ninjam.toml`. +- With `--config /path/settings.toml`: `/path/applications/ninjam.toml`. + +NetworkManager uses `networkmanager.toml`. The directory and file are created +on the first successful save. Files contain only customized settings; omitted +values inherit ordinary settings followed by the built-in application defaults. +A customized voice selection saves module, language, and voice together so a +later change to the ordinary voice does not break that selection. Profiles are +loaded when their override first recognizes the application; restarting Fenrir +still starts with Default. Reselect the application to reload an edited file. + +Application settings include voice, rate, pitch, volume, reading behavior, +sound enablement/volume, and typing echo. Startup settings such as speech driver, +serial device, keyboard layout, and sound theme remain in the default menu; +clipboard storage and console routing also remain global. Session mute and the +temporary automatic-reading toggle are not implicitly saved. Remote `SAVEAS` +and `screen#ignore_screen` are unavailable while an application is active. +Saving is blocked during context transitions or failed voice restoration. +A malformed profile or unavailable voice leaves ordinary settings in effect +and reports an error. Select the application again after correcting the file. + +For example, a rate-only profile contains current TOML setting names, with +no top-level `version` field: + +```toml +[presentation.speech] +rate = 40 +``` + +To make a saved value inherit again, remove that setting from the application +file and reselect the override. Removing the file restores complete inheritance. An override belongs to its terminal. Switching VTs or leaving the terminal suspends its effects; returning resumes it when the application screen is diff --git a/internal/app/application.go b/internal/app/application.go index 599abafd..dbf886e8 100644 --- a/internal/app/application.go +++ b/internal/app/application.go @@ -53,6 +53,7 @@ type application struct { saver *settingsSaver speechPending bool speechResults chan speechChangeResult + overrideLoads chan func() speechChoices chan preferences.Discovery prepareSpeech func(config.Speech) (speech.Transport, error) @@ -99,6 +100,7 @@ func new_application(ctx context.Context, source Source, keyboards Keyboards, vo a.ui.previewSpelling = a.preview_spelling a.ui.discoverChoices, a.ui.saveSettings = options.SettingsChoices, a.save_menu_settings a.speechResults = make(chan speechChangeResult, 1) + a.overrideLoads = make(chan func(), 1) a.speechChoices = make(chan preferences.Discovery, 1) a.prepareSpeech = prepare_speech a.quick.selectSpeech = func(target config.Speech, done func(error, bool)) { diff --git a/internal/app/application_overrides.go b/internal/app/application_overrides.go index 6e741c0d..31bdb76a 100644 --- a/internal/app/application_overrides.go +++ b/internal/app/application_overrides.go @@ -5,11 +5,13 @@ package app import ( "maps" + "path/filepath" "slices" "git.stormux.org/storm/fenrir/internal/applicationoverrides" "git.stormux.org/storm/fenrir/internal/applicationoverrides/networkmanager" "git.stormux.org/storm/fenrir/internal/applicationoverrides/ninjam" + "git.stormux.org/storm/fenrir/internal/config" "git.stormux.org/storm/fenrir/internal/i18n" "git.stormux.org/storm/fenrir/internal/interaction" "git.stormux.org/storm/fenrir/internal/prompt" @@ -26,6 +28,9 @@ type overrideSnapshot struct { type applicationOverride struct { id string + path string + base config.Settings + profile *config.Document terminal int adapter applicationoverrides.Adapter underlying, temporary overrideSnapshot @@ -33,7 +38,59 @@ type applicationOverride struct { initialized, recognized, exitPending bool } -func (a *application) saves_unavailable() bool { return len(a.overrides) != 0 || a.overrideTransition } +func (a *application) saves_unavailable() bool { + return a.overrideTransition || a.overrideRestoreFailed || a.overrideFailed != nil || (a.overrideCurrent != nil && (!a.overrideCurrent.recognized || a.overrideCurrent.path == "")) +} + +func (a *application) settings_path() string { + if a.overrideCurrent != nil { + return a.overrideCurrent.path + } + return a.options.ConfigPath +} + +func (a *application) start_settings_save(path string, settings config.Settings) bool { + if selected := a.overrideCurrent; selected != nil { + base := selected.base + return a.saver.start(a.ctx, settings, func() error { + doc, err := config.Open_application_document(path, base) + if err != nil { + return err + } + return doc.Save(settings) + }) + } + return a.saver.save(a.ctx, path, settings) +} + +// Profile reads are bounded and run off the event loop, like settings writes. +func (a *application) load_application_profile(selected *applicationOverride) { + selected.base = config.Apply_application_settings(a.effective_settings(), a.settings_to_save()) + // Inherit the actual working selection, including startup fallback, rather + // than unavailable saved voices or selections for a next-launch backend. + copy_speech_selection(&selected.base.Presentation.Speech, a.settings.live.Presentation.Speech) + selected.adapter.Configure(&selected.base) + path, base := selected.path, selected.base + a.overrideTransition = true + go func() { + doc, err := config.Open_application_document(path, base) + select { + case a.overrideLoads <- func() { + a.overrideTransition = false + if err != nil { + selected.adapter.Close() + delete(a.overrides, selected.terminal) + a.voice.Say(i18n.Format("Could not load application settings: {0}", err.Error())) + } else { + selected.profile = doc + selected.adapter.Reset() + } + a.overrideDirty = true + }: + case <-a.ctx.Done(): + } + }() +} func (a *application) capture_override(terminal int) overrideSnapshot { settings := a.settings @@ -68,7 +125,7 @@ func (a *application) restore_override(done func()) bool { done() return true } - if a.speechPending || a.overrideTransition || a.overrideRestoreFailed { + if a.speechPending || a.saver.busy || a.overrideTransition || a.overrideRestoreFailed { return false } // A form opened under the application must never outlive that context: @@ -142,6 +199,9 @@ func (a *application) select_override(id string) { a.overrides = make(map[int]*applicationOverride) } selected := &applicationOverride{id: id, terminal: terminal, adapter: adapter} + if a.options.ConfigPath != "" { + selected.path = filepath.Join(filepath.Dir(a.options.ConfigPath), "applications", id+".toml") + } selected.shell.Observe(a.lastSnapshot) a.overrides[terminal] = selected a.voice.Say(i18n.Format("{0} application override selected", i18n.T(adapter.Name()))) @@ -151,7 +211,7 @@ func (a *application) select_override(id string) { // Called before reader.Update so exit detection and restored settings affect // the first shell snapshot, rather than interpreting it with the old adapter. func (a *application) observe_override(next screen.Snapshot) (applicationoverrides.Frame, bool) { - if a.overrideTransition { + if a.overrideTransition || (a.saver.busy && len(a.overrides) != 0) { return applicationoverrides.Frame{}, false } if a.overrideRestoreFailed { @@ -188,14 +248,42 @@ func (a *application) observe_override(next screen.Snapshot) (applicationoverrid return applicationoverrides.Frame{}, false } a.ui.close() + if !selected.initialized && selected.profile == nil { + a.load_application_profile(selected) + return applicationoverrides.Frame{}, false + } selected.underlying = a.capture_override(next.Terminal) if !selected.initialized { settings := a.effective_settings() selected.adapter.Configure(&settings) - a.apply_remote_state(settings, false) - a.configure_reader() - a.engine.Clear_session_regions(next.Terminal) - selected.initialized = true + settings = selected.profile.Apply_application(settings) + activate := func() { + a.apply_remote_state(settings, false) + a.settings.saveDraft, a.settings.resetBase = selected.profile.Settings, selected.profile.Settings + a.settings.remoteFields = make(map[string]uint64) + a.settings.remoteRevision = 0 + *a.quick = *new_quick_settings(settings.Presentation.Speech.Driver) + a.configure_reader() + a.engine.Clear_session_regions(next.Terminal) + selected.initialized, selected.recognized = true, true + a.overrideCurrent = selected + } + if !same_speech_selection(settings.Presentation.Speech, a.settings.live.Presentation.Speech) { + a.overrideTransition = true + a.change_speech(settings.Presentation.Speech, func(err error) { + a.overrideTransition = false + if err != nil { + selected.adapter.Close() + delete(a.overrides, selected.terminal) + a.voice.Say(i18n.Format("Could not load application settings: {0}", err.Error())) + return + } + activate() + selected.adapter.Reset() + }) + return applicationoverrides.Frame{}, false + } + activate() } else { // Numeric/runtime settings are synchronous. Speech selection is // restored separately before resuming a suspended adapter. diff --git a/internal/app/application_overrides_test.go b/internal/app/application_overrides_test.go index 922f30a1..067e32c8 100644 --- a/internal/app/application_overrides_test.go +++ b/internal/app/application_overrides_test.go @@ -6,10 +6,12 @@ package app import ( "errors" "os" + "path/filepath" "reflect" "slices" "strings" "testing" + "time" "git.stormux.org/storm/fenrir/internal/command" "git.stormux.org/storm/fenrir/internal/config" @@ -50,8 +52,8 @@ func TestOverrideRestoresSynthesizerAndKeepsSaveBlockedUntilCompletion(t *testin if response := request_selection(t, a, "SET speech#voice=Temporary;speech#module=fixture"); response != "" { t.Fatal(response) } - if !reflect.DeepEqual(a.settings.saveDraft, baseline) { - t.Fatal("temporary voice modified draft") + if a.settings.saveDraft.Presentation.Speech.Voice != "Temporary" || !same_speech_selection(a.overrideCurrent.underlying.settings.saveDraft.Presentation.Speech, baseline.Presentation.Speech) { + t.Fatal("application voice did not stay in its own draft") } if failed { a.prepareSpeech = func(config.Speech) (speech.Transport, error) { return nil, errors.New("fixture unavailable") } @@ -109,6 +111,19 @@ func override_read(t *testing.T, a *application, snapshot screen.Snapshot) { if err := a.read_screen(); err != nil { t.Fatal(err) } + // Model the event loop's asynchronous profile-read completion, leaving + // speech transitions to the tests which explicitly exercise them. + if a.overrideTransition && !a.speechPending { + select { + case loaded := <-a.overrideLoads: + loaded() + case <-time.After(time.Second): + t.Fatal("profile load stalled") + } + if err := a.read_screen(); err != nil { + t.Fatal(err) + } + } } func TestApplicationsMenuActivatesOnlyOnEnterAndReleasesCapture(t *testing.T) { @@ -183,7 +198,7 @@ func TestApplicationsMenuPrefixSearch(t *testing.T) { } } -func TestOverrideCannotSaveOrContaminateDraftAndRestoresSession(t *testing.T) { +func TestOverrideSavesOwnDraftAndRestoresSession(t *testing.T) { a, voice := persistence_application(t, true) a.handle_command(command.NextQuickValue) // Preserve a pre-existing unsaved adjustment. a.handle_command(command.AutoRead) @@ -202,20 +217,30 @@ func TestOverrideCannotSaveOrContaminateDraftAndRestoresSession(t *testing.T) { if response := a.apply_remote_settings("SET speech#rate=0.77;keyboard#word_echo=True"); response != "" { t.Fatal(response) } - if !reflect.DeepEqual(a.settings.saveDraft, baseSettings.saveDraft) { - t.Fatal("override modified save draft") + if !reflect.DeepEqual(a.overrideCurrent.underlying.settings, baseSettings) { + t.Fatal("ordinary draft changed") } + profilePath := filepath.Join(filepath.Dir(a.options.ConfigPath), "applications", "ninjam.toml") a.save_settings() - if a.saver.busy || voice.messages[len(voice.messages)-1] != "Save unavailable." { - t.Fatal("shortcut save admitted") + finish_persistence(t, a, false) + if !strings.Contains(voice.messages[len(voice.messages)-1], "Ninjam application settings saved") { + t.Fatal(voice.messages) } - for _, text := range []string{"SETTING SAVE", "SETTING SAVEAS " + a.options.ConfigPath} { - if got := runtime_request(a, text); got != "Save unavailable." { - t.Fatal(text, got) - } + doc, err := config.Open_application_document(profilePath, a.overrideCurrent.base) + if err != nil || doc.Settings.Presentation.Speech.Rate != a.settings.live.Presentation.Speech.Rate || !doc.Settings.Keyboard.WordEcho { + t.Fatal("profile not saved", err) + } + reply := persistence_request(a, "SAVE") + finish_persistence(t, a, false) + persistence_reply(t, reply, false) + if got := runtime_request(a, "SETTING SAVEAS "+a.options.ConfigPath); got != "SAVEAS is unavailable in application settings" { + t.Fatal(got) } a.open_settings() receive_ui(t, a.ui) + if !strings.Contains(voice.messages[len(voice.messages)-1], "Ninjam application settings") { + t.Fatal(voice.messages) + } for range 100 { if control, ok := a.ui.session.Focused_control(); ok && control.Action == "save" { break @@ -223,9 +248,7 @@ func TestOverrideCannotSaveOrContaminateDraftAndRestoresSession(t *testing.T) { a.ui.handle(interaction.Key{Name: "tab"}) } a.ui.handle(interaction.Key{Name: "enter"}) - if !a.ui.active || a.saver.busy || voice.messages[len(voice.messages)-1] != "Save unavailable." { - t.Fatal("menu save admitted", voice.messages) - } + finish_persistence(t, a, false) a.ui.close() a.select_override("default") if !reflect.DeepEqual(a.settings, baseSettings) || a.quick.revision != baseQuick || !reflect.DeepEqual(a.engine.Capture_session(), baseRuntime) { @@ -355,3 +378,132 @@ func TestActivationRejectsPendingOperationsAndRestartStartsWithoutOverride(t *te t.Fatal("restart retained override", err) } } + +func TestApplicationProfileReloadAndDefaultInheritance(t *testing.T) { + for _, terminalMode := range []bool{false, true} { + a, _ := persistence_application(t, terminalMode) + override_read(t, a, override_screen(1, "ninjam")) + a.select_override("ninjam") + override_read(t, a, override_screen(1, "ninjam")) + a.handle_command(command.NextQuickValue) + savedRate := a.settings.live.Presentation.Speech.Rate + a.save_settings() + // A terminal switch must wait for the admitted application write. + override_read(t, a, override_screen(3, "other terminal")) + if a.overrideCurrent == nil { + t.Fatal("save ownership lost before completion") + } + finish_persistence(t, a, false) + override_read(t, a, override_screen(3, "other terminal")) + if a.overrideCurrent != nil || a.settings.live.Presentation.Speech.Rate == savedRate { + t.Fatal("application leaked across terminal") + } + // Change an unrelated ordinary setting, then discard/reselect the adapter. + if text := a.apply_remote_settings("SET speech#pitch=0.6"); text != "" { + t.Fatal(text) + } + pitch := a.settings.live.Presentation.Speech.Pitch + override_read(t, a, override_screen(1, "ninjam")) + a.select_override("default") + a.select_override("ninjam") + override_read(t, a, override_screen(1, "ninjam")) + if a.settings.live.Presentation.Speech.Rate != savedRate || a.settings.live.Presentation.Speech.Pitch != pitch { + t.Fatal("saved rate or new inherited pitch missing", a.settings.live.Presentation.Speech) + } + if text := a.apply_remote_settings("SET speech#rate=0.1"); text != "" { + t.Fatal(text) + } + if text := a.reset_remote_settings(); text != "" { + t.Fatal(text) + } + if a.settings.live.Presentation.Speech.Rate != savedRate { + t.Fatal("RESET did not restore profile") + } + before := a.settings + if text := a.apply_remote_settings("SET speech#rate=0.2;screen#ignore_screen=1"); text == "" || !reflect.DeepEqual(a.settings, before) { + t.Fatal("global remote change was not rejected atomically") + } + override_read(t, a, override_screen(1, "[Username@host directory] $")) + if a.overrideCurrent != nil || a.settings.live.Presentation.Speech.Rate == savedRate { + t.Fatal("exit did not restore default") + } + } +} + +func TestApplicationProfileVoiceLoadAndFailureLeaveDefaultIntact(t *testing.T) { + for _, failure := range []string{"", "invalid", "unavailable"} { + a, _, path := selection_application(t, true) + profilePath := filepath.Join(filepath.Dir(path), "applications", "ninjam.toml") + if err := os.MkdirAll(filepath.Dir(profilePath), 0700); err != nil { + t.Fatal(err) + } + data := "[presentation.speech]\nvoice = 'Application voice'\nrate = 12\n" + if failure == "invalid" { + data = "[presentation.speech]\nrate = 999\n" + } + if err := os.WriteFile(profilePath, []byte(data), 0600); err != nil { + t.Fatal(err) + } + a.prepareSpeech = func(config.Speech) (speech.Transport, error) { + if failure == "unavailable" { + return nil, errors.New("voice unavailable") + } + return &preparedVoice{}, nil + } + baseline := a.settings + override_read(t, a, override_screen(1, "ninjam")) + a.select_override("ninjam") + override_read(t, a, override_screen(1, "ninjam")) + if a.speechPending { + finish_selection(t, a) + } + if failure != "" { + if a.overrideCurrent != nil || !reflect.DeepEqual(a.settings, baseline) || a.saves_unavailable() { + t.Fatal("failed profile changed ordinary settings") + } + continue + } + override_read(t, a, override_screen(1, "ninjam")) + if a.settings.live.Presentation.Speech.Voice != "Application voice" || a.settings.live.Presentation.Speech.Rate != 12 { + t.Fatal("profile voice not applied") + } + a.select_override("default") + finish_selection(t, a) + if !reflect.DeepEqual(a.settings, baseline) { + t.Fatal("default voice not restored") + } + } +} + +func TestApplicationMenuPreservesSessionTogglesAndLaunchChoices(t *testing.T) { + a, _ := persistence_application(t, true) + a.settings.saveDraft.Keyboard.Layout = "laptop" + a.settings.saveDraft.Presentation.Speech.Driver = "dectalk" + a.settings.saveDraft.Presentation.Speech.Voice = "Next launch voice" + override_read(t, a, override_screen(1, "ninjam")) + a.select_override("ninjam") + override_read(t, a, override_screen(1, "ninjam")) + a.handle_command(command.Mute) + a.handle_command(command.AutoRead) + a.open_settings() + receive_ui(t, a.ui) + for range 100 { + if control, ok := a.ui.session.Focused_control(); ok && control.Action == "save" { + break + } + a.ui.handle(interaction.Key{Name: "tab"}) + } + a.ui.handle(interaction.Key{Name: "enter"}) + finish_persistence(t, a, false) + if !a.muted || a.engine.Automatic_reading() || a.settings.live.Keyboard.Layout != "desktop" || a.settings.live.Presentation.Speech.Driver != "speech-dispatcher" || a.settings.live.Presentation.Speech.Voice != "" { + t.Fatal("menu changed session toggles or launch-only choices") + } + data, err := os.ReadFile(a.overrideCurrent.path) + if err != nil || strings.Contains(string(data), "enabled") || strings.Contains(string(data), "automatic") || strings.Contains(string(data), "driver") || strings.Contains(string(data), "layout") || strings.Contains(string(data), "voice") { + t.Fatal("menu saved inherited/session/launch settings", string(data), err) + } + a.select_override("default") + if a.settings.saveDraft.Keyboard.Layout != "laptop" || a.settings.saveDraft.Presentation.Speech.Driver != "dectalk" { + t.Fatal("ordinary next-launch choices lost") + } +} diff --git a/internal/app/commands.go b/internal/app/commands.go index c57da3f3..0e93af44 100644 --- a/internal/app/commands.go +++ b/internal/app/commands.go @@ -121,9 +121,7 @@ func (a *application) handle_command(id command.ID) { case command.ProgressMonitor: a.quick.mark_changed("presentation.progress.enabled") a.settings.live.Presentation.Progress.Enabled = !a.settings.live.Presentation.Progress.Enabled - if a.overrideCurrent == nil { - a.settings.saveDraft.Presentation.Progress = a.settings.live.Presentation.Progress - } + a.settings.saveDraft.Presentation.Progress = a.settings.live.Presentation.Progress a.engine.Configure_progress(a.settings.live.Presentation.Progress.Enabled) if a.settings.live.Presentation.Progress.Enabled { a.voice.Say(i18n.T("Progress monitoring enabled")) @@ -176,9 +174,7 @@ func (a *application) review_command(id command.ID) { } switch id { case command.ToggleTracking: - if a.overrideCurrent == nil { - a.settings.saveDraft.Reading.Mode = a.engine.Tracking_mode() - } + a.settings.saveDraft.Reading.Mode = a.engine.Tracking_mode() } if spelling := result.Spelling; spelling != nil && !a.temporarySilence { a.voice.say_spelling(*spelling) diff --git a/internal/app/event_loop.go b/internal/app/event_loop.go index 3a5eab23..3ad2d700 100644 --- a/internal/app/event_loop.go +++ b/internal/app/event_loop.go @@ -114,6 +114,8 @@ func (a *application) loop(ticks <-chan time.Time) error { select { case discovery := <-a.speechChoices: a.quick.discovery = discovery + case loaded := <-a.overrideLoads: + loaded() case result := <-a.speechResults: a.speech_result(result) if len(a.overrides) != 0 || a.overrideCurrent != nil { @@ -126,6 +128,7 @@ func (a *application) loop(ticks <-chan time.Time) error { a.spellcheck_result(result) case err := <-a.saver.results: a.settings_save_result(err) + a.overrideDirty = true a.quitting = a.quitAfterSave case result := <-scriptResults: if err := a.poll_input(false); err != nil { diff --git a/internal/app/fenrir_menu.go b/internal/app/fenrir_menu.go index 5ceff3e9..b8673f35 100644 --- a/internal/app/fenrir_menu.go +++ b/internal/app/fenrir_menu.go @@ -27,7 +27,15 @@ import ( ) func (ui *internalUI) open_settings(settings config.Settings, path string, liveQuick ...config.Settings) { - generation, err := ui.begin(preferences.MenuTitle) + ui.open_settings_context(settings, path, nil, preferences.MenuTitle, "", liveQuick...) +} + +func (ui *internalUI) open_application_settings(settings config.Settings, path string, base config.Settings, name string) { + ui.open_settings_context(settings, path, &base, i18n.Format("{0} application settings", name), i18n.Format("{0} application settings saved.", name)) +} + +func (ui *internalUI) open_settings_context(settings config.Settings, path string, applicationBase *config.Settings, title string, savedMessage string, liveQuick ...config.Settings) { + generation, err := ui.begin(title) if err != nil { ui.failed("Could not open Fenrir Menu: {0}", err) return @@ -47,7 +55,16 @@ func (ui *internalUI) open_settings(settings config.Settings, path string, liveQ previewChanged := ui.changed speechOverride := ui.speechOverride go func() { - doc, err := config.Open_document(path, settings) + var doc *config.Document + var err error + if applicationBase != nil { + doc, err = config.Open_application_document(path, *applicationBase) + if err == nil { + doc.Settings = settings + } + } else { + doc, err = config.Open_document(path, settings) + } if err != nil { ui.post(uiUpdate{generation: generation, close: true, speech: i18n.Format("Could not open Fenrir Menu: {0}", err.Error())}) return @@ -62,11 +79,20 @@ func (ui *internalUI) open_settings(settings config.Settings, path string, liveQ if ui.discoverChoices != nil { discovery = ui.discoverChoices(doc.Settings) } - session, err := preferences.New_session(doc.Settings, doc.Order, discovery.Choices(doc.Settings)) + var session *interaction.Session + if applicationBase != nil { + session, err = preferences.New_application_session(doc.Settings, doc.Order, title, discovery.Choices(doc.Settings)) + } else { + session, err = preferences.New_session(doc.Settings, doc.Order, discovery.Choices(doc.Settings)) + } if err != nil { ui.post(uiUpdate{generation: generation, close: true, speech: i18n.Format("Could not open Fenrir Menu: {0}", err.Error())}) return } + readSettings := preferences.Settings + if applicationBase != nil { + readSettings = preferences.Application_settings + } previous := doc.Settings catalogs := map[string]preferences.Discovery{previous.Presentation.Speech.Driver: discovery} pending := map[string]bool{} @@ -77,7 +103,7 @@ func (ui *internalUI) open_settings(settings config.Settings, path string, liveQ previewChanged(session) } if ui.cancelTest != nil { - draft, err := preferences.Settings(session, doc.Settings) + draft, err := readSettings(session, doc.Settings) if err != nil || draft.Presentation.Speech != tested { ui.stop_test() } @@ -121,7 +147,7 @@ func (ui *internalUI) open_settings(settings config.Settings, path string, liveQ var action func(string, *interaction.Session) action = func(name string, session *interaction.Session) { if name == "test-synthesizer" { - draft, err := preferences.Settings(session, doc.Settings) + draft, err := readSettings(session, doc.Settings) if err != nil { ui.test_failed(err) return @@ -169,7 +195,7 @@ func (ui *internalUI) open_settings(settings config.Settings, path string, liveQ if name != "save" { return } - draft, err := preferences.Settings(session, doc.Settings) + draft, err := readSettings(session, doc.Settings) if err != nil { ui.failed("Could not save settings: {0}", err) return @@ -184,6 +210,9 @@ func (ui *internalUI) open_settings(settings config.Settings, path string, liveQ ui.onClosed = nil // Successful save commits the form preview. } ui.update(uiUpdate{generation: generation, close: true}) + if applicationBase != nil { + ui.voice.Say(savedMessage) + } }) { ui.session = nil } diff --git a/internal/app/remote_persistence.go b/internal/app/remote_persistence.go index 8f9b50ea..c29583aa 100644 --- a/internal/app/remote_persistence.go +++ b/internal/app/remote_persistence.go @@ -10,6 +10,7 @@ import ( "git.stormux.org/storm/fenrir/internal/config" "git.stormux.org/storm/fenrir/internal/control" + "git.stormux.org/storm/fenrir/internal/i18n" ) // These operations run only after the shared credential and settings gates. @@ -27,6 +28,9 @@ func (a *application) remote_settings_request(text string, request control.Reque return "Settings are busy; try again after the current operation", false } if verb == "SET" { + if err := a.check_application_remote(text); err != nil { + return err.Error(), false + } updated, err := remote_settings(a.effective_settings(), text) if err != nil { return err.Error(), false @@ -77,8 +81,11 @@ func (a *application) remote_settings_request(text string, request control.Reque } return a.reset_remote_settings(), false } - path := a.options.ConfigPath + path := a.settings_path() if verb == "SAVEAS" { + if a.overrideCurrent != nil { + return "SAVEAS is unavailable in application settings", false + } // A root TTY reader also admits its active console user for ordinary // commands. That must not grant arbitrary root-owned file writes. if request.UID != 0 && request.UID != os.Geteuid() { @@ -89,7 +96,7 @@ func (a *application) remote_settings_request(text string, request control.Reque if path == "" || strings.ContainsAny(path, "\x00\r\n") { return "A valid settings destination is required", false } - if !a.saver.save(a.ctx, path, a.settings_to_save()) { + if !a.start_settings_save(path, a.settings_to_save()) { return "Settings are busy; try again after the current operation", false } a.saver.revision = a.settings.remoteRevision @@ -119,6 +126,9 @@ func (a *application) settings_save_result(err error) { response = "Could not save settings: " + err.Error() } remote_reply(reply, response) + } else if a.overrideCurrent != nil && err == nil { + a.saver.busy = false + a.voice.Say(i18n.Format("{0} application settings saved.", i18n.T(a.overrideCurrent.adapter.Name()))) } else { a.saver.result(err, a.voice) } @@ -149,10 +159,6 @@ func (a *application) reset_remote_settings() string { if err != nil { return err.Error() } - if a.overrideCurrent != nil { - a.apply_remote_state(updated, false) - return "" - } a.settings.saveDraft = persisted _, speechOverridden := a.settings.remoteFields["speech#enabled"] a.apply_remote_state(updated, speechOverridden) @@ -203,3 +209,18 @@ func copy_remote_settings(target *config.Settings, source config.Settings, field } } } + +// Screen routing is global and has no application-profile counterpart. +func (a *application) check_application_remote(text string) error { + if a.overrideCurrent == nil { + return nil + } + _, parameters, _ := strings.Cut(text, " ") + for _, assignment := range remote_assignments(parameters) { + path, _, _ := strings.Cut(assignment, "=") + if strings.EqualFold(strings.TrimSpace(path), "screen#ignore_screen") { + return fmt.Errorf("screen#ignore_screen is unavailable in application settings") + } + } + return nil +} diff --git a/internal/app/runtime_settings.go b/internal/app/runtime_settings.go index 8e750e34..eea33e29 100644 --- a/internal/app/runtime_settings.go +++ b/internal/app/runtime_settings.go @@ -139,6 +139,10 @@ func (a *application) open_settings() { selected := a.settings.live.Presentation.Speech a.ui.speechOverride = &selected } + if selected := a.overrideCurrent; selected != nil { + a.ui.open_application_settings(a.settings_to_save(), selected.path, selected.base, i18n.T(selected.adapter.Name())) + return + } if a.quick.changed { a.ui.open_settings(a.settings.live, a.options.ConfigPath, a.settings.live) } else { @@ -155,7 +159,7 @@ func (a *application) save_settings() { a.voice.Say(i18n.T("Failed to save settings.")) return } - if a.saver.save(a.ctx, a.options.ConfigPath, a.settings_to_save()) { + if a.start_settings_save(a.settings_path(), a.settings_to_save()) { a.saver.revision = a.settings.remoteRevision } } @@ -172,6 +176,7 @@ func (a *application) save_menu_settings(doc *config.Document, draft config.Sett a.ui.failed("Could not save settings: {0}", fmt.Errorf("speech selection in progress")) return false } + previousDraft := a.settings.saveDraft speechApplied := a.settings.speechAvailable && draft.Presentation.Speech.Driver == a.settings.live.Presentation.Speech.Driver && !same_speech_selection(draft.Presentation.Speech, a.settings.saveDraft.Presentation.Speech) if !a.saver.start(a.ctx, draft, func() error { var prepared speech.Transport @@ -220,7 +225,24 @@ func (a *application) save_menu_settings(doc *config.Document, draft config.Sett if a.quick.fields["presentation.progress.enabled"] > quickRevision { merged.Presentation.Progress = a.settings.live.Presentation.Progress } - a.apply_saved_settings(merged, speechApplied && err == nil) + if a.overrideCurrent != nil { + live := config.Apply_application_settings(a.effective_settings(), merged) + if merged.Presentation.Speech.Enabled == previousDraft.Presentation.Speech.Enabled { + live.Presentation.Speech.Enabled = !a.muted + } + if merged.Reading.Automatic == previousDraft.Reading.Automatic { + live.Reading.Automatic = a.engine.Automatic_reading() + } + if !speechApplied || err != nil { + copy_speech_selection(&live.Presentation.Speech, a.settings.live.Presentation.Speech) + } + a.apply_remote_state(live, false) + a.configure_reader() + a.settings.saveDraft = merged + a.quick.selectionChanged = false + } else { + a.apply_saved_settings(merged, speechApplied && err == nil) + } a.settings.resetBase = draft // RESET reflects disk, not later overrides. a.quick.changed = newerQuick } @@ -249,6 +271,9 @@ func (a *application) effective_settings() config.Settings { // Remote assignments are validated against effective session state, then // separately overlaid on the save draft so unrelated next-launch choices survive. func (a *application) apply_remote_settings(text string) string { + if err := a.check_application_remote(text); err != nil { + return err.Error() + } updated, err := remote_settings(a.effective_settings(), text) if err != nil { return err.Error() @@ -256,10 +281,6 @@ func (a *application) apply_remote_settings(text string) string { if updated.Presentation.Speech.Enabled && !a.settings.speechAvailable { return "Speech was disabled at startup; enable it in settings and relaunch" } - if a.overrideCurrent != nil { - a.apply_remote_state(updated, false) - return "" - } // Apply only the requested assignments to the persistence // draft, retaining unrelated next-launch menu settings. draftBase := a.settings.saveDraft diff --git a/internal/config/application.go b/internal/config/application.go new file mode 100644 index 00000000..1c5a2ab1 --- /dev/null +++ b/internal/config/application.go @@ -0,0 +1,140 @@ +// Copyright (c) 2026 Storm Dragon +// SPDX-License-Identifier: GPL-3.0-or-later + +package config + +import ( + "bytes" + "errors" + "fmt" + "os" + "reflect" + "strings" + + "github.com/pelletier/go-toml/v2" +) + +// Application_setting admits only settings which can be applied and restored +// without changing devices, routing, storage, or other process-wide services. +func Application_setting(path string) bool { + switch path { + case "presentation.speech.enabled", "presentation.speech.rate", "presentation.speech.pitch", "presentation.speech.volume", "presentation.speech.module", "presentation.speech.language", "presentation.speech.voice", + "presentation.sound.enabled", "presentation.sound.volume", "presentation.progress.enabled", + "presentation.time_and_date.time_format", "presentation.time_and_date.date_format", + "keyboard.character_echo", "keyboard.word_echo", "keyboard.echo_on_capslock": + return true + case "reading.automatic", "reading.cursor_echo", "reading.mode", "reading.punctuation_level", + "reading.raise_pitch_for_capitals", "reading.beep_for_capitals", "reading.say_cap_for_capitals", + "reading.indentation", "reading.attributes", "reading.attribute_format", "reading.spellcheck", + "reading.spellcheck_read_word", "reading.spellcheck_say_misspelled", "reading.spellcheck_sound", + "reading.emoticons", "reading.repeated_symbols", "reading.repeated_symbols_min": + return true + } + + return false +} + +// Open_application_document overlays a sparse, current-schema profile on the +// application's inherited settings. Missing files inherit everything. +func Open_application_document(path string, base Settings) (*Document, error) { + doc := &Document{Settings: base, path: path, applicationBase: &base, applicationFields: make(map[string]bool)} + data, err := read_document(path) + if errors.Is(err, os.ErrNotExist) { + return doc, nil + } + if err != nil { + return nil, err + } + var fields map[string]any + if err := toml.Unmarshal(data, &fields); err != nil { + return nil, err + } + var collect func(map[string]any, string) error + collect = func(fields map[string]any, prefix string) error { + for name, value := range fields { + path := prefix + name + if table, ok := value.(map[string]any); ok { + if err := collect(table, path+"."); err != nil { + return err + } + continue + } + if !Application_setting(path) { + return fmt.Errorf("setting %s is not supported in application profiles", path) + } + doc.applicationFields[path] = true + } + return nil + } + if err := collect(fields, ""); err != nil { + return nil, err + } + if err := toml.NewDecoder(bytes.NewReader(data)).DisallowUnknownFields().Decode(&doc.Settings); err != nil { + return nil, err + } + if err := doc.Settings.Validate(); err != nil { + return nil, err + } + doc.original, doc.existed = data, true + return doc, nil +} + +// Apply_application preserves inherited session state for every omitted field. +func (d *Document) Apply_application(base Settings) Settings { + for path := range d.applicationFields { + setting_value(reflect.ValueOf(&base).Elem(), path).Set(setting_value(reflect.ValueOf(d.Settings), path)) + } + return base +} + +func (d *Document) save_application(settings Settings) error { + fields := make(map[string]any) + selectionChanged := false + for _, path := range []string{"presentation.speech.module", "presentation.speech.language", "presentation.speech.voice"} { + selectionChanged = selectionChanged || d.applicationFields[path] || !reflect.DeepEqual(setting_value(reflect.ValueOf(settings), path).Interface(), setting_value(reflect.ValueOf(*d.applicationBase), path).Interface()) + } + var visit func(reflect.Value, reflect.Value, string) + visit = func(value, base reflect.Value, prefix string) { + for index := range value.NumField() { + path := prefix + value.Type().Field(index).Tag.Get("toml") + child, inherited := value.Field(index), base.Field(index) + if child.Kind() == reflect.Struct { + visit(child, inherited, path+".") + continue + } + speechSelection := selectionChanged && (path == "presentation.speech.module" || path == "presentation.speech.language" || path == "presentation.speech.voice") + if !Application_setting(path) || (!speechSelection && !d.applicationFields[path] && reflect.DeepEqual(child.Interface(), inherited.Interface())) { + continue + } + parts := strings.Split(path, ".") + table := fields + for _, part := range parts[:len(parts)-1] { + if table[part] == nil { + table[part] = make(map[string]any) + } + table = table[part].(map[string]any) + } + table[parts[len(parts)-1]] = child.Interface() + } + } + visit(reflect.ValueOf(settings), reflect.ValueOf(*d.applicationBase), "") + data, err := toml.Marshal(fields) + if err != nil { + return err + } + return save_data(d.path, data) +} + +// Apply_application_settings copies the supported live settings only. Inherited +// next-launch choices must never replace the running backend or layout. +func Apply_application_settings(base Settings, source Settings) Settings { + for _, definition := range Definitions() { + if Application_setting(definition.Path) { + if err := definition.Set(&base, definition.Value(source)); err != nil { + panic(err) + } + } + } + base.Presentation.Progress = source.Presentation.Progress + return base +} diff --git a/internal/config/application_test.go b/internal/config/application_test.go new file mode 100644 index 00000000..2c87e314 --- /dev/null +++ b/internal/config/application_test.go @@ -0,0 +1,117 @@ +// Copyright (c) 2026 Storm Dragon +// SPDX-License-Identifier: GPL-3.0-or-later + +package config + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestApplicationDocumentSparseInheritanceAndExplicitValues(t *testing.T) { + path := filepath.Join(t.TempDir(), "applications", "ninjam.toml") + base := Defaults() + doc, err := Open_application_document(path, base) + if err != nil { + t.Fatal(err) + } + saved := base + saved.Presentation.Speech.Rate = 12 + saved.Keyboard.WordEcho = true + if err := doc.Save(saved); err != nil { + t.Fatal(err) + } + data, _ := os.ReadFile(path) + if strings.Contains(string(data), "driver") || strings.Contains(string(data), "automatic") || strings.Contains(string(data), "version") { + t.Fatal("inherited values copied", string(data)) + } + info, _ := os.Stat(path) + if info.Mode().Perm() != 0600 { + t.Fatal(info.Mode()) + } + // A new ordinary voice is inherited; explicitly saved rate/echo survive. + base.Presentation.Speech.Voice = "New default" + base.Presentation.Speech.Rate = 12 + doc, err = Open_application_document(path, base) + if err != nil || doc.Settings.Presentation.Speech.Voice != "New default" || !doc.Settings.Keyboard.WordEcho { + t.Fatal(doc, err) + } + if err := doc.Save(doc.Settings); err != nil { + t.Fatal(err) + } + base.Presentation.Speech.Rate = 60 + doc, err = Open_application_document(path, base) + if err != nil || doc.Settings.Presentation.Speech.Rate != 12 { + t.Fatal("explicit value equal to previous base lost", doc, err) + } + session := base + session.Presentation.Speech.Enabled = false + session.Reading.Automatic = false + effective := doc.Apply_application(session) + if effective.Presentation.Speech.Enabled || effective.Reading.Automatic || effective.Presentation.Speech.Rate != 12 { + t.Fatal("session state overwritten", effective) + } +} + +func TestApplicationDocumentRejectsInvalidProfilesAndExternalChanges(t *testing.T) { + path := filepath.Join(t.TempDir(), "profile.toml") + for _, data := range []string{ + "[presentation.speech]\nrate = 101\n", + "[presentation.speech]\nrate = 'fast'\n", + "[presentation.speech]\ndriver = 'dectalk'\n", + "[keyboard]\nlayout = 'laptop'\n", + "[screen]\nignored_vts = [1]\n", + "[reading]\nunknown = true\n", + "[reading]\nmode = 'invalid'\n", + "[broken", + } { + if err := os.WriteFile(path, []byte(data), 0600); err != nil { + t.Fatal(err) + } + if _, err := Open_application_document(path, Defaults()); err == nil { + t.Fatal("invalid profile accepted", data) + } + } + if err := os.WriteFile(path, []byte("[presentation.speech]\nrate = 10\n"), 0600); err != nil { + t.Fatal(err) + } + doc, err := Open_application_document(path, Defaults()) + if err != nil { + t.Fatal(err) + } + changed := []byte("[presentation.speech]\nrate = 20\n") + if err := os.WriteFile(path, changed, 0600); err != nil { + t.Fatal(err) + } + if err := doc.Save(doc.Settings); err == nil { + t.Fatal("external edit overwritten") + } + data, _ := os.ReadFile(path) + if string(data) != string(changed) { + t.Fatal(string(data)) + } +} + +func TestApplicationVoiceSelectionSavedTogether(t *testing.T) { + path := filepath.Join(t.TempDir(), "profile.toml") + base := Defaults() + base.Presentation.Speech.Module = "original-module" + base.Presentation.Speech.Language = "en" + doc, err := Open_application_document(path, base) + if err != nil { + t.Fatal(err) + } + chosen := base + chosen.Presentation.Speech.Voice = "Application voice" + if err := doc.Save(chosen); err != nil { + t.Fatal(err) + } + base.Presentation.Speech.Module = "different-module" + base.Presentation.Speech.Language = "fr" + doc, err = Open_application_document(path, base) + if err != nil || doc.Settings.Presentation.Speech.Module != "original-module" || doc.Settings.Presentation.Speech.Language != "en" || doc.Settings.Presentation.Speech.Voice != "Application voice" { + t.Fatal("coupled voice selection changed", doc, err) + } +} diff --git a/internal/config/document.go b/internal/config/document.go index 2a014340..848d32f8 100644 --- a/internal/config/document.go +++ b/internal/config/document.go @@ -34,11 +34,13 @@ import ( // 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 + Settings Settings + Order []string + path string + original []byte + existed bool + applicationBase *Settings + applicationFields map[string]bool } func read_document(path string) ([]byte, error) { @@ -156,6 +158,9 @@ func (d *Document) Save(settings Settings) error { if err := settings.Validate(); err != nil { return err } + if d.applicationBase != nil { + return d.save_application(settings) + } // The same schema drives persistence and the form, including nested groups. var output bytes.Buffer fmt.Fprintf(&output, "version = %d\n", settings.Version) diff --git a/internal/i18n/locale/de/LC_MESSAGES/fenrir.po b/internal/i18n/locale/de/LC_MESSAGES/fenrir.po index 4820eec6..0be42942 100644 --- a/internal/i18n/locale/de/LC_MESSAGES/fenrir.po +++ b/internal/i18n/locale/de/LC_MESSAGES/fenrir.po @@ -67,11 +67,11 @@ msgstr "Wütend" msgid "Apple" msgstr "Apfel" -#: internal/app/application_overrides.go:237 +#: internal/app/application_overrides.go:325 msgid "Application" msgstr "Anwendung" -#: internal/app/application_overrides.go:236 +#: internal/app/application_overrides.go:324 msgid "Applications" msgstr "Anwendungen" @@ -163,8 +163,8 @@ msgstr "Schmetterling" msgid "Canada" msgstr "Kanada" -#: internal/app/application_overrides.go:238 -#: internal/preferences/settings.go:79 +#: internal/app/application_overrides.go:326 +#: internal/preferences/settings.go:93 msgid "Cancel" msgstr "Abbrechen" @@ -256,7 +256,7 @@ msgstr "Live-Bereich 9 löschen" msgid "Clipboard" msgstr "Zwischenablage" -#: internal/app/application.go:142 internal/app/application.go:145 +#: internal/app/application.go:144 internal/app/application.go:147 #: internal/app/clipboard.go:218 internal/app/clipboard.go:374 msgid "Clipboard import failed" msgstr "Import aus der Zwischenablage fehlgeschlagen" @@ -289,8 +289,13 @@ msgstr "Cool" msgid "Could not check spelling: {0}" msgstr "Rechtschreibung konnte nicht geprüft werden: {0}" -#: internal/app/application_overrides.go:241 -#: internal/app/application_overrides.go:245 +#: internal/app/application_overrides.go:278 +#: internal/app/application_overrides.go:83 +msgid "Could not load application settings: {0}" +msgstr "Anwendungseinstellungen konnten nicht geladen werden: {0}" + +#: internal/app/application_overrides.go:329 +#: internal/app/application_overrides.go:333 msgid "Could not open Applications: {0}" msgstr "Anwendungen konnten nicht geöffnet werden: {0}" @@ -298,8 +303,8 @@ msgstr "Anwendungen konnten nicht geöffnet werden: {0}" msgid "Could not open Emoji Menu: {0}" msgstr "Emoji-Menü konnte nicht geöffnet werden: {0}" -#: internal/app/fenrir_menu.go:32 internal/app/fenrir_menu.go:52 -#: internal/app/fenrir_menu.go:67 internal/app/runtime_settings.go:133 +#: internal/app/fenrir_menu.go:40 internal/app/fenrir_menu.go:69 +#: internal/app/fenrir_menu.go:89 internal/app/runtime_settings.go:133 msgid "Could not open Fenrir Menu: {0}" msgstr "Fenrir-Menü konnte nicht geöffnet werden: {0}" @@ -307,8 +312,8 @@ msgstr "Fenrir-Menü konnte nicht geöffnet werden: {0}" msgid "Could not open Spellcheck Menu: {0}" msgstr "Rechtschreibmenü konnte nicht geöffnet werden: {0}" -#: internal/app/application_overrides.go:104 -#: internal/app/application_overrides.go:208 +#: internal/app/application_overrides.go:161 +#: internal/app/application_overrides.go:296 msgid "Could not restore application settings: {0}" msgstr "Anwendungseinstellungen konnten nicht wiederhergestellt werden: {0}" @@ -316,8 +321,8 @@ msgstr "Anwendungseinstellungen konnten nicht wiederhergestellt werden: {0}" msgid "Could not save exceptions: {0}" msgstr "Ausnahmen konnten nicht gespeichert werden: {0}" -#: internal/app/fenrir_menu.go:174 internal/app/fenrir_menu.go:180 -#: internal/app/runtime_settings.go:172 internal/app/runtime_settings.go:199 +#: internal/app/fenrir_menu.go:200 internal/app/fenrir_menu.go:206 +#: internal/app/runtime_settings.go:176 internal/app/runtime_settings.go:204 msgid "Could not save settings: {0}" msgstr "Einstellungen konnten nicht gespeichert werden: {0}" @@ -337,12 +342,12 @@ msgstr "Dolch" msgid "Date format" msgstr "Datumsformat" -#: internal/app/application_overrides.go:230 +#: internal/app/application_overrides.go:318 msgid "Default" msgstr "Standard" -#: internal/app/application_overrides.go:129 -#: internal/app/application_overrides.go:182 +#: internal/app/application_overrides.go:186 +#: internal/app/application_overrides.go:242 msgid "Default settings restored" msgstr "Standardeinstellungen wiederhergestellt" @@ -445,7 +450,7 @@ msgstr "" msgid "Face with symbols" msgstr "Gesicht mit Symbolen" -#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:155 +#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:159 msgid "Failed to save settings." msgstr "Einstellungen konnten nicht gespeichert werden." @@ -489,7 +494,7 @@ msgstr "Gefaltete Hände" msgid "Food" msgstr "Essen" -#: internal/app/commands.go:145 +#: internal/app/commands.go:143 msgid "Forward next keypress" msgstr "Nächste Taste durchreichen" @@ -726,7 +731,7 @@ msgstr "Übelkeit" msgid "Netherlands" msgstr "Niederlande" -#: internal/app/application_overrides.go:230 +#: internal/app/application_overrides.go:318 msgid "NetworkManager" msgstr "NetworkManager" @@ -734,7 +739,7 @@ msgstr "NetworkManager" msgid "New channel" msgstr "Neuer Kanal" -#: internal/app/application_overrides.go:230 +#: internal/app/application_overrides.go:318 msgid "Ninjam" msgstr "Ninjam" @@ -787,7 +792,7 @@ msgstr "Nicht im Lesemodus" msgid "Not in window Mode" msgstr "Nicht im Fenstermodus" -#: internal/preferences/settings.go:79 +#: internal/preferences/settings.go:93 msgid "OK" msgstr "OK" @@ -850,11 +855,11 @@ msgstr "" "Gibt den aktuell ausgewählten Text aus, der in die Zwischenablage kopiert " "wird" -#: internal/app/commands.go:131 +#: internal/app/commands.go:129 msgid "Progress monitoring disabled" msgstr "Fortschrittsüberwachung deaktiviert" -#: internal/app/commands.go:129 +#: internal/app/commands.go:127 msgid "Progress monitoring enabled" msgstr "Fortschrittsüberwachung aktiviert" @@ -1006,7 +1011,7 @@ msgstr "Traurig" msgid "Santa" msgstr "Weihnachtsmann" -#: internal/app/runtime_settings.go:151 internal/app/runtime_settings.go:168 +#: internal/app/runtime_settings.go:155 internal/app/runtime_settings.go:172 msgid "Save unavailable." msgstr "Speichern nicht verfügbar." @@ -1030,7 +1035,7 @@ msgstr "„Falsch geschrieben“ ansagen" msgid "Screaming" msgstr "Schreiend" -#: internal/app/commands.go:154 internal/app/scripts.go +#: internal/app/commands.go:152 internal/app/scripts.go msgid "Script failed" msgstr "Skript fehlgeschlagen" @@ -1078,8 +1083,8 @@ msgstr "Live-Bereich aus einer oder zwei Markierungen festlegen" msgid "Set window begin and end marks" msgstr "Setze Fensteranfangs- und -endmarken" -#: internal/app/application_overrides.go:114 -#: internal/app/application_overrides.go:227 internal/app/commands.go:84 +#: internal/app/application_overrides.go:171 +#: internal/app/application_overrides.go:315 internal/app/commands.go:84 msgid "Settings are busy" msgstr "Einstellungen sind beschäftigt" @@ -1227,11 +1232,11 @@ msgstr "Symbole" msgid "Taco" msgstr "Taco" -#: internal/preferences/settings.go:72 +#: internal/preferences/settings.go:83 msgid "Test synthesizer" msgstr "Sprachsynthesizer testen" -#: internal/app/fenrir_menu.go:139 +#: internal/app/fenrir_menu.go:165 msgid "Testing voice" msgstr "Stimme wird getestet" @@ -2187,7 +2192,7 @@ msgstr "Tonhöhe" msgid "speech rate" msgstr "Sprechgeschwindigkeit" -#: internal/app/commands.go:140 +#: internal/app/commands.go:138 msgid "speech temporary disabled" msgstr "Sprachausgabe vorübergehend ausgeschaltet" @@ -2282,10 +2287,18 @@ msgstr "Zwinkern" msgid "{0} added to dictionary" msgstr "{0} zum Wörterbuch hinzugefügt" -#: internal/app/application_overrides.go:147 +#: internal/app/application_overrides.go:207 msgid "{0} application override selected" msgstr "Anwendungseinstellungen für {0} ausgewählt" +#: internal/app/fenrir_menu.go:34 +msgid "{0} application settings" +msgstr "Anwendungseinstellungen für {0}" + +#: internal/app/fenrir_menu.go:34 internal/app/remote_persistence.go:131 +msgid "{0} application settings saved." +msgstr "Anwendungseinstellungen für {0} gespeichert." + #: internal/interaction/presentation.go:53 msgid "{0} button" msgstr "{0} Schaltfläche" diff --git a/internal/i18n/locale/es/LC_MESSAGES/fenrir.po b/internal/i18n/locale/es/LC_MESSAGES/fenrir.po index aced78b8..4bea434a 100644 --- a/internal/i18n/locale/es/LC_MESSAGES/fenrir.po +++ b/internal/i18n/locale/es/LC_MESSAGES/fenrir.po @@ -67,11 +67,11 @@ msgstr "Enfadado" msgid "Apple" msgstr "Manzana" -#: internal/app/application_overrides.go:237 +#: internal/app/application_overrides.go:325 msgid "Application" msgstr "Aplicación" -#: internal/app/application_overrides.go:236 +#: internal/app/application_overrides.go:324 msgid "Applications" msgstr "Aplicaciones" @@ -163,8 +163,8 @@ msgstr "Mariposa" msgid "Canada" msgstr "Canadá" -#: internal/app/application_overrides.go:238 -#: internal/preferences/settings.go:79 +#: internal/app/application_overrides.go:326 +#: internal/preferences/settings.go:93 msgid "Cancel" msgstr "Cancelar" @@ -256,7 +256,7 @@ msgstr "Borrar la región dinámica 9" msgid "Clipboard" msgstr "Portapapeles" -#: internal/app/application.go:142 internal/app/application.go:145 +#: internal/app/application.go:144 internal/app/application.go:147 #: internal/app/clipboard.go:218 internal/app/clipboard.go:374 msgid "Clipboard import failed" msgstr "Error al importar el portapapeles" @@ -289,8 +289,13 @@ msgstr "Genial" msgid "Could not check spelling: {0}" msgstr "No se pudo revisar la ortografía: {0}" -#: internal/app/application_overrides.go:241 -#: internal/app/application_overrides.go:245 +#: internal/app/application_overrides.go:278 +#: internal/app/application_overrides.go:83 +msgid "Could not load application settings: {0}" +msgstr "No se pudo cargar la configuración de la aplicación: {0}" + +#: internal/app/application_overrides.go:329 +#: internal/app/application_overrides.go:333 msgid "Could not open Applications: {0}" msgstr "No se pudo abrir Aplicaciones: {0}" @@ -298,8 +303,8 @@ msgstr "No se pudo abrir Aplicaciones: {0}" msgid "Could not open Emoji Menu: {0}" msgstr "No se pudo abrir el menú de emojis: {0}" -#: internal/app/fenrir_menu.go:32 internal/app/fenrir_menu.go:52 -#: internal/app/fenrir_menu.go:67 internal/app/runtime_settings.go:133 +#: internal/app/fenrir_menu.go:40 internal/app/fenrir_menu.go:69 +#: internal/app/fenrir_menu.go:89 internal/app/runtime_settings.go:133 msgid "Could not open Fenrir Menu: {0}" msgstr "No se pudo abrir el menú de Fenrir: {0}" @@ -307,8 +312,8 @@ msgstr "No se pudo abrir el menú de Fenrir: {0}" msgid "Could not open Spellcheck Menu: {0}" msgstr "No se pudo abrir el menú de ortografía: {0}" -#: internal/app/application_overrides.go:104 -#: internal/app/application_overrides.go:208 +#: internal/app/application_overrides.go:161 +#: internal/app/application_overrides.go:296 msgid "Could not restore application settings: {0}" msgstr "No se pudo restaurar la configuración de la aplicación: {0}" @@ -316,8 +321,8 @@ msgstr "No se pudo restaurar la configuración de la aplicación: {0}" msgid "Could not save exceptions: {0}" msgstr "No se pudieron guardar las excepciones: {0}" -#: internal/app/fenrir_menu.go:174 internal/app/fenrir_menu.go:180 -#: internal/app/runtime_settings.go:172 internal/app/runtime_settings.go:199 +#: internal/app/fenrir_menu.go:200 internal/app/fenrir_menu.go:206 +#: internal/app/runtime_settings.go:176 internal/app/runtime_settings.go:204 msgid "Could not save settings: {0}" msgstr "No se pudo guardar la configuración: {0}" @@ -337,12 +342,12 @@ msgstr "Daga" msgid "Date format" msgstr "Formato de fecha" -#: internal/app/application_overrides.go:230 +#: internal/app/application_overrides.go:318 msgid "Default" msgstr "Predeterminado" -#: internal/app/application_overrides.go:129 -#: internal/app/application_overrides.go:182 +#: internal/app/application_overrides.go:186 +#: internal/app/application_overrides.go:242 msgid "Default settings restored" msgstr "Configuración predeterminada restaurada" @@ -446,7 +451,7 @@ msgstr "" msgid "Face with symbols" msgstr "Cara con símbolos" -#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:155 +#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:159 msgid "Failed to save settings." msgstr "No se pudo guardar la configuración." @@ -490,7 +495,7 @@ msgstr "Manos juntas" msgid "Food" msgstr "Comida" -#: internal/app/commands.go:145 +#: internal/app/commands.go:143 msgid "Forward next keypress" msgstr "Enviar la siguiente tecla" @@ -734,7 +739,7 @@ msgstr "Náuseas" msgid "Netherlands" msgstr "Países Bajos" -#: internal/app/application_overrides.go:230 +#: internal/app/application_overrides.go:318 msgid "NetworkManager" msgstr "NetworkManager" @@ -742,7 +747,7 @@ msgstr "NetworkManager" msgid "New channel" msgstr "Nuevo canal" -#: internal/app/application_overrides.go:230 +#: internal/app/application_overrides.go:318 msgid "Ninjam" msgstr "Ninjam" @@ -796,7 +801,7 @@ msgstr "Fuera del modo de revisión" msgid "Not in window Mode" msgstr "No está en modo ventana" -#: internal/preferences/settings.go:79 +#: internal/preferences/settings.go:93 msgid "OK" msgstr "Aceptar" @@ -857,11 +862,11 @@ msgid "" "Presents the currently selected text that will be copied to the clipboard" msgstr "Presenta el texto seleccionado que se copiará al portapapeles" -#: internal/app/commands.go:131 +#: internal/app/commands.go:129 msgid "Progress monitoring disabled" msgstr "Seguimiento del progreso desactivado" -#: internal/app/commands.go:129 +#: internal/app/commands.go:127 msgid "Progress monitoring enabled" msgstr "Seguimiento del progreso activado" @@ -1013,7 +1018,7 @@ msgstr "Triste" msgid "Santa" msgstr "Papá Noel" -#: internal/app/runtime_settings.go:151 internal/app/runtime_settings.go:168 +#: internal/app/runtime_settings.go:155 internal/app/runtime_settings.go:172 msgid "Save unavailable." msgstr "Guardado no disponible." @@ -1037,7 +1042,7 @@ msgstr "Decir «mal escrita»" msgid "Screaming" msgstr "Gritando" -#: internal/app/commands.go:154 internal/app/scripts.go +#: internal/app/commands.go:152 internal/app/scripts.go msgid "Script failed" msgstr "El script falló" @@ -1085,8 +1090,8 @@ msgstr "Definir una región dinámica a partir de una o dos marcas" msgid "Set window begin and end marks" msgstr "Establece una marca de principio y otra para el final de la ventana" -#: internal/app/application_overrides.go:114 -#: internal/app/application_overrides.go:227 internal/app/commands.go:84 +#: internal/app/application_overrides.go:171 +#: internal/app/application_overrides.go:315 internal/app/commands.go:84 msgid "Settings are busy" msgstr "La configuración está ocupada" @@ -1234,11 +1239,11 @@ msgstr "Símbolos" msgid "Taco" msgstr "Taco" -#: internal/preferences/settings.go:72 +#: internal/preferences/settings.go:83 msgid "Test synthesizer" msgstr "Probar sintetizador" -#: internal/app/fenrir_menu.go:139 +#: internal/app/fenrir_menu.go:165 msgid "Testing voice" msgstr "Probando la voz" @@ -2193,7 +2198,7 @@ msgstr "tono de voz" msgid "speech rate" msgstr "velocidad de voz" -#: internal/app/commands.go:140 +#: internal/app/commands.go:138 msgid "speech temporary disabled" msgstr "Habla desactivada temporalmente" @@ -2288,10 +2293,18 @@ msgstr "guiño" msgid "{0} added to dictionary" msgstr "{0} añadido al diccionario" -#: internal/app/application_overrides.go:147 +#: internal/app/application_overrides.go:207 msgid "{0} application override selected" msgstr "Ajustes de aplicación seleccionados para {0}" +#: internal/app/fenrir_menu.go:34 +msgid "{0} application settings" +msgstr "Configuración de la aplicación {0}" + +#: internal/app/fenrir_menu.go:34 internal/app/remote_persistence.go:131 +msgid "{0} application settings saved." +msgstr "Configuración de la aplicación {0} guardada." + #: internal/interaction/presentation.go:53 msgid "{0} button" msgstr "{0} botón" diff --git a/internal/i18n/locale/fenrir.pot b/internal/i18n/locale/fenrir.pot index d9f4005c..1f48a814 100644 --- a/internal/i18n/locale/fenrir.pot +++ b/internal/i18n/locale/fenrir.pot @@ -57,11 +57,11 @@ msgstr "" msgid "Apple" msgstr "" -#: internal/app/application_overrides.go:237 +#: internal/app/application_overrides.go:325 msgid "Application" msgstr "" -#: internal/app/application_overrides.go:236 +#: internal/app/application_overrides.go:324 msgid "Applications" msgstr "" @@ -153,7 +153,7 @@ msgstr "" msgid "Canada" msgstr "" -#: internal/app/application_overrides.go:238 internal/preferences/settings.go:79 +#: internal/app/application_overrides.go:326 internal/preferences/settings.go:93 msgid "Cancel" msgstr "" @@ -245,7 +245,7 @@ msgstr "" msgid "Clipboard" msgstr "" -#: internal/app/application.go:142 internal/app/application.go:145 internal/app/clipboard.go:218 internal/app/clipboard.go:374 +#: internal/app/application.go:144 internal/app/application.go:147 internal/app/clipboard.go:218 internal/app/clipboard.go:374 msgid "Clipboard import failed" msgstr "" @@ -277,7 +277,11 @@ msgstr "" msgid "Could not check spelling: {0}" msgstr "" -#: internal/app/application_overrides.go:241 internal/app/application_overrides.go:245 +#: internal/app/application_overrides.go:278 internal/app/application_overrides.go:83 +msgid "Could not load application settings: {0}" +msgstr "" + +#: internal/app/application_overrides.go:329 internal/app/application_overrides.go:333 msgid "Could not open Applications: {0}" msgstr "" @@ -285,7 +289,7 @@ msgstr "" msgid "Could not open Emoji Menu: {0}" msgstr "" -#: internal/app/fenrir_menu.go:32 internal/app/fenrir_menu.go:52 internal/app/fenrir_menu.go:67 internal/app/runtime_settings.go:133 +#: internal/app/fenrir_menu.go:40 internal/app/fenrir_menu.go:69 internal/app/fenrir_menu.go:89 internal/app/runtime_settings.go:133 msgid "Could not open Fenrir Menu: {0}" msgstr "" @@ -293,7 +297,7 @@ msgstr "" msgid "Could not open Spellcheck Menu: {0}" msgstr "" -#: internal/app/application_overrides.go:104 internal/app/application_overrides.go:208 +#: internal/app/application_overrides.go:161 internal/app/application_overrides.go:296 msgid "Could not restore application settings: {0}" msgstr "" @@ -301,7 +305,7 @@ msgstr "" msgid "Could not save exceptions: {0}" msgstr "" -#: internal/app/fenrir_menu.go:174 internal/app/fenrir_menu.go:180 internal/app/runtime_settings.go:172 internal/app/runtime_settings.go:199 +#: internal/app/fenrir_menu.go:200 internal/app/fenrir_menu.go:206 internal/app/runtime_settings.go:176 internal/app/runtime_settings.go:204 msgid "Could not save settings: {0}" msgstr "" @@ -321,11 +325,11 @@ msgstr "" msgid "Date format" msgstr "" -#: internal/app/application_overrides.go:230 +#: internal/app/application_overrides.go:318 msgid "Default" msgstr "" -#: internal/app/application_overrides.go:129 internal/app/application_overrides.go:182 +#: internal/app/application_overrides.go:186 internal/app/application_overrides.go:242 msgid "Default settings restored" msgstr "" @@ -417,7 +421,7 @@ msgstr "" msgid "Face with symbols" msgstr "" -#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:155 +#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:159 msgid "Failed to save settings." msgstr "" @@ -461,7 +465,7 @@ msgstr "" msgid "Food" msgstr "" -#: internal/app/commands.go:145 +#: internal/app/commands.go:143 msgid "Forward next keypress" msgstr "" @@ -697,7 +701,7 @@ msgstr "" msgid "Netherlands" msgstr "" -#: internal/app/application_overrides.go:230 +#: internal/app/application_overrides.go:318 msgid "NetworkManager" msgstr "" @@ -705,7 +709,7 @@ msgstr "" msgid "New channel" msgstr "" -#: internal/app/application_overrides.go:230 +#: internal/app/application_overrides.go:318 msgid "Ninjam" msgstr "" @@ -753,7 +757,7 @@ msgstr "" msgid "Not in window Mode" msgstr "" -#: internal/preferences/settings.go:79 +#: internal/preferences/settings.go:93 msgid "OK" msgstr "" @@ -813,11 +817,11 @@ msgstr "" msgid "Presents the currently selected text that will be copied to the clipboard" msgstr "" -#: internal/app/commands.go:131 +#: internal/app/commands.go:129 msgid "Progress monitoring disabled" msgstr "" -#: internal/app/commands.go:129 +#: internal/app/commands.go:127 msgid "Progress monitoring enabled" msgstr "" @@ -969,7 +973,7 @@ msgstr "" msgid "Santa" msgstr "" -#: internal/app/runtime_settings.go:151 internal/app/runtime_settings.go:168 +#: internal/app/runtime_settings.go:155 internal/app/runtime_settings.go:172 msgid "Save unavailable." msgstr "" @@ -993,7 +997,7 @@ msgstr "" msgid "Screaming" msgstr "" -#: internal/app/commands.go:154 internal/app/scripts.go +#: internal/app/commands.go:152 internal/app/scripts.go msgid "Script failed" msgstr "" @@ -1041,7 +1045,7 @@ msgstr "" msgid "Set window begin and end marks" msgstr "" -#: internal/app/application_overrides.go:114 internal/app/application_overrides.go:227 internal/app/commands.go:84 +#: internal/app/application_overrides.go:171 internal/app/application_overrides.go:315 internal/app/commands.go:84 msgid "Settings are busy" msgstr "" @@ -1189,11 +1193,11 @@ msgstr "" msgid "Taco" msgstr "" -#: internal/preferences/settings.go:72 +#: internal/preferences/settings.go:83 msgid "Test synthesizer" msgstr "" -#: internal/app/fenrir_menu.go:139 +#: internal/app/fenrir_menu.go:165 msgid "Testing voice" msgstr "" @@ -2085,7 +2089,7 @@ msgstr "" msgid "speech rate" msgstr "" -#: internal/app/commands.go:140 +#: internal/app/commands.go:138 msgid "speech temporary disabled" msgstr "" @@ -2177,10 +2181,18 @@ msgstr "" msgid "{0} added to dictionary" msgstr "" -#: internal/app/application_overrides.go:147 +#: internal/app/application_overrides.go:207 msgid "{0} application override selected" msgstr "" +#: internal/app/fenrir_menu.go:34 +msgid "{0} application settings" +msgstr "" + +#: internal/app/fenrir_menu.go:34 internal/app/remote_persistence.go:131 +msgid "{0} application settings saved." +msgstr "" + #: internal/interaction/presentation.go:53 msgid "{0} button" msgstr "" diff --git a/internal/i18n/locale/pl/LC_MESSAGES/fenrir.po b/internal/i18n/locale/pl/LC_MESSAGES/fenrir.po index ab41bfcb..f6607da1 100644 --- a/internal/i18n/locale/pl/LC_MESSAGES/fenrir.po +++ b/internal/i18n/locale/pl/LC_MESSAGES/fenrir.po @@ -68,11 +68,11 @@ msgstr "Złość" msgid "Apple" msgstr "Jabłko" -#: internal/app/application_overrides.go:237 +#: internal/app/application_overrides.go:325 msgid "Application" msgstr "Aplikacja" -#: internal/app/application_overrides.go:236 +#: internal/app/application_overrides.go:324 msgid "Applications" msgstr "Aplikacje" @@ -164,8 +164,8 @@ msgstr "Motyl" msgid "Canada" msgstr "Kanada" -#: internal/app/application_overrides.go:238 -#: internal/preferences/settings.go:79 +#: internal/app/application_overrides.go:326 +#: internal/preferences/settings.go:93 msgid "Cancel" msgstr "Anuluj" @@ -257,7 +257,7 @@ msgstr "Wyczyść obszar dynamiczny 9" msgid "Clipboard" msgstr "Schowek" -#: internal/app/application.go:142 internal/app/application.go:145 +#: internal/app/application.go:144 internal/app/application.go:147 #: internal/app/clipboard.go:218 internal/app/clipboard.go:374 msgid "Clipboard import failed" msgstr "Importowanie schowka nie powiodło się" @@ -290,8 +290,13 @@ msgstr "Wyluzowanie" msgid "Could not check spelling: {0}" msgstr "Nie można sprawdzić pisowni: {0}" -#: internal/app/application_overrides.go:241 -#: internal/app/application_overrides.go:245 +#: internal/app/application_overrides.go:278 +#: internal/app/application_overrides.go:83 +msgid "Could not load application settings: {0}" +msgstr "Nie można wczytać ustawień aplikacji: {0}" + +#: internal/app/application_overrides.go:329 +#: internal/app/application_overrides.go:333 msgid "Could not open Applications: {0}" msgstr "Nie można otworzyć menu aplikacji: {0}" @@ -299,8 +304,8 @@ msgstr "Nie można otworzyć menu aplikacji: {0}" msgid "Could not open Emoji Menu: {0}" msgstr "Nie można otworzyć menu emoji: {0}" -#: internal/app/fenrir_menu.go:32 internal/app/fenrir_menu.go:52 -#: internal/app/fenrir_menu.go:67 internal/app/runtime_settings.go:133 +#: internal/app/fenrir_menu.go:40 internal/app/fenrir_menu.go:69 +#: internal/app/fenrir_menu.go:89 internal/app/runtime_settings.go:133 msgid "Could not open Fenrir Menu: {0}" msgstr "Nie można otworzyć menu Fenrira: {0}" @@ -308,8 +313,8 @@ msgstr "Nie można otworzyć menu Fenrira: {0}" msgid "Could not open Spellcheck Menu: {0}" msgstr "Nie można otworzyć menu sprawdzania pisowni: {0}" -#: internal/app/application_overrides.go:104 -#: internal/app/application_overrides.go:208 +#: internal/app/application_overrides.go:161 +#: internal/app/application_overrides.go:296 msgid "Could not restore application settings: {0}" msgstr "Nie można przywrócić ustawień aplikacji: {0}" @@ -317,8 +322,8 @@ msgstr "Nie można przywrócić ustawień aplikacji: {0}" msgid "Could not save exceptions: {0}" msgstr "Nie można zapisać wyjątków: {0}" -#: internal/app/fenrir_menu.go:174 internal/app/fenrir_menu.go:180 -#: internal/app/runtime_settings.go:172 internal/app/runtime_settings.go:199 +#: internal/app/fenrir_menu.go:200 internal/app/fenrir_menu.go:206 +#: internal/app/runtime_settings.go:176 internal/app/runtime_settings.go:204 msgid "Could not save settings: {0}" msgstr "Nie można zapisać ustawień: {0}" @@ -338,12 +343,12 @@ msgstr "Sztylet" msgid "Date format" msgstr "Format daty" -#: internal/app/application_overrides.go:230 +#: internal/app/application_overrides.go:318 msgid "Default" msgstr "Domyślne" -#: internal/app/application_overrides.go:129 -#: internal/app/application_overrides.go:182 +#: internal/app/application_overrides.go:186 +#: internal/app/application_overrides.go:242 msgid "Default settings restored" msgstr "Przywrócono ustawienia domyślne" @@ -446,7 +451,7 @@ msgstr "" msgid "Face with symbols" msgstr "Twarz z symbolami" -#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:155 +#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:159 msgid "Failed to save settings." msgstr "Nie udało się zapisać ustawień." @@ -490,7 +495,7 @@ msgstr "Złożone dłonie" msgid "Food" msgstr "Jedzenie" -#: internal/app/commands.go:145 +#: internal/app/commands.go:143 msgid "Forward next keypress" msgstr "Przepuść następny klawisz" @@ -730,7 +735,7 @@ msgstr "Nudności" msgid "Netherlands" msgstr "Holandia" -#: internal/app/application_overrides.go:230 +#: internal/app/application_overrides.go:318 msgid "NetworkManager" msgstr "NetworkManager" @@ -738,7 +743,7 @@ msgstr "NetworkManager" msgid "New channel" msgstr "Nowy kanał" -#: internal/app/application_overrides.go:230 +#: internal/app/application_overrides.go:318 msgid "Ninjam" msgstr "Ninjam" @@ -791,7 +796,7 @@ msgstr "Poza trybem przeglądu" msgid "Not in window Mode" msgstr "Nie znajdujesz się w trybie okna." -#: internal/preferences/settings.go:79 +#: internal/preferences/settings.go:93 msgid "OK" msgstr "OK" @@ -852,11 +857,11 @@ msgid "" "Presents the currently selected text that will be copied to the clipboard" msgstr "Odczytuje zaznaczony tekst, który zostanie skopiowany do schowka" -#: internal/app/commands.go:131 +#: internal/app/commands.go:129 msgid "Progress monitoring disabled" msgstr "Monitorowanie postępu wyłączone" -#: internal/app/commands.go:129 +#: internal/app/commands.go:127 msgid "Progress monitoring enabled" msgstr "Monitorowanie postępu włączone" @@ -1008,7 +1013,7 @@ msgstr "Smutek" msgid "Santa" msgstr "Święty Mikołaj" -#: internal/app/runtime_settings.go:151 internal/app/runtime_settings.go:168 +#: internal/app/runtime_settings.go:155 internal/app/runtime_settings.go:172 msgid "Save unavailable." msgstr "Zapisywanie niedostępne." @@ -1032,7 +1037,7 @@ msgstr "Mów „błędna pisownia”" msgid "Screaming" msgstr "Krzyk" -#: internal/app/commands.go:154 internal/app/scripts.go +#: internal/app/commands.go:152 internal/app/scripts.go msgid "Script failed" msgstr "Skrypt nie powiódł się" @@ -1080,8 +1085,8 @@ msgstr "Ustaw obszar dynamiczny za pomocą jednego lub dwóch znaczników" msgid "Set window begin and end marks" msgstr "Ustaw początek i koniec okna." -#: internal/app/application_overrides.go:114 -#: internal/app/application_overrides.go:227 internal/app/commands.go:84 +#: internal/app/application_overrides.go:171 +#: internal/app/application_overrides.go:315 internal/app/commands.go:84 msgid "Settings are busy" msgstr "Ustawienia są zajęte" @@ -1229,11 +1234,11 @@ msgstr "Symbole" msgid "Taco" msgstr "Taco" -#: internal/preferences/settings.go:72 +#: internal/preferences/settings.go:83 msgid "Test synthesizer" msgstr "Testuj syntezator" -#: internal/app/fenrir_menu.go:139 +#: internal/app/fenrir_menu.go:165 msgid "Testing voice" msgstr "Testowanie głosu" @@ -2185,7 +2190,7 @@ msgstr "wysokość głosu" msgid "speech rate" msgstr "szybkość mowy" -#: internal/app/commands.go:140 +#: internal/app/commands.go:138 msgid "speech temporary disabled" msgstr "Mowa tymczasowo wyłączona." @@ -2280,10 +2285,18 @@ msgstr "mrugnięcie" msgid "{0} added to dictionary" msgstr "Dodano {0} do słownika" -#: internal/app/application_overrides.go:147 +#: internal/app/application_overrides.go:207 msgid "{0} application override selected" msgstr "Wybrano ustawienia aplikacji {0}" +#: internal/app/fenrir_menu.go:34 +msgid "{0} application settings" +msgstr "Ustawienia aplikacji {0}" + +#: internal/app/fenrir_menu.go:34 internal/app/remote_persistence.go:131 +msgid "{0} application settings saved." +msgstr "Ustawienia aplikacji {0} zapisane." + #: internal/interaction/presentation.go:53 msgid "{0} button" msgstr "{0} przycisk" diff --git a/internal/i18n/locale/pt/LC_MESSAGES/fenrir.po b/internal/i18n/locale/pt/LC_MESSAGES/fenrir.po index 7c0cce32..011ec10e 100644 --- a/internal/i18n/locale/pt/LC_MESSAGES/fenrir.po +++ b/internal/i18n/locale/pt/LC_MESSAGES/fenrir.po @@ -66,11 +66,11 @@ msgstr "Zangado" msgid "Apple" msgstr "Maçã" -#: internal/app/application_overrides.go:237 +#: internal/app/application_overrides.go:325 msgid "Application" msgstr "Aplicativo" -#: internal/app/application_overrides.go:236 +#: internal/app/application_overrides.go:324 msgid "Applications" msgstr "Aplicativos" @@ -162,8 +162,8 @@ msgstr "Borboleta" msgid "Canada" msgstr "Canadá" -#: internal/app/application_overrides.go:238 -#: internal/preferences/settings.go:79 +#: internal/app/application_overrides.go:326 +#: internal/preferences/settings.go:93 msgid "Cancel" msgstr "Cancelar" @@ -255,7 +255,7 @@ msgstr "Limpar região dinâmica 9" msgid "Clipboard" msgstr "Área de transferência" -#: internal/app/application.go:142 internal/app/application.go:145 +#: internal/app/application.go:144 internal/app/application.go:147 #: internal/app/clipboard.go:218 internal/app/clipboard.go:374 msgid "Clipboard import failed" msgstr "Falha ao importar a área de transferência" @@ -288,8 +288,13 @@ msgstr "Legal" msgid "Could not check spelling: {0}" msgstr "Não foi possível verificar a ortografia: {0}" -#: internal/app/application_overrides.go:241 -#: internal/app/application_overrides.go:245 +#: internal/app/application_overrides.go:278 +#: internal/app/application_overrides.go:83 +msgid "Could not load application settings: {0}" +msgstr "Não foi possível carregar as configurações do aplicativo: {0}" + +#: internal/app/application_overrides.go:329 +#: internal/app/application_overrides.go:333 msgid "Could not open Applications: {0}" msgstr "Não foi possível abrir Aplicativos: {0}" @@ -297,8 +302,8 @@ msgstr "Não foi possível abrir Aplicativos: {0}" msgid "Could not open Emoji Menu: {0}" msgstr "Não foi possível abrir o menu de emojis: {0}" -#: internal/app/fenrir_menu.go:32 internal/app/fenrir_menu.go:52 -#: internal/app/fenrir_menu.go:67 internal/app/runtime_settings.go:133 +#: internal/app/fenrir_menu.go:40 internal/app/fenrir_menu.go:69 +#: internal/app/fenrir_menu.go:89 internal/app/runtime_settings.go:133 msgid "Could not open Fenrir Menu: {0}" msgstr "Não foi possível abrir o menu do Fenrir: {0}" @@ -306,8 +311,8 @@ msgstr "Não foi possível abrir o menu do Fenrir: {0}" msgid "Could not open Spellcheck Menu: {0}" msgstr "Não foi possível abrir o menu de ortografia: {0}" -#: internal/app/application_overrides.go:104 -#: internal/app/application_overrides.go:208 +#: internal/app/application_overrides.go:161 +#: internal/app/application_overrides.go:296 msgid "Could not restore application settings: {0}" msgstr "Não foi possível restaurar as configurações do aplicativo: {0}" @@ -315,8 +320,8 @@ msgstr "Não foi possível restaurar as configurações do aplicativo: {0}" msgid "Could not save exceptions: {0}" msgstr "Não foi possível salvar as exceções: {0}" -#: internal/app/fenrir_menu.go:174 internal/app/fenrir_menu.go:180 -#: internal/app/runtime_settings.go:172 internal/app/runtime_settings.go:199 +#: internal/app/fenrir_menu.go:200 internal/app/fenrir_menu.go:206 +#: internal/app/runtime_settings.go:176 internal/app/runtime_settings.go:204 msgid "Could not save settings: {0}" msgstr "Não foi possível salvar as configurações: {0}" @@ -336,12 +341,12 @@ msgstr "Adaga" msgid "Date format" msgstr "Formato da data" -#: internal/app/application_overrides.go:230 +#: internal/app/application_overrides.go:318 msgid "Default" msgstr "Padrão" -#: internal/app/application_overrides.go:129 -#: internal/app/application_overrides.go:182 +#: internal/app/application_overrides.go:186 +#: internal/app/application_overrides.go:242 msgid "Default settings restored" msgstr "Configurações padrão restauradas" @@ -446,7 +451,7 @@ msgstr "" msgid "Face with symbols" msgstr "Rosto com símbolos" -#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:155 +#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:159 msgid "Failed to save settings." msgstr "Falha ao salvar as configurações." @@ -490,7 +495,7 @@ msgstr "Mãos juntas" msgid "Food" msgstr "Comida" -#: internal/app/commands.go:145 +#: internal/app/commands.go:143 msgid "Forward next keypress" msgstr "Avançar próxima tecla pressionada" @@ -732,7 +737,7 @@ msgstr "Enjoado" msgid "Netherlands" msgstr "Países Baixos" -#: internal/app/application_overrides.go:230 +#: internal/app/application_overrides.go:318 msgid "NetworkManager" msgstr "NetworkManager" @@ -740,7 +745,7 @@ msgstr "NetworkManager" msgid "New channel" msgstr "Novo canal" -#: internal/app/application_overrides.go:230 +#: internal/app/application_overrides.go:318 msgid "Ninjam" msgstr "Ninjam" @@ -794,7 +799,7 @@ msgstr "Fora do modo de revisão" msgid "Not in window Mode" msgstr "Não no Modo de Janela" -#: internal/preferences/settings.go:79 +#: internal/preferences/settings.go:93 msgid "OK" msgstr "OK" @@ -856,11 +861,11 @@ msgid "" msgstr "" "Apresenta o texto selecionado que será copiado para a área de transferência" -#: internal/app/commands.go:131 +#: internal/app/commands.go:129 msgid "Progress monitoring disabled" msgstr "Monitoramento de progresso desativado" -#: internal/app/commands.go:129 +#: internal/app/commands.go:127 msgid "Progress monitoring enabled" msgstr "Monitoramento de progresso ativado" @@ -1012,7 +1017,7 @@ msgstr "Triste" msgid "Santa" msgstr "Papai Noel" -#: internal/app/runtime_settings.go:151 internal/app/runtime_settings.go:168 +#: internal/app/runtime_settings.go:155 internal/app/runtime_settings.go:172 msgid "Save unavailable." msgstr "Salvamento indisponível." @@ -1036,7 +1041,7 @@ msgstr "Dizer “erro ortográfico”" msgid "Screaming" msgstr "Gritando" -#: internal/app/commands.go:154 internal/app/scripts.go +#: internal/app/commands.go:152 internal/app/scripts.go msgid "Script failed" msgstr "O script falhou" @@ -1084,8 +1089,8 @@ msgstr "Definir uma região dinâmica a partir de uma ou duas marcas" msgid "Set window begin and end marks" msgstr "Definir marcas de início e fim da janela" -#: internal/app/application_overrides.go:114 -#: internal/app/application_overrides.go:227 internal/app/commands.go:84 +#: internal/app/application_overrides.go:171 +#: internal/app/application_overrides.go:315 internal/app/commands.go:84 msgid "Settings are busy" msgstr "As configurações estão ocupadas" @@ -1233,11 +1238,11 @@ msgstr "Símbolos" msgid "Taco" msgstr "Taco" -#: internal/preferences/settings.go:72 +#: internal/preferences/settings.go:83 msgid "Test synthesizer" msgstr "Testar sintetizador" -#: internal/app/fenrir_menu.go:139 +#: internal/app/fenrir_menu.go:165 msgid "Testing voice" msgstr "Testando a voz" @@ -2198,7 +2203,7 @@ msgstr "tom da fala" msgid "speech rate" msgstr "velocidade da fala" -#: internal/app/commands.go:140 +#: internal/app/commands.go:138 msgid "speech temporary disabled" msgstr "fala temporariamente desativada" @@ -2293,10 +2298,18 @@ msgstr "piscadela" msgid "{0} added to dictionary" msgstr "{0} adicionado ao dicionário" -#: internal/app/application_overrides.go:147 +#: internal/app/application_overrides.go:207 msgid "{0} application override selected" msgstr "Configurações do aplicativo {0} selecionadas" +#: internal/app/fenrir_menu.go:34 +msgid "{0} application settings" +msgstr "Configurações do aplicativo {0}" + +#: internal/app/fenrir_menu.go:34 internal/app/remote_persistence.go:131 +msgid "{0} application settings saved." +msgstr "Configurações do aplicativo {0} salvas." + #: internal/interaction/presentation.go:53 msgid "{0} button" msgstr "{0} botão" diff --git a/internal/i18n/locale/ru/LC_MESSAGES/fenrir.po b/internal/i18n/locale/ru/LC_MESSAGES/fenrir.po index a0d7914f..6c6dde2c 100644 --- a/internal/i18n/locale/ru/LC_MESSAGES/fenrir.po +++ b/internal/i18n/locale/ru/LC_MESSAGES/fenrir.po @@ -65,11 +65,11 @@ msgstr "Злость" msgid "Apple" msgstr "Яблоко" -#: internal/app/application_overrides.go:237 +#: internal/app/application_overrides.go:325 msgid "Application" msgstr "Приложение" -#: internal/app/application_overrides.go:236 +#: internal/app/application_overrides.go:324 msgid "Applications" msgstr "Приложения" @@ -161,8 +161,8 @@ msgstr "Бабочка" msgid "Canada" msgstr "Канада" -#: internal/app/application_overrides.go:238 -#: internal/preferences/settings.go:79 +#: internal/app/application_overrides.go:326 +#: internal/preferences/settings.go:93 msgid "Cancel" msgstr "Отмена" @@ -254,7 +254,7 @@ msgstr "Очистить динамическую область 9" msgid "Clipboard" msgstr "Буфер обмена" -#: internal/app/application.go:142 internal/app/application.go:145 +#: internal/app/application.go:144 internal/app/application.go:147 #: internal/app/clipboard.go:218 internal/app/clipboard.go:374 msgid "Clipboard import failed" msgstr "Не удалось импортировать буфер обмена" @@ -287,8 +287,13 @@ msgstr "Круто" msgid "Could not check spelling: {0}" msgstr "Не удалось проверить правописание: {0}" -#: internal/app/application_overrides.go:241 -#: internal/app/application_overrides.go:245 +#: internal/app/application_overrides.go:278 +#: internal/app/application_overrides.go:83 +msgid "Could not load application settings: {0}" +msgstr "Не удалось загрузить настройки приложения: {0}" + +#: internal/app/application_overrides.go:329 +#: internal/app/application_overrides.go:333 msgid "Could not open Applications: {0}" msgstr "Не удалось открыть меню приложений: {0}" @@ -296,8 +301,8 @@ msgstr "Не удалось открыть меню приложений: {0}" msgid "Could not open Emoji Menu: {0}" msgstr "Не удалось открыть меню эмодзи: {0}" -#: internal/app/fenrir_menu.go:32 internal/app/fenrir_menu.go:52 -#: internal/app/fenrir_menu.go:67 internal/app/runtime_settings.go:133 +#: internal/app/fenrir_menu.go:40 internal/app/fenrir_menu.go:69 +#: internal/app/fenrir_menu.go:89 internal/app/runtime_settings.go:133 msgid "Could not open Fenrir Menu: {0}" msgstr "Не удалось открыть меню Fenrir: {0}" @@ -305,8 +310,8 @@ msgstr "Не удалось открыть меню Fenrir: {0}" msgid "Could not open Spellcheck Menu: {0}" msgstr "Не удалось открыть меню проверки правописания: {0}" -#: internal/app/application_overrides.go:104 -#: internal/app/application_overrides.go:208 +#: internal/app/application_overrides.go:161 +#: internal/app/application_overrides.go:296 msgid "Could not restore application settings: {0}" msgstr "Не удалось восстановить настройки приложения: {0}" @@ -314,8 +319,8 @@ msgstr "Не удалось восстановить настройки прил msgid "Could not save exceptions: {0}" msgstr "Не удалось сохранить исключения: {0}" -#: internal/app/fenrir_menu.go:174 internal/app/fenrir_menu.go:180 -#: internal/app/runtime_settings.go:172 internal/app/runtime_settings.go:199 +#: internal/app/fenrir_menu.go:200 internal/app/fenrir_menu.go:206 +#: internal/app/runtime_settings.go:176 internal/app/runtime_settings.go:204 msgid "Could not save settings: {0}" msgstr "Не удалось сохранить настройки: {0}" @@ -335,12 +340,12 @@ msgstr "Кинжал" msgid "Date format" msgstr "Формат даты" -#: internal/app/application_overrides.go:230 +#: internal/app/application_overrides.go:318 msgid "Default" msgstr "По умолчанию" -#: internal/app/application_overrides.go:129 -#: internal/app/application_overrides.go:182 +#: internal/app/application_overrides.go:186 +#: internal/app/application_overrides.go:242 msgid "Default settings restored" msgstr "Настройки по умолчанию восстановлены" @@ -442,7 +447,7 @@ msgstr "Экспорт текущего буфера обмена fenrir в бу msgid "Face with symbols" msgstr "Лицо с символами" -#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:155 +#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:159 msgid "Failed to save settings." msgstr "Не удалось сохранить настройки." @@ -486,7 +491,7 @@ msgstr "Сложенные ладони" msgid "Food" msgstr "Еда" -#: internal/app/commands.go:145 +#: internal/app/commands.go:143 msgid "Forward next keypress" msgstr "Переслать следующую клавишу." @@ -725,7 +730,7 @@ msgstr "Тошнота" msgid "Netherlands" msgstr "Нидерланды" -#: internal/app/application_overrides.go:230 +#: internal/app/application_overrides.go:318 msgid "NetworkManager" msgstr "NetworkManager" @@ -733,7 +738,7 @@ msgstr "NetworkManager" msgid "New channel" msgstr "Новый канал" -#: internal/app/application_overrides.go:230 +#: internal/app/application_overrides.go:318 msgid "Ninjam" msgstr "Ninjam" @@ -786,7 +791,7 @@ msgstr "Нет в режиме просмотра." msgid "Not in window Mode" msgstr "Не в режиме окна." -#: internal/preferences/settings.go:79 +#: internal/preferences/settings.go:93 msgid "OK" msgstr "ОК" @@ -848,11 +853,11 @@ msgid "" msgstr "" "Сообщает текущий выбранный текст, который будет скопирован в буфер обмена." -#: internal/app/commands.go:131 +#: internal/app/commands.go:129 msgid "Progress monitoring disabled" msgstr "Отслеживание хода выполнения отключено" -#: internal/app/commands.go:129 +#: internal/app/commands.go:127 msgid "Progress monitoring enabled" msgstr "Отслеживание хода выполнения включено" @@ -1004,7 +1009,7 @@ msgstr "Грусть" msgid "Santa" msgstr "Дед Мороз" -#: internal/app/runtime_settings.go:151 internal/app/runtime_settings.go:168 +#: internal/app/runtime_settings.go:155 internal/app/runtime_settings.go:172 msgid "Save unavailable." msgstr "Сохранение недоступно." @@ -1028,7 +1033,7 @@ msgstr "Говорить «ошибка»" msgid "Screaming" msgstr "Крик" -#: internal/app/commands.go:154 internal/app/scripts.go +#: internal/app/commands.go:152 internal/app/scripts.go msgid "Script failed" msgstr "Ошибка сценария" @@ -1076,8 +1081,8 @@ msgstr "Задать динамическую область по одной и msgid "Set window begin and end marks" msgstr "Установка начала и конца окна" -#: internal/app/application_overrides.go:114 -#: internal/app/application_overrides.go:227 internal/app/commands.go:84 +#: internal/app/application_overrides.go:171 +#: internal/app/application_overrides.go:315 internal/app/commands.go:84 msgid "Settings are busy" msgstr "Настройки заняты" @@ -1225,11 +1230,11 @@ msgstr "Символы" msgid "Taco" msgstr "Тако" -#: internal/preferences/settings.go:72 +#: internal/preferences/settings.go:83 msgid "Test synthesizer" msgstr "Проверить синтезатор" -#: internal/app/fenrir_menu.go:139 +#: internal/app/fenrir_menu.go:165 msgid "Testing voice" msgstr "Проверка голоса" @@ -2182,7 +2187,7 @@ msgstr "высота голоса" msgid "speech rate" msgstr "скорость речи" -#: internal/app/commands.go:140 +#: internal/app/commands.go:138 msgid "speech temporary disabled" msgstr "Речь временно отключена" @@ -2277,10 +2282,18 @@ msgstr "подмигивание" msgid "{0} added to dictionary" msgstr "{0} добавлено в словарь" -#: internal/app/application_overrides.go:147 +#: internal/app/application_overrides.go:207 msgid "{0} application override selected" msgstr "Выбраны настройки приложения {0}" +#: internal/app/fenrir_menu.go:34 +msgid "{0} application settings" +msgstr "Настройки приложения {0}" + +#: internal/app/fenrir_menu.go:34 internal/app/remote_persistence.go:131 +msgid "{0} application settings saved." +msgstr "Настройки приложения {0} сохранены." + #: internal/interaction/presentation.go:53 msgid "{0} button" msgstr "{0}, кнопка" diff --git a/internal/preferences/application_test.go b/internal/preferences/application_test.go new file mode 100644 index 00000000..b2564475 --- /dev/null +++ b/internal/preferences/application_test.go @@ -0,0 +1,34 @@ +// Copyright (c) 2026 Storm Dragon +// SPDX-License-Identifier: GPL-3.0-or-later + +package preferences + +import ( + "git.stormux.org/storm/fenrir/internal/config" + "git.stormux.org/storm/fenrir/internal/interaction" + "testing" +) + +func TestApplicationSettingsOnlyExposeRestorableControls(t *testing.T) { + base := config.Defaults() + session, err := New_application_session(base, nil, "Ninjam application settings") + if err != nil { + t.Fatal(err) + } + values := session.Values() + for _, path := range []string{"presentation.speech.driver", "presentation.sound.theme", "keyboard.layout", "keyboard.devices", "reading.punctuation_profile", "clipboard.persistent"} { + if _, exists := values[path]; exists { + t.Fatal("startup/global control exposed", path) + } + } + if _, exists := values["presentation.speech.voice"]; !exists { + t.Fatal("voice control missing") + } + session.Handle(interaction.Key{Name: "tab"}) + session.Handle(interaction.Key{Name: "tab"}) + session.Handle(interaction.Key{Name: "up"}) + saved, err := Application_settings(session, base) + if err != nil || saved.Presentation.Speech.Rate != base.Presentation.Speech.Rate+1 || saved.Keyboard.Layout != base.Keyboard.Layout { + t.Fatal(saved, err) + } +} diff --git a/internal/preferences/discovery.go b/internal/preferences/discovery.go index c5707f09..66607dc9 100644 --- a/internal/preferences/discovery.go +++ b/internal/preferences/discovery.go @@ -119,6 +119,9 @@ func (d Discovery) Refresh_speech(session *interaction.Session, previous *config "presentation.speech.voice": &next.Presentation.Speech.Voice, } { control, ok := values[path] + if !ok && path == "presentation.speech.driver" { + continue + } if !ok || control.Selected < 0 || control.Selected >= len(control.Choices) { return false } diff --git a/internal/preferences/settings.go b/internal/preferences/settings.go index 97b240be..ccdd4f4f 100644 --- a/internal/preferences/settings.go +++ b/internal/preferences/settings.go @@ -38,10 +38,18 @@ type Choices map[string][]config.Choice const MenuTitle = "Fenrir Menu" func New_session(settings config.Settings, order []string, discovered ...Choices) (*interaction.Session, error) { + return new_session(settings, order, MenuTitle, false, discovered...) +} + +func New_application_session(settings config.Settings, order []string, title string, discovered ...Choices) (*interaction.Session, error) { + return new_session(settings, order, title, true, discovered...) +} + +func new_session(settings config.Settings, order []string, title string, application bool, discovered ...Choices) (*interaction.Session, error) { if err := settings.Validate(); err != nil { return nil, err } - form := interaction.Form{Type: interaction.TabForm, Title: MenuTitle} + form := interaction.Form{Type: interaction.TabForm, Title: title} for _, section := range config.Ordered_sections(config.Sections(), order) { if section.Label == "" { continue @@ -50,6 +58,9 @@ func New_session(settings config.Settings, order []string, discovered ...Choices var append_section func(config.Section, string) error append_section = func(section config.Section, group string) error { for _, definition := range section.Settings { + if application && !config.Application_setting(definition.Path) { + continue + } control, err := make_control(definition, settings, discovered) if err != nil { return err @@ -76,6 +87,9 @@ func New_session(settings config.Settings, order []string, discovered ...Choices if err := append_section(section, ""); err != nil { return nil, err } + if len(tab.Controls) == 0 { + continue + } tab.Controls = append(tab.Controls, interaction.Control{ID: "ok", Label: "OK", Kind: interaction.Button, Action: "save"}, interaction.Control{ID: "cancel", Label: "Cancel", Kind: interaction.Button, Action: "cancel"}) form.Tabs = append(form.Tabs, tab) } @@ -137,15 +151,25 @@ func make_control(definition config.Definition, settings config.Settings, discov } func Settings(session *interaction.Session, base config.Settings) (config.Settings, error) { + return settings_values(session, base, false) +} + +func Application_settings(session *interaction.Session, base config.Settings) (config.Settings, error) { + return settings_values(session, base, true) +} + +func settings_values(session *interaction.Session, base config.Settings, application bool) (config.Settings, error) { values := session.Values() // Visibility follows the form's selected driver, including unsaved switches. - driver := values["presentation.speech.driver"] - if driver.Selected < 0 || driver.Selected >= len(driver.Choices) { + driver, hasDriver := values["presentation.speech.driver"] + if (!application && !hasDriver) || (hasDriver && (driver.Selected < 0 || driver.Selected >= len(driver.Choices))) { return config.Settings{}, fmt.Errorf("invalid speech driver selection") } - base.Presentation.Speech.Driver = driver.Choices[driver.Selected].ID + if hasDriver { + base.Presentation.Speech.Driver = driver.Choices[driver.Selected].ID + } for _, definition := range config.Definitions() { - if !definition.Available(base) { + if !definition.Available(base) || (application && !config.Application_setting(definition.Path)) { continue } control, ok := values[definition.Path] diff --git a/tests/native_application_overrides.py b/tests/native_application_overrides.py index a39e7b45..1555b6f0 100644 --- a/tests/native_application_overrides.py +++ b/tests/native_application_overrides.py @@ -121,9 +121,13 @@ def main(): action("Return", "Master unmuted") original = (root / "settings.toml").read_bytes() action("Insert+Up", "20") - action("Insert+ctrl+s", "Save unavailable") - assert remote("SETTING SAVE") == "Save unavailable." - assert remote("SETTING SAVEAS " + str(root / "other.toml")) == "Save unavailable." + action("Insert+ctrl+s", "Ninjam application settings saved") + profile = root / "applications" / "ninjam.toml" + assert tomllib.loads(profile.read_text())["presentation"]["speech"]["rate"] == 20 + assert remote("SETTING SAVE") == "" + assert remote("SETTING SAVEAS " + str(root / "other.toml")) == "SAVEAS is unavailable in application settings" + action("Insert+f", "Ninjam application settings") + action("Escape", "Ninjam application settings closed") assert not (root / "other.toml").exists() assert (root / "settings.toml").read_bytes() == original # Start detached review, then deliver actual XTest application @@ -143,7 +147,8 @@ def main(): action("Insert+a", "Application Default") action("Down", "NetworkManager") action("Down", "Ninjam") - action("Return", "Ninjam application override selected") + action("Return", "Master Volume and pan") + action("Insert+space", "20") action("q", "Default settings restored") action("Insert+ctrl+s", "Settings saved") saved = tomllib.loads((root / "settings.toml").read_text()) @@ -154,7 +159,7 @@ def main(): finished = json.loads((root / "finished").read_text()) assert finished == {"status": 0, "restored": True, "flagsRestored": True}, finished assert not server.errors, server.errors - print(f"PASS: {layout} application menu, cancellation, semantic navigation/chat, region reads, save guards and restoration") + print(f"PASS: {layout} application menu, cancellation, semantic navigation/chat, region reads, application saves/reloads and default restoration") except BaseException: print("SSIP events:", server.events[-25:], file=sys.stderr) for name in ("reader-errors", "timing.log"):