diff --git a/README.md b/README.md index 3bd3ce6f..833f246f 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ See the [user manual](docs/user.md) for detailed usage documentation. - Cursor, table, and highlight tracking. - Desktop and laptop keyboard layouts, with an interactive tutorial. - A keyboard-operated settings menu and quick settings shortcuts. +- Temporary built-in application overrides for Ninjam and NetworkManager's nmtui. - Speech Dispatcher and hardware speech synthesizer support. - Sound icons, progress tones, and indentation and text attribute feedback. - Multiple clipboards, optional persistent history, and X clipboard synchronization. @@ -111,6 +112,7 @@ Shortcuts shared by both layouts: - **Fenrir+H** or **Fenrir+F1**: enter the tutorial. Press command shortcuts to hear their descriptions, or browse with the arrow keys. Escape exits. - **Fenrir+S**: open the spellcheck menu. +- **Fenrir+A**: select an application override; choose **Default** to clear it. - **Fenrir+Control+E**: open the emoji picker. - **Fenrir+T**: announce the time; press twice quickly for the date. - **Fenrir+F4**: toggle mute. @@ -151,6 +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. +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**. + ### Configuration files Fenrir uses TOML. The annotated [settings.toml](config/settings/settings.toml) diff --git a/config/keyboard/desktop.toml b/config/keyboard/desktop.toml index ad682137..b57a05a8 100644 --- a/config/keyboard/desktop.toml +++ b/config/keyboard/desktop.toml @@ -478,3 +478,8 @@ command = "read_live_region_10" name = "Clear live region 10" keys = ["FENRIR", "SHIFT", "0"] command = "clear_live_region_10" + +[[bindings]] +name = "Applications" +keys = ["FENRIR", "A"] +command = "applications_menu" diff --git a/config/keyboard/laptop.toml b/config/keyboard/laptop.toml index aa55038a..6fb17a30 100644 --- a/config/keyboard/laptop.toml +++ b/config/keyboard/laptop.toml @@ -332,7 +332,7 @@ command = "review_screen_last_char" [[bindings]] name = "Attributes" -keys = ["FENRIR", "A"] +keys = ["FENRIR", "SHIFT", "A"] command = "attribute_cursor" [[bindings]] @@ -472,3 +472,8 @@ command = "read_live_region_10" name = "Clear live region 10" keys = ["FENRIR", "SHIFT", "0"] command = "clear_live_region_10" + +[[bindings]] +name = "Applications" +keys = ["FENRIR", "A"] +command = "applications_menu" diff --git a/docs/user.md b/docs/user.md index 1c749be6..c67381c2 100644 --- a/docs/user.md +++ b/docs/user.md @@ -8,6 +8,62 @@ consoles and can run in an X11 terminal as a regular user. For build requirements and setup, see the [README](../README.md). +## Application overrides + +Press **Fenrir+A** to open **Applications**. Up and Down select an item; Enter +activates it and closes the menu. Escape or the Cancel button closes the menu +without changing the selection. Tab and Shift+Tab move between the list, Cancel, +and the tab selector. The active override is initially selected. + +Start the application normally, then select its override. This menu does not +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. + +An override belongs to its terminal. Switching VTs or leaving the terminal +suspends its effects; returning resumes it when the application screen is +recognized. Unrecognized screens use ordinary reading. A fresh shell prompt on +the cursor row, with the application interface absent, clears the selection and +announces **Default settings restored**. This works from screen contents, so it +does not require access to remote processes over SSH or inside GNU Screen. +Unusual prompts may require selecting Default manually. Password prompts, +confirmation questions, and old shell prompts in scrollback do not clear it. +Split windows, heavily customized application colors, and localized or unfamiliar +layouts may prevent recognition; Default is always available. + +**NetworkManager** targets `nmtui` and temporarily selects highlight tracking. + +**Ninjam** targets the `cninjam` curses client. It announces selected controls +with master, metronome, or channel context; mute, transmit, and receive controls +are spoken as states. Volume and pan edits are announced without the continuously +changing peak meters and interval bar. Navigating the application's controls +after manual review resumes tracking; passive updates leave review in place. +Incoming chat is announced independently of control changes. The following +shortcuts temporarily replace numbered region reads: + +| Shortcut | Ninjam information | +| --- | --- | +| Fenrir+1 | Latest new chat message received since activation | +| Fenrir+2 | Selected channel or control | +| Fenrir+3 | Connection, tempo, and interval position | + +Fenrir+Shift+B lists these application assignments. Existing user regions are +preserved and restored when the override ends. Review, Read Screen, and copying +continue to expose the actual terminal text, including its literal `X` marks. +Chat history is baselined on activation, resize, buffer changes, and history +paging to avoid announcing old messages as new. Messages that appear and +disappear between screen captures cannot be recovered reliably. + +The laptop shortcut for reading attributes is **Fenrir+Shift+A**; Fenrir+A opens +Applications in both layouts. + ## Starting and stopping Run these commands from the checkout root after building `fenrir`: diff --git a/internal/app/application.go b/internal/app/application.go index 361b34a4..599abafd 100644 --- a/internal/app/application.go +++ b/internal/app/application.go @@ -28,6 +28,7 @@ import ( "git.stormux.org/storm/fenrir/internal/presentation" "git.stormux.org/storm/fenrir/internal/prompt" "git.stormux.org/storm/fenrir/internal/reader" + "git.stormux.org/storm/fenrir/internal/screen" "git.stormux.org/storm/fenrir/internal/speech" "git.stormux.org/storm/fenrir/internal/terminal" ) @@ -60,11 +61,16 @@ type application struct { scriptGeneration, spellSession, spellFeedback uint64 - lastTerminal int - lastActive bool - lastGeneration uint64 - lastRows, lastCols int - lastNumLock bool + lastTerminal int + lastActive bool + lastGeneration uint64 + lastRows, lastCols int + lastNumLock bool + lastSnapshot screen.Snapshot + overrides map[int]*applicationOverride + overrideCurrent *applicationOverride + overrideFailed *applicationOverride + overrideTransition, overrideRestoreFailed, overrideDirty bool } func new_application(ctx context.Context, source Source, keyboards Keyboards, voice Speaker, options RunOptions) (*application, error) { diff --git a/internal/app/application_overrides.go b/internal/app/application_overrides.go new file mode 100644 index 00000000..6e741c0d --- /dev/null +++ b/internal/app/application_overrides.go @@ -0,0 +1,262 @@ +// Copyright (c) 2026 Storm Dragon +// SPDX-License-Identifier: GPL-3.0-or-later + +package app + +import ( + "maps" + "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/i18n" + "git.stormux.org/storm/fenrir/internal/interaction" + "git.stormux.org/storm/fenrir/internal/prompt" + "git.stormux.org/storm/fenrir/internal/reader" + "git.stormux.org/storm/fenrir/internal/screen" +) + +type overrideSnapshot struct { + settings applicationSettings + quick quickSettings + runtime reader.SessionState + muted, silence bool +} + +type applicationOverride struct { + id string + terminal int + adapter applicationoverrides.Adapter + underlying, temporary overrideSnapshot + shell prompt.ShellDetector + initialized, recognized, exitPending bool +} + +func (a *application) saves_unavailable() bool { return len(a.overrides) != 0 || a.overrideTransition } + +func (a *application) capture_override(terminal int) overrideSnapshot { + settings := a.settings + settings.remoteFields = maps.Clone(settings.remoteFields) + quick := *a.quick + quick.fields = maps.Clone(quick.fields) + quick.categories = slices.Clone(quick.categories) + return overrideSnapshot{settings, quick, a.engine.Capture_terminal_session(terminal), a.muted, a.temporarySilence} +} + +func (a *application) install_override(saved overrideSnapshot, terminal int) { + live := saved.settings.live + live.Presentation.Speech.Enabled = !saved.muted + a.apply_remote_state(live, false) + a.settings = saved.settings + *a.quick = saved.quick + a.muted, a.temporarySilence = saved.muted, saved.silence + a.configure_reader() + a.engine.Restore_session(saved.runtime, terminal) + a.spellSession++ + a.reading_regions_changed() +} + +// A synthesizer selection can require asynchronous restoration. Until it +// completes, no new context is installed and all settings mutations are gated. +func (a *application) restore_override(done func()) bool { + current := a.overrideCurrent + if current == nil { + current = a.overrideFailed + } + if current == nil { + done() + return true + } + if a.speechPending || a.overrideTransition || a.overrideRestoreFailed { + return false + } + // A form opened under the application must never outlive that context: + // its previews and save draft belong to the old effective configuration. + a.ui.close() + if a.overrideCurrent != nil { + current.temporary = a.capture_override(current.terminal) + } + restore := func() { + a.install_override(current.underlying, current.terminal) + a.overrideCurrent = nil + a.overrideFailed = nil + current.adapter.Reset() + done() + } + if same_speech_selection(a.settings.live.Presentation.Speech, current.underlying.settings.live.Presentation.Speech) { + restore() + return true + } + a.overrideTransition = true + a.change_speech(current.underlying.settings.live.Presentation.Speech, func(err error) { + a.overrideTransition = false + if err != nil { + a.overrideRestoreFailed = true + // Restore ordinary reading even if the old synthesizer cannot be + // prepared. Keep its actual current selection and retain the save + // guard until an explicit Default/selection retries restoration. + fallback := current.underlying + copy_speech_selection(&fallback.settings.live.Presentation.Speech, a.settings.live.Presentation.Speech) + a.install_override(fallback, current.terminal) + a.overrideCurrent, a.overrideFailed = nil, current + current.adapter.Reset() + a.voice.Say(i18n.Format("Could not restore application settings: {0}", err.Error())) + return + } + restore() + }) + return false +} + +func (a *application) select_override(id string) { + if a.saver.busy || a.speechPending || a.overrideTransition { + a.voice.Say(i18n.T("Settings are busy")) + return + } + terminal := a.lastTerminal + if terminal == 0 || !a.lastActive { + return + } + a.overrideRestoreFailed = false // An explicit selection retries restoration. + a.restore_override(func() { + a.overrideDirty = true + if old := a.overrides[terminal]; old != nil { + old.adapter.Close() + delete(a.overrides, terminal) + } + if id == "default" { + a.voice.Say(i18n.T("Default settings restored")) + return + } + var adapter applicationoverrides.Adapter + switch id { + case "ninjam": + adapter = &ninjam.Adapter{} + case "networkmanager": + adapter = &networkmanager.Adapter{} + default: + return + } + if a.overrides == nil { + a.overrides = make(map[int]*applicationOverride) + } + selected := &applicationOverride{id: id, terminal: terminal, adapter: adapter} + selected.shell.Observe(a.lastSnapshot) + a.overrides[terminal] = selected + a.voice.Say(i18n.Format("{0} application override selected", i18n.T(adapter.Name()))) + }) +} + +// 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 { + return applicationoverrides.Frame{}, false + } + if a.overrideRestoreFailed { + return applicationoverrides.Frame{}, true + } + selected := a.overrides[next.Terminal] + if a.overrideCurrent != nil && (a.overrideCurrent != selected || !next.Active) { + if !a.restore_override(func() {}) { + return applicationoverrides.Frame{}, false + } + } + if selected == nil || !next.Active { + return applicationoverrides.Frame{}, true + } + frame := selected.adapter.Observe(next) + freshShell := selected.shell.Observe(next) + if !frame.Recognized { + selected.recognized = false + selected.exitPending = selected.exitPending || freshShell + if a.overrideCurrent == selected { + if !a.restore_override(func() {}) { + return applicationoverrides.Frame{}, false + } + } + if selected.exitPending { + selected.adapter.Close() + delete(a.overrides, next.Terminal) + a.voice.Say(i18n.T("Default settings restored")) + } + return applicationoverrides.Frame{}, true + } + if a.overrideCurrent != selected { + if a.speechPending { + return applicationoverrides.Frame{}, false + } + a.ui.close() + 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 + } else { + // Numeric/runtime settings are synchronous. Speech selection is + // restored separately before resuming a suspended adapter. + if !same_speech_selection(a.settings.live.Presentation.Speech, selected.temporary.settings.live.Presentation.Speech) { + a.overrideTransition = true + a.change_speech(selected.temporary.settings.live.Presentation.Speech, func(err error) { + a.overrideTransition = false + if err != nil { + a.overrideRestoreFailed = true + a.voice.Say(i18n.Format("Could not restore application settings: {0}", err.Error())) + return + } + a.install_override(selected.temporary, next.Terminal) + selected.adapter.Reset() + a.overrideCurrent = selected + }) + return applicationoverrides.Frame{}, false + } + a.install_override(selected.temporary, next.Terminal) + } + a.overrideCurrent = selected + } + selected.recognized = true + return frame, true +} + +func (a *application) open_applications() { + if a.saver.busy || a.speechPending || a.overrideTransition { + a.voice.Say(i18n.T("Settings are busy")) + return + } + items := []string{i18n.T("Default"), i18n.T("NetworkManager"), i18n.T("Ninjam")} + ids := []string{"default", "networkmanager", "ninjam"} + position := 0 + if selected := a.overrides[a.lastTerminal]; selected != nil { + position = slices.Index(ids, selected.id) + } + session, err := interaction.New_session(interaction.Form{Type: interaction.TabForm, Title: "Applications", Tabs: []interaction.Tab{{ID: "applications", Label: "Applications", Controls: []interaction.Control{ + {ID: "application", Label: "Application", Kind: interaction.List, Items: items, Selected: position, Action: "activate"}, + {ID: "cancel", Label: "Cancel", Kind: interaction.Button, Action: "cancel"}, + }}}}) + if err != nil { + a.ui.failed("Could not open Applications: {0}", err) + return + } + if _, err := a.ui.begin("Applications"); err != nil { + a.ui.failed("Could not open Applications: {0}", err) + return + } + session.Handle(interaction.Key{Name: "tab"}) // Start on the list, not its tab strip. + a.ui.session = session + a.ui.action = func(action string, session *interaction.Session) { + if action == "cancel" { + a.ui.close() + return + } + if action == "activate" { + selected := session.Values()["applications.application"].Selected + a.ui.close() + a.select_override(ids[selected]) + } + } + a.voice.Say(session.Open()) +} diff --git a/internal/app/application_overrides_test.go b/internal/app/application_overrides_test.go new file mode 100644 index 00000000..737bcc47 --- /dev/null +++ b/internal/app/application_overrides_test.go @@ -0,0 +1,329 @@ +// Copyright (c) 2026 Storm Dragon +// SPDX-License-Identifier: GPL-3.0-or-later + +package app + +import ( + "errors" + "os" + "reflect" + "slices" + "strings" + "testing" + + "git.stormux.org/storm/fenrir/internal/command" + "git.stormux.org/storm/fenrir/internal/config" + "git.stormux.org/storm/fenrir/internal/input" + "git.stormux.org/storm/fenrir/internal/interaction" + "git.stormux.org/storm/fenrir/internal/screen" + "git.stormux.org/storm/fenrir/internal/speech" +) + +func override_screen(terminal int, kind string) screen.Snapshot { + s := screen.Snapshot{Terminal: terminal, Active: true, Rows: 24, Cols: 100, Y: 23, Cells: make([]rune, 2400), Attributes: make([]screen.Attribute, 2400)} + for index := range s.Cells { + s.Cells[index] = ' ' + } + switch kind { + case "ninjam": + copy(s.Line(0), []rune("LOCAL")) + copy(s.Line(1), []rune(" master: [ ]mute [0.0dB center] | metronome: [ ]mute [0.0dB center]")) + copy(s.Line(16), []rune("CHAT")) + copy(s.Line(23), []rune("[QUIT NINJAM] : fixture.invalid : 120.0BPM 16BPI")) + s.Attributes[111] = screen.Attribute{Foreground: screen.ColorIndexed, Background: screen.ColorIndexed | 7} + case "networkmanager": + copy(s.Line(2), []rune("┌──────── NetworkManager TUI ─────────┐")) + default: + copy(s.Line(23), []rune(kind)) + } + return s +} + +func TestOverrideRestoresSynthesizerAndKeepsSaveBlockedUntilCompletion(t *testing.T) { + for _, failed := range []bool{false, true} { + a, voice, _ := selection_application(t, true) + a.prepareSpeech = func(config.Speech) (speech.Transport, error) { return &preparedVoice{}, nil } + override_read(t, a, override_screen(1, "ninjam")) + a.select_override("ninjam") + override_read(t, a, override_screen(1, "ninjam")) + baseline := a.settings.saveDraft + 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 failed { + a.prepareSpeech = func(config.Speech) (speech.Transport, error) { return nil, errors.New("fixture unavailable") } + } + a.select_override("default") + if !a.speechPending || !a.saves_unavailable() { + t.Fatal("restoration did not retain save guard") + } + a.handle_command(command.NextQuickValue) + if voice.messages[len(voice.messages)-1] != "Settings are busy" { + t.Fatal("settings changed during restoration") + } + finish_selection(t, a) + if failed { + if !a.saves_unavailable() || !a.overrideRestoreFailed { + t.Fatal("failed restoration released save guard") + } + override_read(t, a, override_screen(1, "shell output remains readable")) + if a.overrideCurrent != nil || !strings.Contains(voice.messages[len(voice.messages)-1], "shell output remains readable") { + t.Fatal("failed speech restoration suppressed ordinary reading", voice.messages) + } + a.prepareSpeech = func(config.Speech) (speech.Transport, error) { return &preparedVoice{}, nil } + a.select_override("default") + finish_selection(t, a) + } + if a.saves_unavailable() || !same_speech_selection(a.settings.live.Presentation.Speech, baseline.Presentation.Speech) { + t.Fatal("speech selection not restored") + } + } +} + +func TestExitWaitsForPendingSpeechChangeAndRestoresBeforeShellOutput(t *testing.T) { + a, _, _ := selection_application(t, true) + a.prepareSpeech = func(config.Speech) (speech.Transport, error) { return &preparedVoice{}, nil } + override_read(t, a, override_screen(1, "ninjam")) + a.select_override("ninjam") + override_read(t, a, override_screen(1, "ninjam")) + if response := request_selection(t, a, "SET speech#voice=Temporary;speech#module=fixture"); response != "" { + t.Fatal(response) + } + override_read(t, a, override_screen(1, "[Username@host directory] $")) + if !a.speechPending || a.overrides[1] == nil { + t.Fatal("exit did not wait for restoration") + } + finish_selection(t, a) + override_read(t, a, override_screen(1, "[Username@host directory] $")) + if a.overrides[1] != nil || a.overrideCurrent != nil || a.engine.Tracking_mode() != "cursor" { + t.Fatal("async exit missed") + } +} + +func override_read(t *testing.T, a *application, snapshot screen.Snapshot) { + t.Helper() + a.source.(*fakeSource).snapshot = snapshot + if err := a.read_screen(); err != nil { + t.Fatal(err) + } +} + +func TestApplicationsMenuActivatesOnlyOnEnterAndReleasesCapture(t *testing.T) { + a, voice := persistence_application(t, true) + override_read(t, a, override_screen(1, "ninjam")) + a.open_applications() + if !a.ui.active || !strings.Contains(voice.messages[len(voice.messages)-1], "Default") { + t.Fatal(voice.messages) + } + a.ui.handle(interaction.Key{Name: "down"}) + a.ui.handle(interaction.Key{Name: "down"}) + if len(a.overrides) != 0 { + t.Fatal("navigation activated override") + } + a.ui.handle(interaction.Key{Name: "escape"}) + if len(a.overrides) != 0 || a.ui.active { + t.Fatal("cancel activated override") + } + a.open_applications() + a.ui.handle(interaction.Key{Name: "down"}) + a.ui.handle(interaction.Key{Name: "down"}) + a.ui.handle(interaction.Key{Name: "enter"}) + if a.ui.active || a.overrides[1] == nil || a.overrides[1].id != "ninjam" { + t.Fatal("activation failed") + } + override_read(t, a, override_screen(1, "ninjam")) + if a.engine.Tracking_mode() != "highlight" { + t.Fatal("settings not applied") + } + a.open_applications() + if a.ui.session.Values()["applications.application"].Selected != 2 { + t.Fatal("active item not selected") + } + a.ui.handle(interaction.Key{Name: "tab"}) + a.ui.handle(interaction.Key{Name: "enter"}) + if a.ui.active || a.overrides[1] == nil { + t.Fatal("Cancel button changed selection") + } + a.open_applications() + a.ui.handle(interaction.Key{Name: "down"}) // list wraps to Default + a.ui.handle(interaction.Key{Name: "enter"}) + if a.saves_unavailable() || a.engine.Tracking_mode() != "cursor" { + t.Fatal("default failed") + } +} + +func TestOverrideCannotSaveOrContaminateDraftAndRestoresSession(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) + a.handle_command(command.ToggleAttributes) + override_read(t, a, override_screen(1, "ninjam")) + baseSettings, baseQuick, baseRuntime := a.settings, a.quick.revision, a.engine.Capture_session() + before, err := os.ReadFile(a.options.ConfigPath) + if err != nil { + t.Fatal(err) + } + a.select_override("ninjam") + override_read(t, a, override_screen(1, "ninjam")) + for _, id := range []command.ID{command.NextQuickValue, command.ProgressMonitor, command.ToggleTracking, command.AutoRead, command.ToggleAttributes} { + a.handle_command(id) + } + 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") + } + a.save_settings() + if a.saver.busy || voice.messages[len(voice.messages)-1] != "Save unavailable." { + t.Fatal("shortcut save admitted") + } + for _, text := range []string{"SETTING SAVE", "SETTING SAVEAS " + a.options.ConfigPath} { + if got := runtime_request(a, text); got != "Save unavailable." { + t.Fatal(text, got) + } + } + 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"}) + if !a.ui.active || a.saver.busy || voice.messages[len(voice.messages)-1] != "Save unavailable." { + t.Fatal("menu save admitted", voice.messages) + } + a.ui.close() + a.select_override("default") + if !reflect.DeepEqual(a.settings, baseSettings) || a.quick.revision != baseQuick || !reflect.DeepEqual(a.engine.Capture_session(), baseRuntime) { + t.Fatal("pre-override session was not restored") + } + after, _ := os.ReadFile(a.options.ConfigPath) + if !slices.Equal(before, after) { + t.Fatal("configuration changed") + } + a.save_settings() + finish_persistence(t, a, false) + saved, _, err := config.Load(a.options.ConfigPath, nil) + if err != nil || saved.Presentation.Speech.Rate != baseSettings.live.Presentation.Speech.Rate || saved.Keyboard.WordEcho { + t.Fatal("override leaked after clearing", saved, err) + } +} + +func TestOverridesSuspendPerTerminalAndClearOnlyOnFreshShell(t *testing.T) { + a, _ := persistence_application(t, false) + 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) + appRate := a.settings.live.Presentation.Speech.Rate + override_read(t, a, override_screen(2, "other terminal")) + if a.overrideCurrent != nil || a.engine.Tracking_mode() != "cursor" || a.settings.live.Presentation.Speech.Rate == appRate { + t.Fatal("override leaked across VT") + } + override_read(t, a, override_screen(2, "networkmanager")) + a.select_override("networkmanager") + override_read(t, a, override_screen(2, "networkmanager")) + if a.overrideCurrent == nil || a.overrideCurrent.id != "networkmanager" { + t.Fatal("second terminal activation") + } + override_read(t, a, override_screen(1, "ninjam")) + if a.overrideCurrent == nil || a.overrideCurrent.id != "ninjam" || a.settings.live.Presentation.Speech.Rate != appRate { + t.Fatal("resume lost temporary state") + } + for _, text := range []string{"Password:", "Confirm? [y/n]", "unrecognized screen", " $"} { + override_read(t, a, override_screen(1, text)) + if a.overrides[1] == nil || a.overrideCurrent != nil || a.engine.Tracking_mode() != "cursor" { + t.Fatal("bad suspension", text) + } + } + s := override_screen(1, "[Username@host directory] $") + s.Generation = 2 // Returning from an alternate buffer must still detect exit. + override_read(t, a, s) + if a.overrides[1] != nil || a.overrides[2] == nil { + t.Fatal("exit affected wrong terminal") + } + a.select_override("default") + if a.overrides[2] == nil { + t.Fatal("Default cleared another terminal") + } +} + +func TestOverrideRestoresUserRegionsAndPreservesRawReview(t *testing.T) { + a, voice := persistence_application(t, true) + override_read(t, a, override_screen(1, "ninjam")) + a.engine.Command(command.CurrentLine) + a.engine.Set_mark() + a.engine.Command(command.SetLiveRegion) + before := a.engine.Command(command.ReadLiveRegion1).Text + a.select_override("ninjam") + override_read(t, a, override_screen(1, "ninjam")) + a.handle_command(command.ReadLiveRegion1) + if voice.messages[len(voice.messages)-1] != "No chat message received" { + t.Fatal(voice.messages) + } + a.handle_command(command.ReadScreen) + if !strings.Contains(voice.messages[len(voice.messages)-1], "[ ]mute") { + t.Fatal("raw screen replaced", voice.messages) + } + a.select_override("default") + if after := a.engine.Command(command.ReadLiveRegion1).Text; after != before { + t.Fatal("user region lost", before, after) + } +} + +func TestApplicationExitInvalidatesSettingsFormAndItsPreviews(t *testing.T) { + a, _ := persistence_application(t, true) + override_read(t, a, override_screen(1, "ninjam")) + baseline := a.settings + a.select_override("ninjam") + override_read(t, a, override_screen(1, "ninjam")) + a.open_settings() + receive_ui(t, a.ui) + generation := a.ui.generation + reading := a.settings.live.Reading + reading.Spellcheck = !reading.Spellcheck + a.preview_spelling(reading) + override_read(t, a, override_screen(1, "[Username@host directory] $")) + if a.ui.active || !reflect.DeepEqual(a.settings, baseline) { + t.Fatal("old form or preview survived exit") + } + a.ui.update(uiUpdate{generation: generation, apply: func() { t.Fatal("stale form callback applied") }}) + a.save_settings() + finish_persistence(t, a, false) + saved, _, err := config.Load(a.options.ConfigPath, nil) + if err != nil || saved.Reading.Mode != baseline.saveDraft.Reading.Mode || saved.Reading.Spellcheck != baseline.saveDraft.Reading.Spellcheck { + t.Fatal("old application form contaminated subsequent save", err) + } +} + +func TestActivationRejectsPendingOperationsAndRestartStartsWithoutOverride(t *testing.T) { + a, _ := persistence_application(t, true) + override_read(t, a, override_screen(1, "ninjam")) + a.saver.busy = true + a.select_override("ninjam") + if len(a.overrides) != 0 { + t.Fatal("activated during save") + } + a.saver.busy, a.speechPending = false, true + a.select_override("ninjam") + if len(a.overrides) != 0 { + t.Fatal("activated during speech selection") + } + a.speechPending = false + a.select_override("ninjam") + override_read(t, a, override_screen(1, "ninjam")) + saved, _, err := config.Load(a.options.ConfigPath, nil) + if err != nil { + t.Fatal(err) + } + fresh, err := new_application(a.ctx, &fakeSource{}, &fakeKeys{poll: func(bool, func(input.Decision)) error { return nil }}, &fakeVoice{}, RunOptions{Settings: saved}) + if err != nil || fresh.saves_unavailable() || fresh.engine.Tracking_mode() != "cursor" { + t.Fatal("restart retained override", err) + } +} diff --git a/internal/app/commands.go b/internal/app/commands.go index a1756e2b..c57da3f3 100644 --- a/internal/app/commands.go +++ b/internal/app/commands.go @@ -42,6 +42,11 @@ func (a *application) handle_input(decision input.Decision) { "interrupt", decision.Interrupt, "keypress", decision.Keypress, "typing", decision.Typing, "deletion", decision.Deletion, "ui_active", a.ui.active) if decision.Interrupt { + if decision.Forward && !a.ui.active && a.overrideCurrent != nil { + if observer, ok := a.overrideCurrent.adapter.(interface{ Note_key(uint16) }); ok { + observer.Note_key(decision.Code) + } + } a.spellFeedback++ a.engine.Reset_progress() if a.cues != nil { @@ -75,6 +80,16 @@ func (a *application) handle_input(decision input.Decision) { } func (a *application) handle_command(id command.ID) { + if a.overrideTransition && id != "" && id != command.Quit && id != command.Mute { + a.voice.Say(i18n.T("Settings are busy")) + return + } + if selected := a.overrideCurrent; selected != nil && selected.recognized { + if result, handled := selected.adapter.Command(id); handled { + a.present(result.Text, result.Cue) + return + } + } switch id { case "": case command.Help: @@ -99,12 +114,16 @@ func (a *application) handle_command(id command.ID) { a.spellMenu.open(a.engine.Spellcheck_word()) case command.FenrirMenu: a.open_settings() + case command.ApplicationsMenu: + a.open_applications() case command.SaveSettings: a.save_settings() case command.ProgressMonitor: a.quick.mark_changed("presentation.progress.enabled") a.settings.live.Presentation.Progress.Enabled = !a.settings.live.Presentation.Progress.Enabled - a.settings.saveDraft.Presentation.Progress = a.settings.live.Presentation.Progress + if a.overrideCurrent == nil { + 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")) @@ -157,7 +176,9 @@ func (a *application) review_command(id command.ID) { } switch id { case command.ToggleTracking: - a.settings.saveDraft.Reading.Mode = a.engine.Tracking_mode() + if a.overrideCurrent == nil { + 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 0088ba38..3a5eab23 100644 --- a/internal/app/event_loop.go +++ b/internal/app/event_loop.go @@ -57,6 +57,11 @@ func run(ctx context.Context, source Source, keyboards Keyboards, voices Speaker return err } defer func() { a.ui.close(); a.ui.testWorkers.Wait() }() + defer func() { + for _, selected := range a.overrides { + selected.adapter.Close() + } + }() return a.loop(ticks) } @@ -111,6 +116,9 @@ func (a *application) loop(ticks <-chan time.Time) error { a.quick.discovery = discovery case result := <-a.speechResults: a.speech_result(result) + if len(a.overrides) != 0 || a.overrideCurrent != nil { + a.overrideDirty = true + } case result := <-spellResults: if err := a.poll_input(false); err != nil { return err @@ -175,6 +183,12 @@ func (a *application) loop(ticks <-chan time.Time) error { return err } case <-ticks: + if a.overrideDirty { + a.overrideDirty = false + if err := a.read_screen(); err != nil { + return err + } + } if err := a.poll_input(false); err != nil { return err } diff --git a/internal/app/remote_persistence.go b/internal/app/remote_persistence.go index 19e43fff..8f9b50ea 100644 --- a/internal/app/remote_persistence.go +++ b/internal/app/remote_persistence.go @@ -17,6 +17,12 @@ import ( func (a *application) remote_settings_request(text string, request control.Request) (string, bool) { verb, parameter, _ := strings.Cut(text, " ") verb, parameter = strings.ToUpper(verb), strings.TrimSpace(parameter) + if a.saves_unavailable() && (verb == "SAVE" || verb == "SAVEAS") { + return "Save unavailable.", false + } + if a.overrideTransition { + return "Settings are busy; try again after the current operation", false + } if a.speechPending { return "Settings are busy; try again after the current operation", false } @@ -143,6 +149,10 @@ 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) diff --git a/internal/app/runtime_settings.go b/internal/app/runtime_settings.go index 9a6b06be..8e750e34 100644 --- a/internal/app/runtime_settings.go +++ b/internal/app/runtime_settings.go @@ -147,6 +147,10 @@ func (a *application) open_settings() { } func (a *application) save_settings() { + if a.saves_unavailable() { + a.voice.Say(i18n.T("Save unavailable.")) + return + } if a.speechPending { a.voice.Say(i18n.T("Failed to save settings.")) return @@ -160,6 +164,10 @@ func (a *application) save_settings() { // presentation belongs to the modal generation; a committed write belongs to // the application even after Escape or a session handoff closes its form. func (a *application) save_menu_settings(doc *config.Document, draft config.Settings, done func(error)) bool { + if a.saves_unavailable() { + a.voice.Say(i18n.T("Save unavailable.")) + return false + } if a.speechPending { a.ui.failed("Could not save settings: {0}", fmt.Errorf("speech selection in progress")) return false @@ -248,6 +256,10 @@ 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/app/screen_updates.go b/internal/app/screen_updates.go index d06c01d0..99dc6976 100644 --- a/internal/app/screen_updates.go +++ b/internal/app/screen_updates.go @@ -58,8 +58,16 @@ func (a *application) read_screen() error { } } a.clipboards.terminal = snapshot.Terminal + a.lastSnapshot = snapshot + frame, ready := a.observe_override(snapshot) + if !ready { + a.lastTerminal, a.lastActive = snapshot.Terminal, snapshot.Active + a.lastGeneration, a.lastRows, a.lastCols = snapshot.Generation, snapshot.Rows, snapshot.Cols + a.lastNumLock = snapshot.NumLock + return nil + } updated := time.Now() - output := a.engine.Update(snapshot) + output := a.engine.Update_application(snapshot, frame) if a.options.Spellcheck != nil && a.settings.live.Reading.Spellcheck && !a.ui.active && !a.muted && !a.temporarySilence { for _, word := range a.engine.Completed_words() { a.options.Spellcheck.Submit(spellcheck.Request{Operation: spellcheck.Automatic, Word: word, Generation: a.spellSession, Revision: a.spellFeedback}) diff --git a/internal/applicationoverrides/networkmanager/networkmanager.go b/internal/applicationoverrides/networkmanager/networkmanager.go new file mode 100644 index 00000000..1c1cf9a3 --- /dev/null +++ b/internal/applicationoverrides/networkmanager/networkmanager.go @@ -0,0 +1,35 @@ +// Copyright (c) 2026 Storm Dragon +// SPDX-License-Identifier: GPL-3.0-or-later + +package networkmanager + +import ( + "strings" + + "git.stormux.org/storm/fenrir/internal/applicationoverrides" + "git.stormux.org/storm/fenrir/internal/command" + "git.stormux.org/storm/fenrir/internal/config" + "git.stormux.org/storm/fenrir/internal/presentation" + "git.stormux.org/storm/fenrir/internal/screen" +) + +type Adapter struct{} + +func (*Adapter) Name() string { return "NetworkManager" } +func (*Adapter) Configure(settings *config.Settings) { settings.Reading.Mode = "highlight" } +func (*Adapter) Reset() {} +func (*Adapter) Close() {} +func (*Adapter) Command(command.ID) (presentation.CommandResult, bool) { + return presentation.CommandResult{}, false +} + +func (*Adapter) Observe(snapshot screen.Snapshot) applicationoverrides.Frame { + // nmtui's newt dialogs retain a border and their standard title/action text. + text := string(snapshot.Cells) + border := strings.ContainsAny(text, "┌┐└┘╔╗╚╝") || strings.Contains(text, "+---") + title := false + for _, label := range []string{"NetworkManager", "Edit Connection", "Activate a connection", "Set Hostname", "Select a connection", "Edit a connection"} { + title = title || strings.Contains(text, label) + } + return applicationoverrides.Frame{Recognized: border && title} +} diff --git a/internal/applicationoverrides/ninjam/ninjam.go b/internal/applicationoverrides/ninjam/ninjam.go new file mode 100644 index 00000000..ef7caffb --- /dev/null +++ b/internal/applicationoverrides/ninjam/ninjam.go @@ -0,0 +1,312 @@ +// Copyright (c) 2026 Storm Dragon +// SPDX-License-Identifier: GPL-3.0-or-later + +// Package ninjam interprets the cninjam curses client's controls and chat. +package ninjam + +import ( + "fmt" + "slices" + "strconv" + "strings" + "time" + + "git.stormux.org/storm/fenrir/internal/applicationoverrides" + "git.stormux.org/storm/fenrir/internal/command" + "git.stormux.org/storm/fenrir/internal/config" + "git.stormux.org/storm/fenrir/internal/i18n" + "git.stormux.org/storm/fenrir/internal/presentation" + "git.stormux.org/storm/fenrir/internal/screen" +) + +type Adapter struct { + previous screen.Snapshot + focusID, focusText, instruction, status string + chat []string + latest string + regions []applicationoverrides.Region + historyNavigation bool + historyAt time.Time +} + +func (*Adapter) Name() string { return "Ninjam" } +func (*Adapter) Configure(settings *config.Settings) { + settings.Reading.Mode = "highlight" + settings.Review.LeaveReviewOnCursorChange = true +} +func (a *Adapter) Reset() { + a.previous = screen.Snapshot{} + a.focusID, a.focusText, a.instruction, a.status = "", "", "", "" + a.chat, a.regions, a.historyNavigation = nil, nil, false +} +func (a *Adapter) Close() { a.Reset(); a.latest = "" } + +// PageUp/PageDown browse existing chat. Rebaseline the next redraw instead of +// announcing it as incoming traffic. Linux input key codes are protocol data. +func (a *Adapter) Note_key(key uint16) { + if key == 104 || key == 109 { + a.historyNavigation = true + a.historyAt = time.Now() + } +} + +func (a *Adapter) Command(id command.ID) (presentation.CommandResult, bool) { + slot := -1 + switch id { + case command.ReadLiveRegion1: + slot = 0 + case command.ReadLiveRegion2: + slot = 1 + case command.ReadLiveRegion3: + slot = 2 + case command.ListReadingRegions: + return presentation.CommandResult{Text: i18n.T("Ninjam regions: 1 latest chat message; 2 selected channel or control; 3 connection, tempo and interval")}, true + } + if slot < 0 { + return presentation.CommandResult{}, false + } + text := "" + if slot < len(a.regions) { + text = a.regions[slot].Text + } + if slot == 0 { + text = a.latest + } + if text == "" { + if slot == 0 { + text = i18n.T("No chat message received") + } else { + text = i18n.T("Ninjam information unavailable") + } + } + return presentation.CommandResult{Text: text}, true +} + +func (a *Adapter) Observe(next screen.Snapshot) applicationoverrides.Frame { + frame := applicationoverrides.Frame{} + if !next.Active || next.Rows < 8 || next.Cols < 20 || len(next.Cells) != next.Rows*next.Cols { + a.Reset() + return frame + } + // Both fixed landmarks are drawn by showmainview, even when the chat topic + // replaces CHAT. A chat message mentioning NINJAM is insufficient evidence. + if strings.TrimSpace(next.Text(0)) != "LOCAL" || !strings.HasPrefix(strings.TrimSpace(next.Text(next.Rows-1)), "[QUIT NINJAM]") { + a.Reset() + return frame + } + frame.Recognized, frame.OwnsPresentation = true, true + stable := a.previous.Active && a.previous.Terminal == next.Terminal && a.previous.Generation == next.Generation && a.previous.Rows == next.Rows && a.previous.Cols == next.Cols + chatHeight := max(next.Rows/4, 4) + chatTop := next.Rows - 2 - chatHeight + focusID, focusText := selected_control(next, chatTop) + if next.Y == next.Rows-3 { + focusID, focusText = "chat", i18n.T("Chat message") + frame.Input = &screen.Region{Left: 0, Top: next.Y, Right: next.Cols - 1, Bottom: next.Y} + } + instruction := strings.TrimSpace(next.Text(next.Rows - 2)) + if strings.Trim(instruction, "# ") == "" { + instruction = "" + } + if strings.HasPrefix(instruction, "RENAME CHANNEL:") { + focusID, focusText = "rename", i18n.T("Rename channel") + frame.Input = &screen.Region{Left: len("RENAME CHANNEL: "), Top: next.Rows - 2, Right: next.Cols - 1, Bottom: next.Rows - 2} + instruction = "RENAME CHANNEL:" + } + frame.FocusChanged = stable && focusID != "" && a.focusID != "" && focusID != a.focusID + if focusText != "" && (!stable || focusText != a.focusText || focusID != a.focusID) { + frame.Focus = []presentation.Message{{Text: focusText}} + } + if instruction != "" && (!stable || instruction != a.instruction) { + frame.Updates = append(frame.Updates, presentation.Message{Text: instruction}) + } + + chat := chat_lines(next, chatTop+1, next.Rows-4) + if stable && len(chat) == 0 && len(a.chat) != 0 { + // A transient cleared chat area during redraw is not new history. + chat = slices.Clone(a.chat) + } + historyNavigation := a.historyNavigation && time.Since(a.historyAt) < 2*time.Second + if stable && !historyNavigation { + for _, text := range appended_lines(a.chat, chat) { + frame.Updates = append(frame.Updates, presentation.Message{Text: text}) + a.latest = text + } + } + status := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(next.Text(next.Rows-1)), "[QUIT NINJAM]")) + if stable && a.status != "" && status != a.status { + frame.Updates = append(frame.Updates, presentation.Message{Text: status}) + } + interval := strings.TrimSpace(next.Text(next.Rows - 2)) + if strings.Trim(interval, "# ") == "" && interval != "" { + interval = i18n.Format("Interval position {0} of {1}", strconv.Itoa(strings.Count(interval, "#")), strconv.Itoa(next.Cols)) + } else { + interval = "" + } + a.regions = []applicationoverrides.Region{{Name: i18n.T("Latest chat message"), Text: a.latest}, {Name: i18n.T("Selected channel or control"), Text: focusText}, {Name: i18n.T("Connection, tempo and interval"), Text: strings.TrimSpace(status + " " + interval)}} + frame.Regions = slices.Clone(a.regions) + chatChanged := !slices.Equal(a.chat, chat) + a.previous, a.chat = next, chat + a.focusID, a.focusText, a.instruction, a.status = focusID, focusText, instruction, status + a.historyNavigation = historyNavigation && !chatChanged + return frame +} + +type field struct { + start, end int + value, label string +} + +func fields(line []rune) []field { + var result []field + for start := 0; start < len(line); start++ { + if line[start] != '[' { + continue + } + end := start + 1 + for end < len(line) && line[end] != ']' { + end++ + } + if end == len(line) { + break + } + labelEnd := end + 1 + for labelEnd < len(line) && line[labelEnd] >= 'a' && line[labelEnd] <= 'z' { + labelEnd++ + } + result = append(result, field{start + 1, end, strings.TrimSpace(string(line[start+1 : end])), string(line[end+1 : labelEnd])}) + start = end + } + return result +} + +func selected_control(snapshot screen.Snapshot, chatTop int) (string, string) { + if len(snapshot.Attributes) != len(snapshot.Cells) { + return "", "" + } + remote, user := false, "" + for row := 1; row < snapshot.Rows; row++ { + if row >= chatTop && row != snapshot.Rows-1 { + continue + } + line := snapshot.Line(row) + text := strings.TrimSpace(string(line)) + if text == "REMOTE" { + remote = true + continue + } + controls := fields(line) + if remote && len(controls) == 0 && text != "" { + user = text + continue + } + for index, control := range controls { + if control.start == control.end { + continue + } + attribute := snapshot.Attributes[row*snapshot.Cols+control.start] + // cninjam uses black-on-white (color pair 5), or A_STANDOUT + // in monochrome. Ordinary bracket values are merely bold. + selected := attribute.Style&screen.Reverse != 0 || attribute.Foreground == screen.ColorIndexed && attribute.Background == screen.ColorIndexed|7 + if !selected { + continue + } + complete := true + for col := control.start; col < control.end; col++ { + complete = complete && snapshot.Attributes[row*snapshot.Cols+col] == attribute + } + if !complete { + continue + } + context := "" + if row == 1 && strings.Contains(text, "master:") { + context = i18n.T("Master") + if index >= 2 { + context = i18n.T("Metronome") + } + } else if remote && strings.HasPrefix(text, "\"") { + end := strings.Index(text[1:], "\"") + if end >= 0 { + context = i18n.Format("Remote {0}, channel {1}", user, text[1:end+1]) + } + } else if len(controls) >= 5 { + context = i18n.Format("Local channel {0}", controls[0].value) + } + value := control.value + switch control.label { + case "mute": + value = i18n.T("unmuted") + if control.value == "X" { + value = i18n.T("muted") + } + case "xmit": + value = i18n.T("not transmitting") + if control.value == "X" { + value = i18n.T("transmitting") + } + case "recv": + value = i18n.T("not receiving") + if control.value == "X" { + value = i18n.T("receiving") + } + default: + if strings.Contains(value, "dB") { + value = i18n.Format("Volume and pan {0}", value) + } + if len(controls) >= 5 && index == 0 { + value = i18n.Format("Channel name {0}", value) + } + if len(controls) >= 5 && index == 2 { + value = i18n.Format("Input {0}", value) + } + if value == "del" { + value = i18n.T("Delete channel") + } + if value == "new channel" { + value = i18n.T("New channel") + } + if value == "no remote users" { + value = i18n.T("No remote users") + } + if value == "QUIT NINJAM" { + value = i18n.T("Quit Ninjam") + } + } + return fmt.Sprintf("%d:%d:%s", row, index, context), strings.TrimSpace(context + " " + value) + } + } + return "", "" +} + +func chat_lines(snapshot screen.Snapshot, first, last int) []string { + var result []string + previousFull := false + for row := first; row <= last; row++ { + text := strings.TrimRight(string(snapshot.Line(row)[:snapshot.Cols-1]), " \x00") + if text == "" { + previousFull = false + continue + } + startsMessage := strings.HasPrefix(text, "<") || strings.HasPrefix(text, "* ") || strings.HasPrefix(text, "*** ") + if previousFull && !startsMessage && len(result) > 0 { + result[len(result)-1] += text + } else { + result = append(result, text) + } + previousFull = len([]rune(text)) == snapshot.Cols-1 + } + return result +} + +func appended_lines(previous, next []string) []string { + if slices.Equal(previous, next) { + return nil + } + for overlap := min(len(previous), len(next)); overlap > 0; overlap-- { + if slices.Equal(previous[len(previous)-overlap:], next[:overlap]) { + return next[overlap:] + } + } + // A burst can replace every visible history row between captures. Paging + // and layout changes are baselined by Observe before reaching this path. + return next +} diff --git a/internal/applicationoverrides/ninjam/ninjam_test.go b/internal/applicationoverrides/ninjam/ninjam_test.go new file mode 100644 index 00000000..43becf47 --- /dev/null +++ b/internal/applicationoverrides/ninjam/ninjam_test.go @@ -0,0 +1,236 @@ +// Copyright (c) 2026 Storm Dragon +// SPDX-License-Identifier: GPL-3.0-or-later + +package ninjam_test + +import ( + "slices" + "strings" + "testing" + + "git.stormux.org/storm/fenrir/internal/applicationoverrides/ninjam" + "git.stormux.org/storm/fenrir/internal/command" + "git.stormux.org/storm/fenrir/internal/presentation" + "git.stormux.org/storm/fenrir/internal/reader" + "git.stormux.org/storm/fenrir/internal/screen" +) + +func snapshot(selected int, mute, volume string, messages ...string) screen.Snapshot { + s := screen.Snapshot{Terminal: 1, Active: true, Rows: 24, Cols: 100, Y: 23, Cells: make([]rune, 2400), Attributes: make([]screen.Attribute, 2400)} + for i := range s.Cells { + s.Cells[i] = ' ' + s.Attributes[i] = screen.Attribute{Foreground: screen.ColorIndexed | 7, Background: screen.ColorIndexed} + } + paint := func(row int, text string) { copy(s.Line(row), []rune(text)) } + paint(0, "LOCAL") + paint(1, " master: ["+mute+"]mute ["+volume+"] | metronome: [ ]mute [0.0dB center]") + paint(2, " [Channel] [X]xmit [Input] [ ]mute [0.0dB center] [del] <-20.0dB>") + paint(3, "REMOTE") + paint(4, "Username") + paint(5, " \"Remote channel\" [X]recv [ ]mute [0.0dB center] <-20.0dB>") + paint(16, "CHAT") + for i, text := range messages { + if i < 4 { + paint(21-len(messages)+i, text) + } + } + paint(22, "####") + paint(23, "[QUIT NINJAM] : fixture.invalid : 120.0BPM 16BPI : 48000Hz") + field := 0 + for row := range s.Rows { + inside := false + for col, char := range s.Line(row) { + if char == ']' { + inside = false + } + if inside && field-1 == selected { + s.Attributes[row*s.Cols+col] = screen.Attribute{Foreground: screen.ColorIndexed, Background: screen.ColorIndexed | 7} + } + if char == '[' { + inside = true + field++ + } + } + } + return s +} + +func texts(messages []presentation.Message) []string { + var result []string + for _, message := range messages { + if message.Text != "" { + result = append(result, message.Text) + } + } + return result +} + +func TestControlsHaveContextAndState(t *testing.T) { + for _, trial := range []struct { + selected int + mute, want string + }{ + {0, " ", "Master unmuted"}, {0, "X", "Master muted"}, + {1, " ", "Master Volume and pan 0.0dB center"}, + {2, " ", "Metronome unmuted"}, {5, " ", "Local channel Channel transmitting"}, + {10, " ", "Remote Username, channel Remote channel receiving"}, + } { + a := &ninjam.Adapter{} + frame := a.Observe(snapshot(trial.selected, trial.mute, "0.0dB center")) + if got := texts(frame.Focus); !frame.Recognized || !slices.Equal(got, []string{trial.want}) { + t.Fatalf("%+v: %v", trial, got) + } + } +} + +func TestFocusChatMetersAndValueReflow(t *testing.T) { + a := &ninjam.Adapter{} + r := reader.New() + update := func(s screen.Snapshot) []string { return texts(r.Update_application(s, a.Observe(s))) } + update(snapshot(0, " ", "0.0dB center", " first")) + got := update(snapshot(1, " ", "0.0dB center", " first", " second")) + if !slices.Equal(got, []string{"Master Volume and pan 0.0dB center", " second"}) { + t.Fatal(got) + } + got = update(snapshot(1, " ", "-0.5dB center", " first", " second")) + if !slices.Equal(got, []string{"Master Volume and pan -0.5dB center"}) { + t.Fatal("reflow", got) + } + s := snapshot(1, " ", "-0.5dB center", " first", " second") + copy(s.Line(22), []rune("##################")) + copy(s.Line(2)[65:], []rune("<-10.0dB>")) + if got := update(s); len(got) != 0 { + t.Fatal("meter chatter", got) + } + if result, _ := a.Command(command.ReadLiveRegion1); result.Text != " second" { + t.Fatal(result) + } + if result, _ := a.Command(command.ReadLiveRegion3); !strings.Contains(result.Text, "120.0BPM") || !strings.Contains(result.Text, "Interval position 18 of 100") { + t.Fatal(result) + } +} + +func TestReviewResumesOnlyOnApplicationNavigation(t *testing.T) { + for _, direct := range []bool{false, true} { + a, r := &ninjam.Adapter{}, reader.New() + s := snapshot(0, " ", "0.0dB center", " old") + r.Update_application(s, a.Observe(s)) + r.Command(command.CurrentLine) + if direct { + r.Note_input(true, false, false) + } + s = snapshot(1, " ", "0.0dB center", " old", " new") + got := texts(r.Update_application(s, a.Observe(s))) + want := []string{" new"} + if direct { + want = append([]string{"Master Volume and pan 0.0dB center"}, want...) + } + if !slices.Equal(got, want) { + t.Fatal(direct, got) + } + } +} + +func TestChatScrollPagingRepetitionAndResize(t *testing.T) { + a := &ninjam.Adapter{} + a.Observe(snapshot(0, " ", "0.0dB center", " a", " b", " c", " d")) + frame := a.Observe(snapshot(0, " ", "0.0dB center", " b", " c", " d", " d")) + if got := texts(frame.Updates); !slices.Equal(got, []string{" d"}) { + t.Fatal("repeated message", got) + } + a.Note_key(104) + // An interval update can arrive before the application's history redraw. + a.Observe(snapshot(0, " ", "0.0dB center", " b", " c", " d", " d")) + frame = a.Observe(snapshot(0, " ", "0.0dB center", " old", " b", " c", " d")) + if len(frame.Updates) != 0 { + t.Fatal("history announced", frame.Updates) + } + s := snapshot(0, " ", "0.0dB center", " old history") + s.Generation++ + if frame = a.Observe(s); len(frame.Updates) != 0 { + t.Fatal("buffer rebaseline", frame.Updates) + } + copy(s.Line(0), []rune("SHELL")) + if a.Observe(s).Recognized { + t.Fatal("unrelated screen recognized") + } +} + +func TestChatAlongsideTypingAndMutedAutomaticReading(t *testing.T) { + a, r := &ninjam.Adapter{}, reader.New() + s := snapshot(-1, " ", "0.0dB center", " old") + s.Y = 21 + r.Update_application(s, a.Observe(s)) + r.Note_input(true, true, false) + s = snapshot(-1, " ", "0.0dB center", " old", " incoming") + s.Y, s.X = 21, 1 + s.Line(21)[0] = 'a' + got := texts(r.Update_application(s, a.Observe(s))) + if !slices.Equal(got, []string{"a", " incoming"}) { + t.Fatal(got) + } + r.Configure(false, true) + s = snapshot(-1, " ", "0.0dB center", " incoming", " silent") + s.Y = 21 + if got := texts(r.Update_application(s, a.Observe(s))); len(got) != 0 { + t.Fatal(got) + } + if result, _ := a.Command(command.ReadLiveRegion1); result.Text != " silent" { + t.Fatal("latest lost while autoread off", result) + } +} + +func TestChatBurstAndTransientClearedArea(t *testing.T) { + a := &ninjam.Adapter{} + old := []string{" a", " b", " c", " d"} + a.Observe(snapshot(0, " ", "0.0dB center", old...)) + if frame := a.Observe(snapshot(0, " ", "0.0dB center")); len(frame.Updates) != 0 { + t.Fatal("cleared area announced", frame.Updates) + } + if frame := a.Observe(snapshot(0, " ", "0.0dB center", old...)); len(frame.Updates) != 0 { + t.Fatal("redraw replayed history", frame.Updates) + } + burst := []string{" e", " f", " g", " h"} + if got := texts(a.Observe(snapshot(0, " ", "0.0dB center", burst...)).Updates); !slices.Equal(got, burst) { + t.Fatal("visible burst lost", got) + } +} + +func TestWrappedChatRecallAndChangedGeometry(t *testing.T) { + a := &ninjam.Adapter{} + a.Observe(snapshot(0, " ", "0.0dB center", " old")) + first := " " + strings.Repeat("x", 99-len(" ")) + frame := a.Observe(snapshot(0, " ", "0.0dB center", " old", first, " continuation")) + want := first + " continuation" + if got := texts(frame.Updates); !slices.Equal(got, []string{want}) { + t.Fatal("wrapped message", got) + } + if result, _ := a.Command(command.ReadLiveRegion1); result.Text != want { + t.Fatal("recall lost continuation", result) + } + old := snapshot(0, " ", "0.0dB center") + resized := screen.Snapshot{Terminal: 1, Active: true, Rows: 20, Cols: 80, Y: 19, Cells: make([]rune, 1600), Attributes: make([]screen.Attribute, 1600)} + for i := range resized.Cells { + resized.Cells[i] = ' ' + } + for row := 0; row < 3; row++ { + copy(resized.Line(row), old.Line(row)[:80]) + copy(resized.Attributes[row*80:(row+1)*80], old.Attributes[row*100:row*100+80]) + } + copy(resized.Line(13), []rune("CHAT")) + copy(resized.Line(16), []rune(" old history")) + copy(resized.Line(18), []rune("#####")) + copy(resized.Line(19), old.Line(23)[:80]) + if frame := a.Observe(resized); len(frame.Updates) != 0 { + t.Fatal("resize replayed history", frame.Updates) + } + copy(resized.Line(15), resized.Line(16)) + clear(resized.Line(16)) + copy(resized.Line(16), []rune(" after resize")) + if got := texts(a.Observe(resized).Updates); !slices.Equal(got, []string{" after resize"}) { + t.Fatal("resized chat region", got) + } + if result, _ := a.Command(command.ReadLiveRegion3); !strings.Contains(result.Text, "of 80") { + t.Fatal("resized interval region", result) + } +} diff --git a/internal/applicationoverrides/override.go b/internal/applicationoverrides/override.go new file mode 100644 index 00000000..316b5224 --- /dev/null +++ b/internal/applicationoverrides/override.go @@ -0,0 +1,38 @@ +// Copyright (c) 2026 Storm Dragon +// SPDX-License-Identifier: GPL-3.0-or-later + +// Package applicationoverrides defines built-in, event-loop-owned application +// adapters. Adapters interpret snapshots; they never own devices or persistence. +package applicationoverrides + +import ( + "git.stormux.org/storm/fenrir/internal/command" + "git.stormux.org/storm/fenrir/internal/config" + "git.stormux.org/storm/fenrir/internal/presentation" + "git.stormux.org/storm/fenrir/internal/screen" +) + +type Region struct { + Name, Text string +} + +// Frame supplements the ordinary reader. OwnsPresentation replaces generic +// automatic diffs, never raw review/copy or typing echo. Updates are independent +// of focus: a focus change must not swallow a simultaneous chat message. +type Frame struct { + Recognized, OwnsPresentation, FocusChanged bool + Focus, Updates []presentation.Message + Regions []Region + // Input limits typing/deletion echo to an application's actual input field. + Input *screen.Region +} + +type Adapter interface { + Name() string + Configure(*config.Settings) + Observe(screen.Snapshot) Frame + Command(command.ID) (presentation.CommandResult, bool) + // Reset forgets screen-derived baselines after suspension or layout changes. + Reset() + Close() +} diff --git a/internal/command/command.go b/internal/command/command.go index cae46a13..db2a69ce 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -64,6 +64,7 @@ const ( EmojiMenu ID = "emoji_menu" SpellcheckMenu ID = "spellcheck_menu" FenrirMenu ID = "fenrir_menu" + ApplicationsMenu ID = "applications_menu" NextQuickCategory ID = "next_quick_menu_category" PreviousQuickCategory ID = "prev_quick_menu_category" CurrentQuickEntry ID = "current_quick_menu_entry" diff --git a/internal/command/descriptions.go b/internal/command/descriptions.go index 4e282a44..83074faa 100644 --- a/internal/command/descriptions.go +++ b/internal/command/descriptions.go @@ -53,6 +53,7 @@ var descriptions = map[ID]string{ EmojiMenu: "Emoji Menu", SpellcheckMenu: "Spellcheck Menu", FenrirMenu: "Fenrir Menu", + ApplicationsMenu: "Select a temporary application override", NextQuickCategory: "get next quick menu category", PreviousQuickCategory: "get previous quick menu category", CurrentQuickEntry: "get current quick menu entry", diff --git a/internal/i18n/locale/de/LC_MESSAGES/fenrir.po b/internal/i18n/locale/de/LC_MESSAGES/fenrir.po index 15066d5e..89b3afa9 100644 --- a/internal/i18n/locale/de/LC_MESSAGES/fenrir.po +++ b/internal/i18n/locale/de/LC_MESSAGES/fenrir.po @@ -67,6 +67,14 @@ msgstr "Wütend" msgid "Apple" msgstr "Apfel" +#: internal/app/application_overrides.go:237 +msgid "Application" +msgstr "Anwendung" + +#: internal/app/application_overrides.go:236 +msgid "Applications" +msgstr "Anwendungen" + #: internal/emoji/catalog.go:117 msgid "Argentina" msgstr "Argentinien" @@ -155,6 +163,7 @@ msgstr "Schmetterling" msgid "Canada" msgstr "Kanada" +#: internal/app/application_overrides.go:238 #: internal/preferences/settings.go:79 msgid "Cancel" msgstr "Abbrechen" @@ -167,10 +176,18 @@ msgstr "Katze" msgid "Chains" msgstr "Ketten" +#: internal/applicationoverrides/ninjam/ninjam.go:256 +msgid "Channel name {0}" +msgstr "Kanalname {0}" + #: internal/config/config.go:140 msgid "Character echo" msgstr "Zeichenansage" +#: internal/applicationoverrides/ninjam/ninjam.go:103 +msgid "Chat message" +msgstr "Chatnachricht" + #: internal/emoji/catalog.go:102 msgid "Checkmark" msgstr "Häkchen" @@ -187,51 +204,51 @@ msgstr "China" msgid "Christmas tree" msgstr "Weihnachtsbaum" -#: internal/command/descriptions.go:76 +#: internal/command/descriptions.go:77 msgid "Clear all live regions" msgstr "Alle Live-Bereiche löschen" -#: internal/command/descriptions.go:77 +#: internal/command/descriptions.go:78 msgid "Clear all reading regions" msgstr "Alle Lesebereiche löschen" -#: internal/command/descriptions.go:79 +#: internal/command/descriptions.go:80 msgid "Clear live region 1" msgstr "Live-Bereich 1 löschen" -#: internal/command/descriptions.go:97 +#: internal/command/descriptions.go:98 msgid "Clear live region 10" msgstr "Live-Bereich 10 löschen" -#: internal/command/descriptions.go:81 +#: internal/command/descriptions.go:82 msgid "Clear live region 2" msgstr "Live-Bereich 2 löschen" -#: internal/command/descriptions.go:83 +#: internal/command/descriptions.go:84 msgid "Clear live region 3" msgstr "Live-Bereich 3 löschen" -#: internal/command/descriptions.go:85 +#: internal/command/descriptions.go:86 msgid "Clear live region 4" msgstr "Live-Bereich 4 löschen" -#: internal/command/descriptions.go:87 +#: internal/command/descriptions.go:88 msgid "Clear live region 5" msgstr "Live-Bereich 5 löschen" -#: internal/command/descriptions.go:89 +#: internal/command/descriptions.go:90 msgid "Clear live region 6" msgstr "Live-Bereich 6 löschen" -#: internal/command/descriptions.go:91 +#: internal/command/descriptions.go:92 msgid "Clear live region 7" msgstr "Live-Bereich 7 löschen" -#: internal/command/descriptions.go:93 +#: internal/command/descriptions.go:94 msgid "Clear live region 8" msgstr "Live-Bereich 8 löschen" -#: internal/command/descriptions.go:95 +#: internal/command/descriptions.go:96 msgid "Clear live region 9" msgstr "Live-Bereich 9 löschen" @@ -239,7 +256,7 @@ msgstr "Live-Bereich 9 löschen" msgid "Clipboard" msgstr "Zwischenablage" -#: internal/app/application.go:136 internal/app/application.go:139 +#: internal/app/application.go:142 internal/app/application.go:145 #: internal/app/clipboard.go:218 internal/app/clipboard.go:374 msgid "Clipboard import failed" msgstr "Import aus der Zwischenablage fehlgeschlagen" @@ -260,6 +277,10 @@ msgstr "Sarg" msgid "Column number for cursor" msgstr "Spaltennummer des Cursors" +#: internal/applicationoverrides/ninjam/ninjam.go:145 +msgid "Connection, tempo and interval" +msgstr "Verbindung, Tempo und Intervall" + #: internal/emoji/catalog.go:26 msgid "Cool" msgstr "Cool" @@ -268,6 +289,11 @@ 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 +msgid "Could not open Applications: {0}" +msgstr "Anwendungen konnten nicht geöffnet werden: {0}" + #: internal/app/emoji_menu.go:17 internal/app/emoji_menu.go:21 msgid "Could not open Emoji Menu: {0}" msgstr "Emoji-Menü konnte nicht geöffnet werden: {0}" @@ -281,12 +307,17 @@ 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 +msgid "Could not restore application settings: {0}" +msgstr "Anwendungseinstellungen konnten nicht wiederhergestellt werden: {0}" + #: internal/app/spellcheck_menu.go:155 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:164 internal/app/runtime_settings.go:191 +#: internal/app/runtime_settings.go:172 internal/app/runtime_settings.go:199 msgid "Could not save settings: {0}" msgstr "Einstellungen konnten nicht gespeichert werden: {0}" @@ -306,6 +337,19 @@ msgstr "Dolch" msgid "Date format" msgstr "Datumsformat" +#: internal/app/application_overrides.go:230 +msgid "Default" +msgstr "Standard" + +#: internal/app/application_overrides.go:129 +#: internal/app/application_overrides.go:182 +msgid "Default settings restored" +msgstr "Standardeinstellungen wiederhergestellt" + +#: internal/applicationoverrides/ninjam/ninjam.go:262 +msgid "Delete channel" +msgstr "Kanal löschen" + #: internal/emoji/catalog.go:123 msgid "Denmark" msgstr "Dänemark" @@ -392,7 +436,7 @@ msgstr "Anleitungsmodus verlassen. Zum erneuten Aufrufen Fenrir+F1 drücken" msgid "Exploding head" msgstr "Explodierender Kopf" -#: internal/command/descriptions.go:113 +#: internal/command/descriptions.go:114 msgid "Export current fenrir clipboard to X or GUI clipboard" msgstr "" "Aktuelle Fenrir-Zwischenablage in die grafische Zwischenablage exportieren" @@ -401,7 +445,7 @@ msgstr "" msgid "Face with symbols" msgstr "Gesicht mit Symbolen" -#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:151 +#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:155 msgid "Failed to save settings." msgstr "Einstellungen konnten nicht gespeichert werden." @@ -409,7 +453,7 @@ msgstr "Einstellungen konnten nicht gespeichert werden." msgid "Fenrir Menu" msgstr "Fenrir-Menü" -#: internal/app/commands.go:83 +#: internal/app/commands.go:98 msgid "Fenrir screen reader version {0}-{1}" msgstr "Fenrir-Bildschirmleser Version {0}-{1}" @@ -445,7 +489,7 @@ msgstr "Gefaltete Hände" msgid "Food" msgstr "Essen" -#: internal/app/commands.go:126 +#: internal/app/commands.go:145 msgid "Forward next keypress" msgstr "Nächste Taste durchreichen" @@ -525,6 +569,14 @@ msgstr "Einrückung" msgid "India" msgstr "Indien" +#: internal/applicationoverrides/ninjam/ninjam.go:259 +msgid "Input {0}" +msgstr "Eingang {0}" + +#: internal/applicationoverrides/ninjam/ninjam.go:141 +msgid "Interval position {0} of {1}" +msgstr "Intervallposition {0} von {1}" + #: internal/emoji/catalog.go:129 msgid "Ireland" msgstr "Irland" @@ -553,6 +605,10 @@ msgstr "Tastatur" msgid "Last clipboard " msgstr "Letzte Zwischenablage " +#: internal/applicationoverrides/ninjam/ninjam.go:145 +msgid "Latest chat message" +msgstr "Letzte Chatnachricht" + #: internal/emoji/catalog.go:37 msgid "Laughing" msgstr "Lachend" @@ -565,7 +621,7 @@ msgstr "Tastaturbelegung" msgid "Line number for cursor" msgstr "Zeilennummer des Cursors" -#: internal/command/descriptions.go:75 +#: internal/command/descriptions.go:76 msgid "List reading regions" msgstr "Lesebereiche auflisten" @@ -593,10 +649,22 @@ msgstr "Live-Bereich {0}: {1}" msgid "Live regions cleared" msgstr "Live-Bereiche gelöscht" +#: internal/applicationoverrides/ninjam/ninjam.go:232 +msgid "Local channel {0}" +msgstr "Lokaler Kanal {0}" + #: internal/emoji/catalog.go:87 msgid "Mage" msgstr "Magier" +#: internal/applicationoverrides/ninjam/ninjam.go:222 +msgid "Master" +msgstr "Hauptausgang" + +#: internal/applicationoverrides/ninjam/ninjam.go:224 +msgid "Metronome" +msgstr "Metronom" + #: internal/emoji/catalog.go:133 msgid "Mexico" msgstr "Mexiko" @@ -658,6 +726,34 @@ msgstr "Übelkeit" msgid "Netherlands" msgstr "Niederlande" +#: internal/app/application_overrides.go:230 +msgid "NetworkManager" +msgstr "NetworkManager" + +#: internal/applicationoverrides/ninjam/ninjam.go:265 +msgid "New channel" +msgstr "Neuer Kanal" + +#: internal/app/application_overrides.go:230 +msgid "Ninjam" +msgstr "Ninjam" + +#: internal/applicationoverrides/ninjam/ninjam.go:79 +msgid "Ninjam information unavailable" +msgstr "Ninjam-Informationen nicht verfügbar" + +#: internal/applicationoverrides/ninjam/ninjam.go:63 +msgid "" +"Ninjam regions: 1 latest chat message; 2 selected channel or control; 3 " +"connection, tempo and interval" +msgstr "" +"Ninjam-Bereiche: 1 letzte Chatnachricht; 2 ausgewählter Kanal oder " +"Bedienelement; 3 Verbindung, Tempo und Intervall" + +#: internal/applicationoverrides/ninjam/ninjam.go:77 +msgid "No chat message received" +msgstr "Keine Chatnachricht empfangen" + #: internal/interaction/grid.go:43 internal/interaction/grid.go:79 msgid "No matches" msgstr "Keine Treffer" @@ -666,6 +762,10 @@ msgstr "Keine Treffer" msgid "No reading regions set" msgstr "Keine Lesebereiche festgelegt" +#: internal/applicationoverrides/ninjam/ninjam.go:268 +msgid "No remote users" +msgstr "Keine entfernten Benutzer" + #: internal/app/clipboard.go:416 msgid "No text found in clipboard or no accessible display" msgstr "Kein Text in der Zwischenablage oder kein zugängliches Display" @@ -742,18 +842,18 @@ msgstr "Verwendete Fenrir-Version ansagen." msgid "Presentation" msgstr "Ausgabe" -#: internal/command/descriptions.go:100 +#: internal/command/descriptions.go:101 msgid "" "Presents the currently selected text that will be copied to the clipboard" msgstr "" "Gibt den aktuell ausgewählten Text aus, der in die Zwischenablage kopiert " "wird" -#: internal/app/commands.go:112 +#: internal/app/commands.go:131 msgid "Progress monitoring disabled" msgstr "Fortschrittsüberwachung deaktiviert" -#: internal/app/commands.go:110 +#: internal/app/commands.go:129 msgid "Progress monitoring enabled" msgstr "Fortschrittsüberwachung aktiviert" @@ -773,6 +873,10 @@ msgstr "Satzzeichenprofil" msgid "Quick menu not available" msgstr "Schnellmenü nicht verfügbar" +#: internal/applicationoverrides/ninjam/ninjam.go:271 +msgid "Quit Ninjam" +msgstr "Ninjam beenden" + #: internal/emoji/catalog.go:56 msgid "Rainbow" msgstr "Regenbogen" @@ -789,43 +893,43 @@ msgstr "Geschwindigkeit" msgid "Raves" msgstr "Raves" -#: internal/command/descriptions.go:78 +#: internal/command/descriptions.go:79 msgid "Read live region 1" msgstr "Live-Bereich 1 lesen" -#: internal/command/descriptions.go:96 +#: internal/command/descriptions.go:97 msgid "Read live region 10" msgstr "Live-Bereich 10 lesen" -#: internal/command/descriptions.go:80 +#: internal/command/descriptions.go:81 msgid "Read live region 2" msgstr "Live-Bereich 2 lesen" -#: internal/command/descriptions.go:82 +#: internal/command/descriptions.go:83 msgid "Read live region 3" msgstr "Live-Bereich 3 lesen" -#: internal/command/descriptions.go:84 +#: internal/command/descriptions.go:85 msgid "Read live region 4" msgstr "Live-Bereich 4 lesen" -#: internal/command/descriptions.go:86 +#: internal/command/descriptions.go:87 msgid "Read live region 5" msgstr "Live-Bereich 5 lesen" -#: internal/command/descriptions.go:88 +#: internal/command/descriptions.go:89 msgid "Read live region 6" msgstr "Live-Bereich 6 lesen" -#: internal/command/descriptions.go:90 +#: internal/command/descriptions.go:91 msgid "Read live region 7" msgstr "Live-Bereich 7 lesen" -#: internal/command/descriptions.go:92 +#: internal/command/descriptions.go:93 msgid "Read live region 8" msgstr "Live-Bereich 8 lesen" -#: internal/command/descriptions.go:94 +#: internal/command/descriptions.go:95 msgid "Read live region 9" msgstr "Live-Bereich 9 lesen" @@ -853,14 +957,22 @@ msgstr "Liest die Attribute an der aktuellen Cursorposition" msgid "Reads from the top of the screen to the cursor position" msgstr "Liest vom oberen Bildschirmrand bis zur Cursorposition" +#: internal/applicationoverrides/ninjam/ninjam.go:229 +msgid "Remote {0}, channel {1}" +msgstr "Entfernt {0}, Kanal {1}" + #: internal/app/clipboard.go:264 msgid "Remove marks" msgstr "Marken entfernen" -#: internal/command/descriptions.go:99 +#: internal/command/descriptions.go:100 msgid "Removes marks from selected text" msgstr "Entfernt die Markierungen des ausgewählten Textes" +#: internal/applicationoverrides/ninjam/ninjam.go:111 +msgid "Rename channel" +msgstr "Kanal umbenennen" + #: internal/config/config.go:161 msgid "Repeated symbols" msgstr "Wiederholte Symbole" @@ -893,7 +1005,11 @@ msgstr "Traurig" msgid "Santa" msgstr "Weihnachtsmann" -#: internal/command/descriptions.go:64 +#: internal/app/runtime_settings.go:151 internal/app/runtime_settings.go:168 +msgid "Save unavailable." +msgstr "Speichern nicht verfügbar." + +#: internal/command/descriptions.go:65 msgid "Saves your current Fenrir settings so they are the default." msgstr "Speichert die aktuellen Fenrir-Einstellungen als Standard." @@ -913,7 +1029,7 @@ msgstr "„Falsch geschrieben“ ansagen" msgid "Screaming" msgstr "Schreiend" -#: internal/app/commands.go:135 internal/app/scripts.go +#: internal/app/commands.go:154 internal/app/scripts.go msgid "Script failed" msgstr "Skript fehlgeschlagen" @@ -937,15 +1053,23 @@ msgstr "Suche gelöscht" msgid "Search {0}" msgstr "Suche {0}" +#: internal/command/descriptions.go:56 +msgid "Select a temporary application override" +msgstr "Temporäre Anwendungseinstellungen auswählen" + #: internal/config/config.go:121 msgid "Select sound theme" msgstr "Klangthema auswählen" +#: internal/applicationoverrides/ninjam/ninjam.go:145 +msgid "Selected channel or control" +msgstr "Ausgewählter Kanal oder Bedienelement" + #: internal/config/config.go:128 msgid "Serial port" msgstr "Serielle Schnittstelle" -#: internal/command/descriptions.go:74 +#: internal/command/descriptions.go:75 msgid "Set a live region from one or two marks" msgstr "Live-Bereich aus einer oder zwei Markierungen festlegen" @@ -953,6 +1077,11 @@ 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 +msgid "Settings are busy" +msgstr "Einstellungen sind beschäftigt" + #: internal/app/quick_settings.go:298 msgid "Settings saved." msgstr "Einstellungen gespeichert." @@ -1025,7 +1154,7 @@ msgstr "Sprachausgabe aktiviert" msgid "Speech language" msgstr "Ausgabesprache" -#: internal/app/screen_updates.go:72 +#: internal/app/screen_updates.go:80 msgid "Speech restored" msgstr "Sprachausgabe wiederhergestellt" @@ -1117,7 +1246,7 @@ msgstr "Uhrzeit und Datum" msgid "Time format" msgstr "Zeitformat" -#: internal/command/descriptions.go:67 +#: internal/command/descriptions.go:68 msgid "Toggle progress bar monitoring with ascending tones" msgstr "Fortschrittsüberwachung mit ansteigenden Tönen umschalten" @@ -1137,7 +1266,7 @@ msgstr "Baum" msgid "Turkey" msgstr "Truthahn" -#: internal/command/descriptions.go:73 +#: internal/command/descriptions.go:74 msgid "Turn off window mode" msgstr "Fenstermodus ausschalten" @@ -1187,6 +1316,10 @@ msgstr "" msgid "Volume" msgstr "Lautstärke" +#: internal/applicationoverrides/ninjam/ninjam.go:253 +msgid "Volume and pan {0}" +msgstr "Lautstärke und Panorama {0}" + #: internal/punctuation/emoticons.go:32 msgid "WTF?" msgstr "Was zum Teufel?" @@ -1278,11 +1411,11 @@ msgstr "Pfeil rechts" msgid "at" msgstr "At-Zeichen" -#: internal/reader/reader.go:197 +#: internal/reader/reader.go:247 msgid "autoread disabled" msgstr "Automatisches Vorlesen ausgeschaltet" -#: internal/reader/reader.go:195 +#: internal/reader/reader.go:245 msgid "autoread enabled" msgstr "Automatisches Vorlesen eingeschaltet" @@ -1366,7 +1499,7 @@ msgstr "Zeichen {0}" msgid "checked" msgstr "aktiviert" -#: internal/command/descriptions.go:108 +#: internal/command/descriptions.go:109 msgid "clears the currently selected clipboard" msgstr "Löscht die momentan gewählte Zwischenablage" @@ -1400,11 +1533,11 @@ msgstr "verwirrt" msgid "cool" msgstr "cool" -#: internal/command/descriptions.go:102 +#: internal/command/descriptions.go:103 msgid "copies last presented text to the clipboard" msgstr "kopiert den zuletzt ausgegebenen Text in die Zwischenablage" -#: internal/command/descriptions.go:101 +#: internal/command/descriptions.go:102 msgid "copies marked text to the currently selected clipboard" msgstr "Kopiert markierten Text in die gewählte Zwischenablage" @@ -1428,7 +1561,7 @@ msgstr "Aktuelle Zeile" msgid "current word." msgstr "Aktuelles Wort." -#: internal/config/definitions.go:225 internal/reader/reader.go:190 +#: internal/config/definitions.go:225 internal/reader/reader.go:240 msgid "cursor tracking" msgstr "Cursor-Verfolgung" @@ -1453,7 +1586,7 @@ msgstr "Bindestrich" msgid "default" msgstr "Standard" -#: internal/command/descriptions.go:66 +#: internal/command/descriptions.go:67 msgid "disables speech until next keypress" msgstr "Sprachausgabe bis zum nächsten Tastendruck ausschalten" @@ -1471,11 +1604,11 @@ msgstr "Dollar" msgid "dot" msgstr "Punkt" -#: internal/command/descriptions.go:68 +#: internal/command/descriptions.go:69 msgid "enables or disables automatic reading of new text as it appears" msgstr "Automatisches Vorlesen von neuem Text ein- oder ausschalten" -#: internal/command/descriptions.go:65 +#: internal/command/descriptions.go:66 msgid "enables or disables speech" msgstr "Schaltet die Sprachausgabe ein oder aus" @@ -1483,7 +1616,7 @@ msgstr "Schaltet die Sprachausgabe ein oder aus" msgid "enables or disables the announcement of attributes" msgstr "aktiviert oder deaktiviert die Attributansage" -#: internal/command/descriptions.go:69 +#: internal/command/descriptions.go:70 msgid "enables or disables tracking of highlighted text" msgstr "Schaltet die Verfolgung von hervorgehobenem Text ein oder aus" @@ -1512,7 +1645,7 @@ msgstr "Gleichheitszeichen" msgid "evil smile" msgstr "böses Lächeln" -#: internal/command/descriptions.go:71 +#: internal/command/descriptions.go:72 msgid "exits Fenrir" msgstr "Beendet Fenrir" @@ -1520,7 +1653,7 @@ msgstr "Beendet Fenrir" msgid "exits review mode" msgstr "Verlässt die Bildschirmdarstellung" -#: internal/command/descriptions.go:111 +#: internal/command/descriptions.go:112 msgid "export the current fenrir clipboard to a file" msgstr "Aktuelle Fenrir-Zwischenablage in Datei exportieren" @@ -1558,35 +1691,35 @@ msgstr "Stirnrunzeln" msgid "frustrated" msgstr "frustriert" -#: internal/command/descriptions.go:58 +#: internal/command/descriptions.go:59 msgid "get current quick menu entry" msgstr "aktuellen Schnellmenüeintrag ansagen" -#: internal/command/descriptions.go:59 +#: internal/command/descriptions.go:60 msgid "get current quick menu value" msgstr "aktuellen Schnellmenüwert ansagen" -#: internal/command/descriptions.go:56 +#: internal/command/descriptions.go:57 msgid "get next quick menu category" msgstr "nächste Schnellmenükategorie auswählen" -#: internal/command/descriptions.go:60 +#: internal/command/descriptions.go:61 msgid "get next quick menu entry" msgstr "nächsten Schnellmenüeintrag auswählen" -#: internal/command/descriptions.go:62 +#: internal/command/descriptions.go:63 msgid "get next quick menu value" msgstr "nächsten Schnellmenüwert auswählen" -#: internal/command/descriptions.go:57 +#: internal/command/descriptions.go:58 msgid "get previous quick menu category" msgstr "vorherige Schnellmenükategorie auswählen" -#: internal/command/descriptions.go:61 +#: internal/command/descriptions.go:62 msgid "get previous quick menu entry" msgstr "vorherigen Schnellmenüeintrag auswählen" -#: internal/command/descriptions.go:63 +#: internal/command/descriptions.go:64 msgid "get previous quick menu value" msgstr "vorherigen Schnellmenüwert auswählen" @@ -1625,7 +1758,7 @@ msgstr "Kopfzeile festgelegt" msgid "heart" msgstr "Herz" -#: internal/config/definitions.go:225 internal/reader/reader.go:187 +#: internal/config/definitions.go:225 internal/reader/reader.go:237 msgid "highlight tracking" msgstr "Hervorhebungen verfolgen" @@ -1633,11 +1766,11 @@ msgstr "Hervorhebungen verfolgen" msgid "hurray" msgstr "hurra" -#: internal/command/descriptions.go:110 +#: internal/command/descriptions.go:111 msgid "imports text from clipboard file to the clipboard" msgstr "Importiert Text aus Datei in die Zwischenablage" -#: internal/command/descriptions.go:112 +#: internal/command/descriptions.go:113 msgid "imports the graphical clipboard to Fenrir's clipboard" msgstr "importiert die grafische Zwischenablage in die Fenrir-Zwischenablage" @@ -1782,6 +1915,10 @@ msgstr "bewegt den Lesecursor zum vorherigen Zeichen" msgid "moves review to the previous line " msgstr "bewegt den Lesecursor zur vorherigen Zeile " +#: internal/applicationoverrides/ninjam/ninjam.go:239 +msgid "muted" +msgstr "stummgeschaltet" + #: internal/punctuation/emoticons.go:19 msgid "neutral" msgstr "neutral" @@ -1809,6 +1946,14 @@ msgstr "keine Textkonsole" msgid "none" msgstr "keine" +#: internal/applicationoverrides/ninjam/ninjam.go:247 +msgid "not receiving" +msgstr "Empfang aus" + +#: internal/applicationoverrides/ninjam/ninjam.go:242 +msgid "not transmitting" +msgstr "Übertragung aus" + #: internal/punctuation/text_processing.go msgid "number" msgstr "Nummer" @@ -1817,7 +1962,7 @@ msgstr "Nummer" msgid "paste clipboard" msgstr "Zwischenablage einfügen" -#: internal/command/descriptions.go:109 +#: internal/command/descriptions.go:110 msgid "pastes the text from the currently selected clipboard" msgstr "Fügt den Text aus der momentan gewählten Zwischenablage ein" @@ -1826,7 +1971,7 @@ msgstr "Fügt den Text aus der momentan gewählten Zwischenablage ein" msgid "percent" msgstr "Prozent" -#: internal/command/descriptions.go:98 +#: internal/command/descriptions.go:99 msgid "places marks to select text to copy to the clipboard" msgstr "Setzt Marken zum Auswählen und Kopieren von Text in die Zwischenablage" @@ -1885,6 +2030,10 @@ msgstr "Liest vom Cursor bis zum unteren Bildschirmrand" msgid "reads the contents of the current screen" msgstr "Liest den Inhalt des aktuellen Bildschirms vor" +#: internal/applicationoverrides/ninjam/ninjam.go:249 +msgid "receiving" +msgstr "Empfang an" + #: internal/presentation/attributes.go msgid "red" msgstr "rot" @@ -1920,7 +2069,7 @@ msgstr "Zeile {0}, Spalte {1} bis Zeile {2}, Spalte {3}" msgid "screen is empty" msgstr "Bildschirm ist leer" -#: internal/reader/reader.go:132 +#: internal/reader/reader.go:155 msgid "screen {0}" msgstr "Bildschirm {0}" @@ -1928,19 +2077,19 @@ msgstr "Bildschirm {0}" msgid "selected" msgstr "ausgewählt" -#: internal/command/descriptions.go:106 +#: internal/command/descriptions.go:107 msgid "selects the first clipboard" msgstr "Wählt die erste Zwischenablage" -#: internal/command/descriptions.go:107 +#: internal/command/descriptions.go:108 msgid "selects the last clipboard" msgstr "Wählt die letzte Zwischenablage" -#: internal/command/descriptions.go:105 +#: internal/command/descriptions.go:106 msgid "selects the next clipboard" msgstr "Wählt die nächste Zwischenablage" -#: internal/command/descriptions.go:104 +#: internal/command/descriptions.go:105 msgid "selects the previous clipboard" msgstr "Wählt die vorherige Zwischenablage" @@ -1949,11 +2098,11 @@ msgstr "Wählt die vorherige Zwischenablage" msgid "semicolon" msgstr "Semikolon" -#: internal/command/descriptions.go:70 +#: internal/command/descriptions.go:71 msgid "sends the following keypress to the terminal or application" msgstr "sendet den nächsten Tastendruck an das Terminal oder die Anwendung" -#: internal/command/descriptions.go:72 +#: internal/command/descriptions.go:73 msgid "set Window Mode, needs 2 marks " msgstr "Fenstermodus setzen, benötigt 2 Marken " @@ -1997,7 +2146,7 @@ msgstr "einige" msgid "space" msgstr "Leerzeichen" -#: internal/command/descriptions.go:103 +#: internal/command/descriptions.go:104 msgid "speaks the contents of the currently selected clipboard" msgstr "Liest den Inhalt der momentan gewählten Zwischenablage vor" @@ -2009,7 +2158,7 @@ msgstr "Sprache" msgid "speech and beeps" msgstr "Sprache und Signaltöne" -#: internal/app/commands.go:59 internal/app/feedback.go:45 +#: internal/app/commands.go:64 internal/app/feedback.go:45 msgid "speech enabled" msgstr "Sprachausgabe eingeschaltet" @@ -2025,7 +2174,7 @@ msgstr "Tonhöhe" msgid "speech rate" msgstr "Sprechgeschwindigkeit" -#: internal/app/commands.go:121 +#: internal/app/commands.go:140 msgid "speech temporary disabled" msgstr "Sprachausgabe vorübergehend ausgeschaltet" @@ -2066,7 +2215,7 @@ msgstr "überrascht" msgid "tab" msgstr "Tabulator" -#: internal/config/definitions.go:225 internal/reader/reader.go:184 +#: internal/config/definitions.go:225 internal/reader/reader.go:234 msgid "table mode" msgstr "Tabellenmodus" @@ -2083,12 +2232,16 @@ msgstr "Schaltet den Lernmodus um" msgid "tongue out" msgstr "Zunge heraus" +#: internal/applicationoverrides/ninjam/ninjam.go:244 +msgid "transmitting" +msgstr "Übertragung an" + #: internal/interaction/presentation.go:62 msgid "unavailable" msgstr "nicht verfügbar" #: internal/app/tutorial.go:107 internal/app/tutorial.go:161 -#: internal/reader/reader.go:199 internal/review/review.go:140 +#: internal/reader/reader.go:249 internal/review/review.go:140 msgid "unbound" msgstr "unbelegt" @@ -2100,6 +2253,10 @@ msgstr "deaktiviert" msgid "underline" msgstr "unterstrichen" +#: internal/applicationoverrides/ninjam/ninjam.go:237 +msgid "unmuted" +msgstr "nicht stummgeschaltet" + #: internal/presentation/attributes.go internal/presentation/attributes.go:27 msgid "white" msgstr "weiß" @@ -2112,6 +2269,10 @@ msgstr "Zwinkern" msgid "{0} added to dictionary" msgstr "{0} zum Wörterbuch hinzugefügt" +#: internal/app/application_overrides.go:147 +msgid "{0} application override selected" +msgstr "Anwendungseinstellungen für {0} ausgewählt" + #: 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 f9e4f4e2..a6d4774c 100644 --- a/internal/i18n/locale/es/LC_MESSAGES/fenrir.po +++ b/internal/i18n/locale/es/LC_MESSAGES/fenrir.po @@ -67,6 +67,14 @@ msgstr "Enfadado" msgid "Apple" msgstr "Manzana" +#: internal/app/application_overrides.go:237 +msgid "Application" +msgstr "Aplicación" + +#: internal/app/application_overrides.go:236 +msgid "Applications" +msgstr "Aplicaciones" + #: internal/emoji/catalog.go:117 msgid "Argentina" msgstr "Argentina" @@ -155,6 +163,7 @@ msgstr "Mariposa" msgid "Canada" msgstr "Canadá" +#: internal/app/application_overrides.go:238 #: internal/preferences/settings.go:79 msgid "Cancel" msgstr "Cancelar" @@ -167,10 +176,18 @@ msgstr "Gato" msgid "Chains" msgstr "Cadenas" +#: internal/applicationoverrides/ninjam/ninjam.go:256 +msgid "Channel name {0}" +msgstr "Nombre del canal {0}" + #: internal/config/config.go:140 msgid "Character echo" msgstr "Eco de caracteres" +#: internal/applicationoverrides/ninjam/ninjam.go:103 +msgid "Chat message" +msgstr "Mensaje de chat" + #: internal/emoji/catalog.go:102 msgid "Checkmark" msgstr "Marca de verificación" @@ -187,51 +204,51 @@ msgstr "China" msgid "Christmas tree" msgstr "Árbol de Navidad" -#: internal/command/descriptions.go:76 +#: internal/command/descriptions.go:77 msgid "Clear all live regions" msgstr "Borrar todas las regiones dinámicas" -#: internal/command/descriptions.go:77 +#: internal/command/descriptions.go:78 msgid "Clear all reading regions" msgstr "Borrar todas las regiones de lectura" -#: internal/command/descriptions.go:79 +#: internal/command/descriptions.go:80 msgid "Clear live region 1" msgstr "Borrar la región dinámica 1" -#: internal/command/descriptions.go:97 +#: internal/command/descriptions.go:98 msgid "Clear live region 10" msgstr "Borrar la región dinámica 10" -#: internal/command/descriptions.go:81 +#: internal/command/descriptions.go:82 msgid "Clear live region 2" msgstr "Borrar la región dinámica 2" -#: internal/command/descriptions.go:83 +#: internal/command/descriptions.go:84 msgid "Clear live region 3" msgstr "Borrar la región dinámica 3" -#: internal/command/descriptions.go:85 +#: internal/command/descriptions.go:86 msgid "Clear live region 4" msgstr "Borrar la región dinámica 4" -#: internal/command/descriptions.go:87 +#: internal/command/descriptions.go:88 msgid "Clear live region 5" msgstr "Borrar la región dinámica 5" -#: internal/command/descriptions.go:89 +#: internal/command/descriptions.go:90 msgid "Clear live region 6" msgstr "Borrar la región dinámica 6" -#: internal/command/descriptions.go:91 +#: internal/command/descriptions.go:92 msgid "Clear live region 7" msgstr "Borrar la región dinámica 7" -#: internal/command/descriptions.go:93 +#: internal/command/descriptions.go:94 msgid "Clear live region 8" msgstr "Borrar la región dinámica 8" -#: internal/command/descriptions.go:95 +#: internal/command/descriptions.go:96 msgid "Clear live region 9" msgstr "Borrar la región dinámica 9" @@ -239,7 +256,7 @@ msgstr "Borrar la región dinámica 9" msgid "Clipboard" msgstr "Portapapeles" -#: internal/app/application.go:136 internal/app/application.go:139 +#: internal/app/application.go:142 internal/app/application.go:145 #: internal/app/clipboard.go:218 internal/app/clipboard.go:374 msgid "Clipboard import failed" msgstr "Error al importar el portapapeles" @@ -260,6 +277,10 @@ msgstr "Ataúd" msgid "Column number for cursor" msgstr "Número de columna del cursor" +#: internal/applicationoverrides/ninjam/ninjam.go:145 +msgid "Connection, tempo and interval" +msgstr "Conexión, tempo e intervalo" + #: internal/emoji/catalog.go:26 msgid "Cool" msgstr "Genial" @@ -268,6 +289,11 @@ 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 +msgid "Could not open Applications: {0}" +msgstr "No se pudo abrir Aplicaciones: {0}" + #: internal/app/emoji_menu.go:17 internal/app/emoji_menu.go:21 msgid "Could not open Emoji Menu: {0}" msgstr "No se pudo abrir el menú de emojis: {0}" @@ -281,12 +307,17 @@ 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 +msgid "Could not restore application settings: {0}" +msgstr "No se pudo restaurar la configuración de la aplicación: {0}" + #: internal/app/spellcheck_menu.go:155 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:164 internal/app/runtime_settings.go:191 +#: internal/app/runtime_settings.go:172 internal/app/runtime_settings.go:199 msgid "Could not save settings: {0}" msgstr "No se pudo guardar la configuración: {0}" @@ -306,6 +337,19 @@ msgstr "Daga" msgid "Date format" msgstr "Formato de fecha" +#: internal/app/application_overrides.go:230 +msgid "Default" +msgstr "Predeterminado" + +#: internal/app/application_overrides.go:129 +#: internal/app/application_overrides.go:182 +msgid "Default settings restored" +msgstr "Configuración predeterminada restaurada" + +#: internal/applicationoverrides/ninjam/ninjam.go:262 +msgid "Delete channel" +msgstr "Eliminar canal" + #: internal/emoji/catalog.go:123 msgid "Denmark" msgstr "Dinamarca" @@ -393,7 +437,7 @@ msgstr "" msgid "Exploding head" msgstr "Cabeza explotando" -#: internal/command/descriptions.go:113 +#: internal/command/descriptions.go:114 msgid "Export current fenrir clipboard to X or GUI clipboard" msgstr "" "Exportar el portapapeles actual de Fenrir al portapapeles gráfico o de X" @@ -402,7 +446,7 @@ msgstr "" msgid "Face with symbols" msgstr "Cara con símbolos" -#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:151 +#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:155 msgid "Failed to save settings." msgstr "No se pudo guardar la configuración." @@ -410,7 +454,7 @@ msgstr "No se pudo guardar la configuración." msgid "Fenrir Menu" msgstr "Menú de Fenrir" -#: internal/app/commands.go:83 +#: internal/app/commands.go:98 msgid "Fenrir screen reader version {0}-{1}" msgstr "Lector de pantalla Fenrir versión {0}-{1}" @@ -446,7 +490,7 @@ msgstr "Manos juntas" msgid "Food" msgstr "Comida" -#: internal/app/commands.go:126 +#: internal/app/commands.go:145 msgid "Forward next keypress" msgstr "Enviar la siguiente tecla" @@ -526,6 +570,14 @@ msgstr "Sangría" msgid "India" msgstr "India" +#: internal/applicationoverrides/ninjam/ninjam.go:259 +msgid "Input {0}" +msgstr "Entrada {0}" + +#: internal/applicationoverrides/ninjam/ninjam.go:141 +msgid "Interval position {0} of {1}" +msgstr "Posición del intervalo {0} de {1}" + #: internal/emoji/catalog.go:129 msgid "Ireland" msgstr "Irlanda" @@ -554,6 +606,10 @@ msgstr "Teclado" msgid "Last clipboard " msgstr "Último portapapeles " +#: internal/applicationoverrides/ninjam/ninjam.go:145 +msgid "Latest chat message" +msgstr "Último mensaje de chat" + #: internal/emoji/catalog.go:37 msgid "Laughing" msgstr "Riendo" @@ -566,7 +622,7 @@ msgstr "Distribución del teclado" msgid "Line number for cursor" msgstr "Número de línea del cursor" -#: internal/command/descriptions.go:75 +#: internal/command/descriptions.go:76 msgid "List reading regions" msgstr "Listar las regiones de lectura" @@ -594,10 +650,22 @@ msgstr "Región dinámica {0}: {1}" msgid "Live regions cleared" msgstr "Regiones dinámicas borradas" +#: internal/applicationoverrides/ninjam/ninjam.go:232 +msgid "Local channel {0}" +msgstr "Canal local {0}" + #: internal/emoji/catalog.go:87 msgid "Mage" msgstr "Mago" +#: internal/applicationoverrides/ninjam/ninjam.go:222 +msgid "Master" +msgstr "Maestro" + +#: internal/applicationoverrides/ninjam/ninjam.go:224 +msgid "Metronome" +msgstr "Metrónomo" + #: internal/emoji/catalog.go:133 msgid "Mexico" msgstr "México" @@ -666,6 +734,34 @@ msgstr "Náuseas" msgid "Netherlands" msgstr "Países Bajos" +#: internal/app/application_overrides.go:230 +msgid "NetworkManager" +msgstr "NetworkManager" + +#: internal/applicationoverrides/ninjam/ninjam.go:265 +msgid "New channel" +msgstr "Nuevo canal" + +#: internal/app/application_overrides.go:230 +msgid "Ninjam" +msgstr "Ninjam" + +#: internal/applicationoverrides/ninjam/ninjam.go:79 +msgid "Ninjam information unavailable" +msgstr "Información de Ninjam no disponible" + +#: internal/applicationoverrides/ninjam/ninjam.go:63 +msgid "" +"Ninjam regions: 1 latest chat message; 2 selected channel or control; 3 " +"connection, tempo and interval" +msgstr "" +"Regiones de Ninjam: 1 último mensaje de chat; 2 canal o control " +"seleccionado; 3 conexión, tempo e intervalo" + +#: internal/applicationoverrides/ninjam/ninjam.go:77 +msgid "No chat message received" +msgstr "No se ha recibido ningún mensaje de chat" + #: internal/interaction/grid.go:43 internal/interaction/grid.go:79 msgid "No matches" msgstr "Sin coincidencias" @@ -674,6 +770,10 @@ msgstr "Sin coincidencias" msgid "No reading regions set" msgstr "No hay regiones de lectura definidas" +#: internal/applicationoverrides/ninjam/ninjam.go:268 +msgid "No remote users" +msgstr "No hay usuarios remotos" + #: internal/app/clipboard.go:416 msgid "No text found in clipboard or no accessible display" msgstr "" @@ -751,16 +851,16 @@ msgstr "Anunciar la versión de Fenrir en uso." msgid "Presentation" msgstr "Presentación" -#: internal/command/descriptions.go:100 +#: internal/command/descriptions.go:101 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:112 +#: internal/app/commands.go:131 msgid "Progress monitoring disabled" msgstr "Seguimiento del progreso desactivado" -#: internal/app/commands.go:110 +#: internal/app/commands.go:129 msgid "Progress monitoring enabled" msgstr "Seguimiento del progreso activado" @@ -780,6 +880,10 @@ msgstr "Perfil de puntuación" msgid "Quick menu not available" msgstr "Menú rápido no disponible" +#: internal/applicationoverrides/ninjam/ninjam.go:271 +msgid "Quit Ninjam" +msgstr "Salir de Ninjam" + #: internal/emoji/catalog.go:56 msgid "Rainbow" msgstr "Arcoíris" @@ -796,43 +900,43 @@ msgstr "Velocidad" msgid "Raves" msgstr "Fiestas rave" -#: internal/command/descriptions.go:78 +#: internal/command/descriptions.go:79 msgid "Read live region 1" msgstr "Leer la región dinámica 1" -#: internal/command/descriptions.go:96 +#: internal/command/descriptions.go:97 msgid "Read live region 10" msgstr "Leer la región dinámica 10" -#: internal/command/descriptions.go:80 +#: internal/command/descriptions.go:81 msgid "Read live region 2" msgstr "Leer la región dinámica 2" -#: internal/command/descriptions.go:82 +#: internal/command/descriptions.go:83 msgid "Read live region 3" msgstr "Leer la región dinámica 3" -#: internal/command/descriptions.go:84 +#: internal/command/descriptions.go:85 msgid "Read live region 4" msgstr "Leer la región dinámica 4" -#: internal/command/descriptions.go:86 +#: internal/command/descriptions.go:87 msgid "Read live region 5" msgstr "Leer la región dinámica 5" -#: internal/command/descriptions.go:88 +#: internal/command/descriptions.go:89 msgid "Read live region 6" msgstr "Leer la región dinámica 6" -#: internal/command/descriptions.go:90 +#: internal/command/descriptions.go:91 msgid "Read live region 7" msgstr "Leer la región dinámica 7" -#: internal/command/descriptions.go:92 +#: internal/command/descriptions.go:93 msgid "Read live region 8" msgstr "Leer la región dinámica 8" -#: internal/command/descriptions.go:94 +#: internal/command/descriptions.go:95 msgid "Read live region 9" msgstr "Leer la región dinámica 9" @@ -860,14 +964,22 @@ msgstr "Lee los atributos de la posición actual del cursor" msgid "Reads from the top of the screen to the cursor position" msgstr "Lee desde el inicio de la pantalla hasta la posición del cursor." +#: internal/applicationoverrides/ninjam/ninjam.go:229 +msgid "Remote {0}, channel {1}" +msgstr "Remoto {0}, canal {1}" + #: internal/app/clipboard.go:264 msgid "Remove marks" msgstr "Marcas eliminadas" -#: internal/command/descriptions.go:99 +#: internal/command/descriptions.go:100 msgid "Removes marks from selected text" msgstr "Elimina las marcas del texto seleccionado" +#: internal/applicationoverrides/ninjam/ninjam.go:111 +msgid "Rename channel" +msgstr "Cambiar nombre del canal" + #: internal/config/config.go:161 msgid "Repeated symbols" msgstr "Símbolos repetidos" @@ -900,7 +1012,11 @@ msgstr "Triste" msgid "Santa" msgstr "Papá Noel" -#: internal/command/descriptions.go:64 +#: internal/app/runtime_settings.go:151 internal/app/runtime_settings.go:168 +msgid "Save unavailable." +msgstr "Guardado no disponible." + +#: internal/command/descriptions.go:65 msgid "Saves your current Fenrir settings so they are the default." msgstr "Guarda la configuración actual de Fenrir como predeterminada." @@ -920,7 +1036,7 @@ msgstr "Decir «mal escrita»" msgid "Screaming" msgstr "Gritando" -#: internal/app/commands.go:135 internal/app/scripts.go +#: internal/app/commands.go:154 internal/app/scripts.go msgid "Script failed" msgstr "El script falló" @@ -944,15 +1060,23 @@ msgstr "Búsqueda borrada" msgid "Search {0}" msgstr "Buscar {0}" +#: internal/command/descriptions.go:56 +msgid "Select a temporary application override" +msgstr "Seleccionar ajustes temporales para una aplicación" + #: internal/config/config.go:121 msgid "Select sound theme" msgstr "Seleccionar tema de sonido" +#: internal/applicationoverrides/ninjam/ninjam.go:145 +msgid "Selected channel or control" +msgstr "Canal o control seleccionado" + #: internal/config/config.go:128 msgid "Serial port" msgstr "Puerto serie" -#: internal/command/descriptions.go:74 +#: internal/command/descriptions.go:75 msgid "Set a live region from one or two marks" msgstr "Definir una región dinámica a partir de una o dos marcas" @@ -960,6 +1084,11 @@ 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 +msgid "Settings are busy" +msgstr "La configuración está ocupada" + #: internal/app/quick_settings.go:298 msgid "Settings saved." msgstr "Configuración guardada." @@ -1032,7 +1161,7 @@ msgstr "Voz activada" msgid "Speech language" msgstr "Idioma de la voz" -#: internal/app/screen_updates.go:72 +#: internal/app/screen_updates.go:80 msgid "Speech restored" msgstr "Voz restablecida" @@ -1124,7 +1253,7 @@ msgstr "Hora y fecha" msgid "Time format" msgstr "Formato de hora" -#: internal/command/descriptions.go:67 +#: internal/command/descriptions.go:68 msgid "Toggle progress bar monitoring with ascending tones" msgstr "Alternar el seguimiento del progreso con tonos ascendentes" @@ -1144,7 +1273,7 @@ msgstr "Árbol" msgid "Turkey" msgstr "Pavo" -#: internal/command/descriptions.go:73 +#: internal/command/descriptions.go:74 msgid "Turn off window mode" msgstr "Desactivar el modo de ventana" @@ -1192,6 +1321,10 @@ msgstr "Prueba de voz: El veloz zorro marrón salta sobre el perro perezoso." msgid "Volume" msgstr "Volumen" +#: internal/applicationoverrides/ninjam/ninjam.go:253 +msgid "Volume and pan {0}" +msgstr "Volumen y panorámica {0}" + #: internal/punctuation/emoticons.go:32 msgid "WTF?" msgstr "¿Pero qué demonios?" @@ -1283,11 +1416,11 @@ msgstr "flecha derecha" msgid "at" msgstr "arroba" -#: internal/reader/reader.go:197 +#: internal/reader/reader.go:247 msgid "autoread disabled" msgstr "Lectura automática desactivada" -#: internal/reader/reader.go:195 +#: internal/reader/reader.go:245 msgid "autoread enabled" msgstr "Lectura automática activada" @@ -1371,7 +1504,7 @@ msgstr "carácter {0}" msgid "checked" msgstr "marcado" -#: internal/command/descriptions.go:108 +#: internal/command/descriptions.go:109 msgid "clears the currently selected clipboard" msgstr "Vacía el portapapeles seleccionado" @@ -1405,11 +1538,11 @@ msgstr "confundido" msgid "cool" msgstr "genial" -#: internal/command/descriptions.go:102 +#: internal/command/descriptions.go:103 msgid "copies last presented text to the clipboard" msgstr "copia el último texto presentado al portapapeles" -#: internal/command/descriptions.go:101 +#: internal/command/descriptions.go:102 msgid "copies marked text to the currently selected clipboard" msgstr "Copia el texto marcado al portapapeles en uso" @@ -1433,7 +1566,7 @@ msgstr "línea actual" msgid "current word." msgstr "Palabra actual." -#: internal/config/definitions.go:225 internal/reader/reader.go:190 +#: internal/config/definitions.go:225 internal/reader/reader.go:240 msgid "cursor tracking" msgstr "Seguimiento del cursor" @@ -1458,7 +1591,7 @@ msgstr "guion" msgid "default" msgstr "predeterminado" -#: internal/command/descriptions.go:66 +#: internal/command/descriptions.go:67 msgid "disables speech until next keypress" msgstr "Desactiva el habla hasta la siguiente pulsación de teclas" @@ -1476,13 +1609,13 @@ msgstr "dólar" msgid "dot" msgstr "punto" -#: internal/command/descriptions.go:68 +#: internal/command/descriptions.go:69 msgid "enables or disables automatic reading of new text as it appears" msgstr "" "Activa o desactiva la lectura de texto cuando hay nuevo contenido en la " "pantalla" -#: internal/command/descriptions.go:65 +#: internal/command/descriptions.go:66 msgid "enables or disables speech" msgstr "Activa o desactiva el habla" @@ -1490,7 +1623,7 @@ msgstr "Activa o desactiva el habla" msgid "enables or disables the announcement of attributes" msgstr "activa o desactiva el anuncio de atributos" -#: internal/command/descriptions.go:69 +#: internal/command/descriptions.go:70 msgid "enables or disables tracking of highlighted text" msgstr "activa o desactiva el seguimiento del texto resaltado" @@ -1519,7 +1652,7 @@ msgstr "igual" msgid "evil smile" msgstr "sonrisa malvada" -#: internal/command/descriptions.go:71 +#: internal/command/descriptions.go:72 msgid "exits Fenrir" msgstr "Sale de Fenrir" @@ -1527,7 +1660,7 @@ msgstr "Sale de Fenrir" msgid "exits review mode" msgstr "Sale del modo de revisión" -#: internal/command/descriptions.go:111 +#: internal/command/descriptions.go:112 msgid "export the current fenrir clipboard to a file" msgstr "exporta el portapapeles actual de Fenrir a un archivo" @@ -1565,35 +1698,35 @@ msgstr "ceño fruncido" msgid "frustrated" msgstr "frustrado" -#: internal/command/descriptions.go:58 +#: internal/command/descriptions.go:59 msgid "get current quick menu entry" msgstr "anunciar la entrada actual del menú rápido" -#: internal/command/descriptions.go:59 +#: internal/command/descriptions.go:60 msgid "get current quick menu value" msgstr "anunciar el valor actual del menú rápido" -#: internal/command/descriptions.go:56 +#: internal/command/descriptions.go:57 msgid "get next quick menu category" msgstr "seleccionar la siguiente categoría del menú rápido" -#: internal/command/descriptions.go:60 +#: internal/command/descriptions.go:61 msgid "get next quick menu entry" msgstr "seleccionar la siguiente entrada del menú rápido" -#: internal/command/descriptions.go:62 +#: internal/command/descriptions.go:63 msgid "get next quick menu value" msgstr "seleccionar el siguiente valor del menú rápido" -#: internal/command/descriptions.go:57 +#: internal/command/descriptions.go:58 msgid "get previous quick menu category" msgstr "seleccionar la categoría anterior del menú rápido" -#: internal/command/descriptions.go:61 +#: internal/command/descriptions.go:62 msgid "get previous quick menu entry" msgstr "seleccionar la entrada anterior del menú rápido" -#: internal/command/descriptions.go:63 +#: internal/command/descriptions.go:64 msgid "get previous quick menu value" msgstr "seleccionar el valor anterior del menú rápido" @@ -1632,7 +1765,7 @@ msgstr "fila de encabezado definida" msgid "heart" msgstr "corazón" -#: internal/config/definitions.go:225 internal/reader/reader.go:187 +#: internal/config/definitions.go:225 internal/reader/reader.go:237 msgid "highlight tracking" msgstr "Seguimiento del texto resaltado" @@ -1640,11 +1773,11 @@ msgstr "Seguimiento del texto resaltado" msgid "hurray" msgstr "hurra" -#: internal/command/descriptions.go:110 +#: internal/command/descriptions.go:111 msgid "imports text from clipboard file to the clipboard" msgstr "importa texto del archivo del portapapeles al portapapeles" -#: internal/command/descriptions.go:112 +#: internal/command/descriptions.go:113 msgid "imports the graphical clipboard to Fenrir's clipboard" msgstr "importa el portapapeles gráfico al portapapeles de Fenrir" @@ -1789,6 +1922,10 @@ msgstr "mueve el cursor de revisión al carácter anterior" msgid "moves review to the previous line " msgstr "mueve el cursor de revisión a la línea anterior " +#: internal/applicationoverrides/ninjam/ninjam.go:239 +msgid "muted" +msgstr "silenciado" + #: internal/punctuation/emoticons.go:19 msgid "neutral" msgstr "neutral" @@ -1816,6 +1953,14 @@ msgstr "sin consola de texto" msgid "none" msgstr "ninguno" +#: internal/applicationoverrides/ninjam/ninjam.go:247 +msgid "not receiving" +msgstr "sin recibir" + +#: internal/applicationoverrides/ninjam/ninjam.go:242 +msgid "not transmitting" +msgstr "sin transmitir" + #: internal/punctuation/text_processing.go msgid "number" msgstr "número" @@ -1824,7 +1969,7 @@ msgstr "número" msgid "paste clipboard" msgstr "pegar portapapeles" -#: internal/command/descriptions.go:109 +#: internal/command/descriptions.go:110 msgid "pastes the text from the currently selected clipboard" msgstr "Pega el texto del portapapeles en uso" @@ -1833,7 +1978,7 @@ msgstr "Pega el texto del portapapeles en uso" msgid "percent" msgstr "por ciento" -#: internal/command/descriptions.go:98 +#: internal/command/descriptions.go:99 msgid "places marks to select text to copy to the clipboard" msgstr "" "Establece marcas para seleccionar texto que será copiado al portapapeles" @@ -1893,6 +2038,10 @@ msgstr "Lee desde el cursor hasta el final de la pantalla" msgid "reads the contents of the current screen" msgstr "Lee el contenido de la pantalla actual" +#: internal/applicationoverrides/ninjam/ninjam.go:249 +msgid "receiving" +msgstr "recibiendo" + #: internal/presentation/attributes.go msgid "red" msgstr "rojo" @@ -1928,7 +2077,7 @@ msgstr "fila {0}, columna {1} hasta fila {2}, columna {3}" msgid "screen is empty" msgstr "La pantalla está vacía" -#: internal/reader/reader.go:132 +#: internal/reader/reader.go:155 msgid "screen {0}" msgstr "pantalla {0}" @@ -1936,19 +2085,19 @@ msgstr "pantalla {0}" msgid "selected" msgstr "seleccionado" -#: internal/command/descriptions.go:106 +#: internal/command/descriptions.go:107 msgid "selects the first clipboard" msgstr "Selecciona el primer portapapeles" -#: internal/command/descriptions.go:107 +#: internal/command/descriptions.go:108 msgid "selects the last clipboard" msgstr "Selecciona el último portapapeles" -#: internal/command/descriptions.go:105 +#: internal/command/descriptions.go:106 msgid "selects the next clipboard" msgstr "Selecciona el siguiente portapapeles" -#: internal/command/descriptions.go:104 +#: internal/command/descriptions.go:105 msgid "selects the previous clipboard" msgstr "Selecciona el portapapeles anterior" @@ -1957,11 +2106,11 @@ msgstr "Selecciona el portapapeles anterior" msgid "semicolon" msgstr "punto y coma" -#: internal/command/descriptions.go:70 +#: internal/command/descriptions.go:71 msgid "sends the following keypress to the terminal or application" msgstr "envía la siguiente pulsación al terminal o a la aplicación" -#: internal/command/descriptions.go:72 +#: internal/command/descriptions.go:73 msgid "set Window Mode, needs 2 marks " msgstr "se necesitan dos marcas para establecer el modo ventana " @@ -2003,7 +2152,7 @@ msgstr "algunos" msgid "space" msgstr "espacio" -#: internal/command/descriptions.go:103 +#: internal/command/descriptions.go:104 msgid "speaks the contents of the currently selected clipboard" msgstr "Habla el contenido del portapapeles seleccionado" @@ -2015,7 +2164,7 @@ msgstr "voz" msgid "speech and beeps" msgstr "voz y pitidos" -#: internal/app/commands.go:59 internal/app/feedback.go:45 +#: internal/app/commands.go:64 internal/app/feedback.go:45 msgid "speech enabled" msgstr "Habla activada" @@ -2031,7 +2180,7 @@ msgstr "tono de voz" msgid "speech rate" msgstr "velocidad de voz" -#: internal/app/commands.go:121 +#: internal/app/commands.go:140 msgid "speech temporary disabled" msgstr "Habla desactivada temporalmente" @@ -2072,7 +2221,7 @@ msgstr "sorprendido" msgid "tab" msgstr "tabulación" -#: internal/config/definitions.go:225 internal/reader/reader.go:184 +#: internal/config/definitions.go:225 internal/reader/reader.go:234 msgid "table mode" msgstr "modo de tabla" @@ -2089,12 +2238,16 @@ msgstr "alterna el modo de aprendizaje" msgid "tongue out" msgstr "lengua fuera" +#: internal/applicationoverrides/ninjam/ninjam.go:244 +msgid "transmitting" +msgstr "transmitiendo" + #: internal/interaction/presentation.go:62 msgid "unavailable" msgstr "no disponible" #: internal/app/tutorial.go:107 internal/app/tutorial.go:161 -#: internal/reader/reader.go:199 internal/review/review.go:140 +#: internal/reader/reader.go:249 internal/review/review.go:140 msgid "unbound" msgstr "sin asignar" @@ -2106,6 +2259,10 @@ msgstr "no marcado" msgid "underline" msgstr "subrayado" +#: internal/applicationoverrides/ninjam/ninjam.go:237 +msgid "unmuted" +msgstr "no silenciado" + #: internal/presentation/attributes.go internal/presentation/attributes.go:27 msgid "white" msgstr "blanco" @@ -2118,6 +2275,10 @@ msgstr "guiño" msgid "{0} added to dictionary" msgstr "{0} añadido al diccionario" +#: internal/app/application_overrides.go:147 +msgid "{0} application override selected" +msgstr "Ajustes de aplicación seleccionados para {0}" + #: 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 708a0fe5..fe18859e 100644 --- a/internal/i18n/locale/fenrir.pot +++ b/internal/i18n/locale/fenrir.pot @@ -57,6 +57,14 @@ msgstr "" msgid "Apple" msgstr "" +#: internal/app/application_overrides.go:237 +msgid "Application" +msgstr "" + +#: internal/app/application_overrides.go:236 +msgid "Applications" +msgstr "" + #: internal/emoji/catalog.go:117 msgid "Argentina" msgstr "" @@ -145,7 +153,7 @@ msgstr "" msgid "Canada" msgstr "" -#: internal/preferences/settings.go:79 +#: internal/app/application_overrides.go:238 internal/preferences/settings.go:79 msgid "Cancel" msgstr "" @@ -157,10 +165,18 @@ msgstr "" msgid "Chains" msgstr "" +#: internal/applicationoverrides/ninjam/ninjam.go:256 +msgid "Channel name {0}" +msgstr "" + #: internal/config/config.go:140 msgid "Character echo" msgstr "" +#: internal/applicationoverrides/ninjam/ninjam.go:103 +msgid "Chat message" +msgstr "" + #: internal/emoji/catalog.go:102 msgid "Checkmark" msgstr "" @@ -177,51 +193,51 @@ msgstr "" msgid "Christmas tree" msgstr "" -#: internal/command/descriptions.go:76 +#: internal/command/descriptions.go:77 msgid "Clear all live regions" msgstr "" -#: internal/command/descriptions.go:77 +#: internal/command/descriptions.go:78 msgid "Clear all reading regions" msgstr "" -#: internal/command/descriptions.go:79 +#: internal/command/descriptions.go:80 msgid "Clear live region 1" msgstr "" -#: internal/command/descriptions.go:97 +#: internal/command/descriptions.go:98 msgid "Clear live region 10" msgstr "" -#: internal/command/descriptions.go:81 +#: internal/command/descriptions.go:82 msgid "Clear live region 2" msgstr "" -#: internal/command/descriptions.go:83 +#: internal/command/descriptions.go:84 msgid "Clear live region 3" msgstr "" -#: internal/command/descriptions.go:85 +#: internal/command/descriptions.go:86 msgid "Clear live region 4" msgstr "" -#: internal/command/descriptions.go:87 +#: internal/command/descriptions.go:88 msgid "Clear live region 5" msgstr "" -#: internal/command/descriptions.go:89 +#: internal/command/descriptions.go:90 msgid "Clear live region 6" msgstr "" -#: internal/command/descriptions.go:91 +#: internal/command/descriptions.go:92 msgid "Clear live region 7" msgstr "" -#: internal/command/descriptions.go:93 +#: internal/command/descriptions.go:94 msgid "Clear live region 8" msgstr "" -#: internal/command/descriptions.go:95 +#: internal/command/descriptions.go:96 msgid "Clear live region 9" msgstr "" @@ -229,7 +245,7 @@ msgstr "" msgid "Clipboard" msgstr "" -#: internal/app/application.go:136 internal/app/application.go:139 internal/app/clipboard.go:218 internal/app/clipboard.go:374 +#: internal/app/application.go:142 internal/app/application.go:145 internal/app/clipboard.go:218 internal/app/clipboard.go:374 msgid "Clipboard import failed" msgstr "" @@ -249,6 +265,10 @@ msgstr "" msgid "Column number for cursor" msgstr "" +#: internal/applicationoverrides/ninjam/ninjam.go:145 +msgid "Connection, tempo and interval" +msgstr "" + #: internal/emoji/catalog.go:26 msgid "Cool" msgstr "" @@ -257,6 +277,10 @@ msgstr "" msgid "Could not check spelling: {0}" msgstr "" +#: internal/app/application_overrides.go:241 internal/app/application_overrides.go:245 +msgid "Could not open Applications: {0}" +msgstr "" + #: internal/app/emoji_menu.go:17 internal/app/emoji_menu.go:21 msgid "Could not open Emoji Menu: {0}" msgstr "" @@ -269,11 +293,15 @@ msgstr "" msgid "Could not open Spellcheck Menu: {0}" msgstr "" +#: internal/app/application_overrides.go:104 internal/app/application_overrides.go:208 +msgid "Could not restore application settings: {0}" +msgstr "" + #: internal/app/spellcheck_menu.go:155 msgid "Could not save exceptions: {0}" msgstr "" -#: internal/app/fenrir_menu.go:174 internal/app/fenrir_menu.go:180 internal/app/runtime_settings.go:164 internal/app/runtime_settings.go:191 +#: internal/app/fenrir_menu.go:174 internal/app/fenrir_menu.go:180 internal/app/runtime_settings.go:172 internal/app/runtime_settings.go:199 msgid "Could not save settings: {0}" msgstr "" @@ -293,6 +321,18 @@ msgstr "" msgid "Date format" msgstr "" +#: internal/app/application_overrides.go:230 +msgid "Default" +msgstr "" + +#: internal/app/application_overrides.go:129 internal/app/application_overrides.go:182 +msgid "Default settings restored" +msgstr "" + +#: internal/applicationoverrides/ninjam/ninjam.go:262 +msgid "Delete channel" +msgstr "" + #: internal/emoji/catalog.go:123 msgid "Denmark" msgstr "" @@ -369,7 +409,7 @@ msgstr "" msgid "Exploding head" msgstr "" -#: internal/command/descriptions.go:113 +#: internal/command/descriptions.go:114 msgid "Export current fenrir clipboard to X or GUI clipboard" msgstr "" @@ -377,7 +417,7 @@ msgstr "" msgid "Face with symbols" msgstr "" -#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:151 +#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:155 msgid "Failed to save settings." msgstr "" @@ -385,7 +425,7 @@ msgstr "" msgid "Fenrir Menu" msgstr "" -#: internal/app/commands.go:83 +#: internal/app/commands.go:98 msgid "Fenrir screen reader version {0}-{1}" msgstr "" @@ -421,7 +461,7 @@ msgstr "" msgid "Food" msgstr "" -#: internal/app/commands.go:126 +#: internal/app/commands.go:145 msgid "Forward next keypress" msgstr "" @@ -501,6 +541,14 @@ msgstr "" msgid "India" msgstr "" +#: internal/applicationoverrides/ninjam/ninjam.go:259 +msgid "Input {0}" +msgstr "" + +#: internal/applicationoverrides/ninjam/ninjam.go:141 +msgid "Interval position {0} of {1}" +msgstr "" + #: internal/emoji/catalog.go:129 msgid "Ireland" msgstr "" @@ -529,6 +577,10 @@ msgstr "" msgid "Last clipboard " msgstr "" +#: internal/applicationoverrides/ninjam/ninjam.go:145 +msgid "Latest chat message" +msgstr "" + #: internal/emoji/catalog.go:37 msgid "Laughing" msgstr "" @@ -541,7 +593,7 @@ msgstr "" msgid "Line number for cursor" msgstr "" -#: internal/command/descriptions.go:75 +#: internal/command/descriptions.go:76 msgid "List reading regions" msgstr "" @@ -569,10 +621,22 @@ msgstr "" msgid "Live regions cleared" msgstr "" +#: internal/applicationoverrides/ninjam/ninjam.go:232 +msgid "Local channel {0}" +msgstr "" + #: internal/emoji/catalog.go:87 msgid "Mage" msgstr "" +#: internal/applicationoverrides/ninjam/ninjam.go:222 +msgid "Master" +msgstr "" + +#: internal/applicationoverrides/ninjam/ninjam.go:224 +msgid "Metronome" +msgstr "" + #: internal/emoji/catalog.go:133 msgid "Mexico" msgstr "" @@ -633,6 +697,30 @@ msgstr "" msgid "Netherlands" msgstr "" +#: internal/app/application_overrides.go:230 +msgid "NetworkManager" +msgstr "" + +#: internal/applicationoverrides/ninjam/ninjam.go:265 +msgid "New channel" +msgstr "" + +#: internal/app/application_overrides.go:230 +msgid "Ninjam" +msgstr "" + +#: internal/applicationoverrides/ninjam/ninjam.go:79 +msgid "Ninjam information unavailable" +msgstr "" + +#: internal/applicationoverrides/ninjam/ninjam.go:63 +msgid "Ninjam regions: 1 latest chat message; 2 selected channel or control; 3 connection, tempo and interval" +msgstr "" + +#: internal/applicationoverrides/ninjam/ninjam.go:77 +msgid "No chat message received" +msgstr "" + #: internal/interaction/grid.go:43 internal/interaction/grid.go:79 msgid "No matches" msgstr "" @@ -641,6 +729,10 @@ msgstr "" msgid "No reading regions set" msgstr "" +#: internal/applicationoverrides/ninjam/ninjam.go:268 +msgid "No remote users" +msgstr "" + #: internal/app/clipboard.go:416 msgid "No text found in clipboard or no accessible display" msgstr "" @@ -717,15 +809,15 @@ msgstr "" msgid "Presentation" msgstr "" -#: internal/command/descriptions.go:100 +#: internal/command/descriptions.go:101 msgid "Presents the currently selected text that will be copied to the clipboard" msgstr "" -#: internal/app/commands.go:112 +#: internal/app/commands.go:131 msgid "Progress monitoring disabled" msgstr "" -#: internal/app/commands.go:110 +#: internal/app/commands.go:129 msgid "Progress monitoring enabled" msgstr "" @@ -745,6 +837,10 @@ msgstr "" msgid "Quick menu not available" msgstr "" +#: internal/applicationoverrides/ninjam/ninjam.go:271 +msgid "Quit Ninjam" +msgstr "" + #: internal/emoji/catalog.go:56 msgid "Rainbow" msgstr "" @@ -761,43 +857,43 @@ msgstr "" msgid "Raves" msgstr "" -#: internal/command/descriptions.go:78 +#: internal/command/descriptions.go:79 msgid "Read live region 1" msgstr "" -#: internal/command/descriptions.go:96 +#: internal/command/descriptions.go:97 msgid "Read live region 10" msgstr "" -#: internal/command/descriptions.go:80 +#: internal/command/descriptions.go:81 msgid "Read live region 2" msgstr "" -#: internal/command/descriptions.go:82 +#: internal/command/descriptions.go:83 msgid "Read live region 3" msgstr "" -#: internal/command/descriptions.go:84 +#: internal/command/descriptions.go:85 msgid "Read live region 4" msgstr "" -#: internal/command/descriptions.go:86 +#: internal/command/descriptions.go:87 msgid "Read live region 5" msgstr "" -#: internal/command/descriptions.go:88 +#: internal/command/descriptions.go:89 msgid "Read live region 6" msgstr "" -#: internal/command/descriptions.go:90 +#: internal/command/descriptions.go:91 msgid "Read live region 7" msgstr "" -#: internal/command/descriptions.go:92 +#: internal/command/descriptions.go:93 msgid "Read live region 8" msgstr "" -#: internal/command/descriptions.go:94 +#: internal/command/descriptions.go:95 msgid "Read live region 9" msgstr "" @@ -825,14 +921,22 @@ msgstr "" msgid "Reads from the top of the screen to the cursor position" msgstr "" +#: internal/applicationoverrides/ninjam/ninjam.go:229 +msgid "Remote {0}, channel {1}" +msgstr "" + #: internal/app/clipboard.go:264 msgid "Remove marks" msgstr "" -#: internal/command/descriptions.go:99 +#: internal/command/descriptions.go:100 msgid "Removes marks from selected text" msgstr "" +#: internal/applicationoverrides/ninjam/ninjam.go:111 +msgid "Rename channel" +msgstr "" + #: internal/config/config.go:161 msgid "Repeated symbols" msgstr "" @@ -865,7 +969,11 @@ msgstr "" msgid "Santa" msgstr "" -#: internal/command/descriptions.go:64 +#: internal/app/runtime_settings.go:151 internal/app/runtime_settings.go:168 +msgid "Save unavailable." +msgstr "" + +#: internal/command/descriptions.go:65 msgid "Saves your current Fenrir settings so they are the default." msgstr "" @@ -885,7 +993,7 @@ msgstr "" msgid "Screaming" msgstr "" -#: internal/app/commands.go:135 internal/app/scripts.go +#: internal/app/commands.go:154 internal/app/scripts.go msgid "Script failed" msgstr "" @@ -909,15 +1017,23 @@ msgstr "" msgid "Search {0}" msgstr "" +#: internal/command/descriptions.go:56 +msgid "Select a temporary application override" +msgstr "" + #: internal/config/config.go:121 msgid "Select sound theme" msgstr "" +#: internal/applicationoverrides/ninjam/ninjam.go:145 +msgid "Selected channel or control" +msgstr "" + #: internal/config/config.go:128 msgid "Serial port" msgstr "" -#: internal/command/descriptions.go:74 +#: internal/command/descriptions.go:75 msgid "Set a live region from one or two marks" msgstr "" @@ -925,6 +1041,10 @@ 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 +msgid "Settings are busy" +msgstr "" + #: internal/app/quick_settings.go:298 msgid "Settings saved." msgstr "" @@ -997,7 +1117,7 @@ msgstr "" msgid "Speech language" msgstr "" -#: internal/app/screen_updates.go:72 +#: internal/app/screen_updates.go:80 msgid "Speech restored" msgstr "" @@ -1089,7 +1209,7 @@ msgstr "" msgid "Time format" msgstr "" -#: internal/command/descriptions.go:67 +#: internal/command/descriptions.go:68 msgid "Toggle progress bar monitoring with ascending tones" msgstr "" @@ -1109,7 +1229,7 @@ msgstr "" msgid "Turkey" msgstr "" -#: internal/command/descriptions.go:73 +#: internal/command/descriptions.go:74 msgid "Turn off window mode" msgstr "" @@ -1153,6 +1273,10 @@ msgstr "" msgid "Volume" msgstr "" +#: internal/applicationoverrides/ninjam/ninjam.go:253 +msgid "Volume and pan {0}" +msgstr "" + #: internal/punctuation/emoticons.go:32 msgid "WTF?" msgstr "" @@ -1237,11 +1361,11 @@ msgstr "" msgid "at" msgstr "" -#: internal/reader/reader.go:197 +#: internal/reader/reader.go:247 msgid "autoread disabled" msgstr "" -#: internal/reader/reader.go:195 +#: internal/reader/reader.go:245 msgid "autoread enabled" msgstr "" @@ -1317,7 +1441,7 @@ msgstr "" msgid "checked" msgstr "" -#: internal/command/descriptions.go:108 +#: internal/command/descriptions.go:109 msgid "clears the currently selected clipboard" msgstr "" @@ -1349,11 +1473,11 @@ msgstr "" msgid "cool" msgstr "" -#: internal/command/descriptions.go:102 +#: internal/command/descriptions.go:103 msgid "copies last presented text to the clipboard" msgstr "" -#: internal/command/descriptions.go:101 +#: internal/command/descriptions.go:102 msgid "copies marked text to the currently selected clipboard" msgstr "" @@ -1377,7 +1501,7 @@ msgstr "" msgid "current word." msgstr "" -#: internal/config/definitions.go:225 internal/reader/reader.go:190 +#: internal/config/definitions.go:225 internal/reader/reader.go:240 msgid "cursor tracking" msgstr "" @@ -1397,7 +1521,7 @@ msgstr "" msgid "default" msgstr "" -#: internal/command/descriptions.go:66 +#: internal/command/descriptions.go:67 msgid "disables speech until next keypress" msgstr "" @@ -1413,11 +1537,11 @@ msgstr "" msgid "dot" msgstr "" -#: internal/command/descriptions.go:68 +#: internal/command/descriptions.go:69 msgid "enables or disables automatic reading of new text as it appears" msgstr "" -#: internal/command/descriptions.go:65 +#: internal/command/descriptions.go:66 msgid "enables or disables speech" msgstr "" @@ -1425,7 +1549,7 @@ msgstr "" msgid "enables or disables the announcement of attributes" msgstr "" -#: internal/command/descriptions.go:69 +#: internal/command/descriptions.go:70 msgid "enables or disables tracking of highlighted text" msgstr "" @@ -1453,7 +1577,7 @@ msgstr "" msgid "evil smile" msgstr "" -#: internal/command/descriptions.go:71 +#: internal/command/descriptions.go:72 msgid "exits Fenrir" msgstr "" @@ -1461,7 +1585,7 @@ msgstr "" msgid "exits review mode" msgstr "" -#: internal/command/descriptions.go:111 +#: internal/command/descriptions.go:112 msgid "export the current fenrir clipboard to a file" msgstr "" @@ -1497,35 +1621,35 @@ msgstr "" msgid "frustrated" msgstr "" -#: internal/command/descriptions.go:58 +#: internal/command/descriptions.go:59 msgid "get current quick menu entry" msgstr "" -#: internal/command/descriptions.go:59 +#: internal/command/descriptions.go:60 msgid "get current quick menu value" msgstr "" -#: internal/command/descriptions.go:56 +#: internal/command/descriptions.go:57 msgid "get next quick menu category" msgstr "" -#: internal/command/descriptions.go:60 +#: internal/command/descriptions.go:61 msgid "get next quick menu entry" msgstr "" -#: internal/command/descriptions.go:62 +#: internal/command/descriptions.go:63 msgid "get next quick menu value" msgstr "" -#: internal/command/descriptions.go:57 +#: internal/command/descriptions.go:58 msgid "get previous quick menu category" msgstr "" -#: internal/command/descriptions.go:61 +#: internal/command/descriptions.go:62 msgid "get previous quick menu entry" msgstr "" -#: internal/command/descriptions.go:63 +#: internal/command/descriptions.go:64 msgid "get previous quick menu value" msgstr "" @@ -1561,7 +1685,7 @@ msgstr "" msgid "heart" msgstr "" -#: internal/config/definitions.go:225 internal/reader/reader.go:187 +#: internal/config/definitions.go:225 internal/reader/reader.go:237 msgid "highlight tracking" msgstr "" @@ -1569,11 +1693,11 @@ msgstr "" msgid "hurray" msgstr "" -#: internal/command/descriptions.go:110 +#: internal/command/descriptions.go:111 msgid "imports text from clipboard file to the clipboard" msgstr "" -#: internal/command/descriptions.go:112 +#: internal/command/descriptions.go:113 msgid "imports the graphical clipboard to Fenrir's clipboard" msgstr "" @@ -1713,6 +1837,10 @@ msgstr "" msgid "moves review to the previous line " msgstr "" +#: internal/applicationoverrides/ninjam/ninjam.go:239 +msgid "muted" +msgstr "" + #: internal/punctuation/emoticons.go:19 msgid "neutral" msgstr "" @@ -1737,6 +1865,14 @@ msgstr "" msgid "none" msgstr "" +#: internal/applicationoverrides/ninjam/ninjam.go:247 +msgid "not receiving" +msgstr "" + +#: internal/applicationoverrides/ninjam/ninjam.go:242 +msgid "not transmitting" +msgstr "" + #: internal/punctuation/text_processing.go msgid "number" msgstr "" @@ -1745,7 +1881,7 @@ msgstr "" msgid "paste clipboard" msgstr "" -#: internal/command/descriptions.go:109 +#: internal/command/descriptions.go:110 msgid "pastes the text from the currently selected clipboard" msgstr "" @@ -1753,7 +1889,7 @@ msgstr "" msgid "percent" msgstr "" -#: internal/command/descriptions.go:98 +#: internal/command/descriptions.go:99 msgid "places marks to select text to copy to the clipboard" msgstr "" @@ -1801,6 +1937,10 @@ msgstr "" msgid "reads the contents of the current screen" msgstr "" +#: internal/applicationoverrides/ninjam/ninjam.go:249 +msgid "receiving" +msgstr "" + #: internal/presentation/attributes.go msgid "red" msgstr "" @@ -1833,7 +1973,7 @@ msgstr "" msgid "screen is empty" msgstr "" -#: internal/reader/reader.go:132 +#: internal/reader/reader.go:155 msgid "screen {0}" msgstr "" @@ -1841,19 +1981,19 @@ msgstr "" msgid "selected" msgstr "" -#: internal/command/descriptions.go:106 +#: internal/command/descriptions.go:107 msgid "selects the first clipboard" msgstr "" -#: internal/command/descriptions.go:107 +#: internal/command/descriptions.go:108 msgid "selects the last clipboard" msgstr "" -#: internal/command/descriptions.go:105 +#: internal/command/descriptions.go:106 msgid "selects the next clipboard" msgstr "" -#: internal/command/descriptions.go:104 +#: internal/command/descriptions.go:105 msgid "selects the previous clipboard" msgstr "" @@ -1861,11 +2001,11 @@ msgstr "" msgid "semicolon" msgstr "" -#: internal/command/descriptions.go:70 +#: internal/command/descriptions.go:71 msgid "sends the following keypress to the terminal or application" msgstr "" -#: internal/command/descriptions.go:72 +#: internal/command/descriptions.go:73 msgid "set Window Mode, needs 2 marks " msgstr "" @@ -1905,7 +2045,7 @@ msgstr "" msgid "space" msgstr "" -#: internal/command/descriptions.go:103 +#: internal/command/descriptions.go:104 msgid "speaks the contents of the currently selected clipboard" msgstr "" @@ -1917,7 +2057,7 @@ msgstr "" msgid "speech and beeps" msgstr "" -#: internal/app/commands.go:59 internal/app/feedback.go:45 +#: internal/app/commands.go:64 internal/app/feedback.go:45 msgid "speech enabled" msgstr "" @@ -1933,7 +2073,7 @@ msgstr "" msgid "speech rate" msgstr "" -#: internal/app/commands.go:121 +#: internal/app/commands.go:140 msgid "speech temporary disabled" msgstr "" @@ -1973,7 +2113,7 @@ msgstr "" msgid "tab" msgstr "" -#: internal/config/definitions.go:225 internal/reader/reader.go:184 +#: internal/config/definitions.go:225 internal/reader/reader.go:234 msgid "table mode" msgstr "" @@ -1989,11 +2129,15 @@ msgstr "" msgid "tongue out" msgstr "" +#: internal/applicationoverrides/ninjam/ninjam.go:244 +msgid "transmitting" +msgstr "" + #: internal/interaction/presentation.go:62 msgid "unavailable" msgstr "" -#: internal/app/tutorial.go:107 internal/app/tutorial.go:161 internal/reader/reader.go:199 internal/review/review.go:140 +#: internal/app/tutorial.go:107 internal/app/tutorial.go:161 internal/reader/reader.go:249 internal/review/review.go:140 msgid "unbound" msgstr "" @@ -2005,6 +2149,10 @@ msgstr "" msgid "underline" msgstr "" +#: internal/applicationoverrides/ninjam/ninjam.go:237 +msgid "unmuted" +msgstr "" + #: internal/presentation/attributes.go internal/presentation/attributes.go:27 msgid "white" msgstr "" @@ -2017,6 +2165,10 @@ msgstr "" msgid "{0} added to dictionary" msgstr "" +#: internal/app/application_overrides.go:147 +msgid "{0} application override selected" +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 0a911671..37daed7a 100644 --- a/internal/i18n/locale/pl/LC_MESSAGES/fenrir.po +++ b/internal/i18n/locale/pl/LC_MESSAGES/fenrir.po @@ -68,6 +68,14 @@ msgstr "Złość" msgid "Apple" msgstr "Jabłko" +#: internal/app/application_overrides.go:237 +msgid "Application" +msgstr "Aplikacja" + +#: internal/app/application_overrides.go:236 +msgid "Applications" +msgstr "Aplikacje" + #: internal/emoji/catalog.go:117 msgid "Argentina" msgstr "Argentyna" @@ -156,6 +164,7 @@ msgstr "Motyl" msgid "Canada" msgstr "Kanada" +#: internal/app/application_overrides.go:238 #: internal/preferences/settings.go:79 msgid "Cancel" msgstr "Anuluj" @@ -168,10 +177,18 @@ msgstr "Kot" msgid "Chains" msgstr "Łańcuchy" +#: internal/applicationoverrides/ninjam/ninjam.go:256 +msgid "Channel name {0}" +msgstr "Nazwa kanału {0}" + #: internal/config/config.go:140 msgid "Character echo" msgstr "Echo znaków" +#: internal/applicationoverrides/ninjam/ninjam.go:103 +msgid "Chat message" +msgstr "Wiadomość czatu" + #: internal/emoji/catalog.go:102 msgid "Checkmark" msgstr "Znacznik wyboru" @@ -188,51 +205,51 @@ msgstr "Chiny" msgid "Christmas tree" msgstr "Choinka" -#: internal/command/descriptions.go:76 +#: internal/command/descriptions.go:77 msgid "Clear all live regions" msgstr "Wyczyść wszystkie obszary dynamiczne" -#: internal/command/descriptions.go:77 +#: internal/command/descriptions.go:78 msgid "Clear all reading regions" msgstr "Wyczyść wszystkie obszary czytania" -#: internal/command/descriptions.go:79 +#: internal/command/descriptions.go:80 msgid "Clear live region 1" msgstr "Wyczyść obszar dynamiczny 1" -#: internal/command/descriptions.go:97 +#: internal/command/descriptions.go:98 msgid "Clear live region 10" msgstr "Wyczyść obszar dynamiczny 10" -#: internal/command/descriptions.go:81 +#: internal/command/descriptions.go:82 msgid "Clear live region 2" msgstr "Wyczyść obszar dynamiczny 2" -#: internal/command/descriptions.go:83 +#: internal/command/descriptions.go:84 msgid "Clear live region 3" msgstr "Wyczyść obszar dynamiczny 3" -#: internal/command/descriptions.go:85 +#: internal/command/descriptions.go:86 msgid "Clear live region 4" msgstr "Wyczyść obszar dynamiczny 4" -#: internal/command/descriptions.go:87 +#: internal/command/descriptions.go:88 msgid "Clear live region 5" msgstr "Wyczyść obszar dynamiczny 5" -#: internal/command/descriptions.go:89 +#: internal/command/descriptions.go:90 msgid "Clear live region 6" msgstr "Wyczyść obszar dynamiczny 6" -#: internal/command/descriptions.go:91 +#: internal/command/descriptions.go:92 msgid "Clear live region 7" msgstr "Wyczyść obszar dynamiczny 7" -#: internal/command/descriptions.go:93 +#: internal/command/descriptions.go:94 msgid "Clear live region 8" msgstr "Wyczyść obszar dynamiczny 8" -#: internal/command/descriptions.go:95 +#: internal/command/descriptions.go:96 msgid "Clear live region 9" msgstr "Wyczyść obszar dynamiczny 9" @@ -240,7 +257,7 @@ msgstr "Wyczyść obszar dynamiczny 9" msgid "Clipboard" msgstr "Schowek" -#: internal/app/application.go:136 internal/app/application.go:139 +#: internal/app/application.go:142 internal/app/application.go:145 #: internal/app/clipboard.go:218 internal/app/clipboard.go:374 msgid "Clipboard import failed" msgstr "Importowanie schowka nie powiodło się" @@ -261,6 +278,10 @@ msgstr "Trumna" msgid "Column number for cursor" msgstr "Numer kolumny kursora" +#: internal/applicationoverrides/ninjam/ninjam.go:145 +msgid "Connection, tempo and interval" +msgstr "Połączenie, tempo i interwał" + #: internal/emoji/catalog.go:26 msgid "Cool" msgstr "Wyluzowanie" @@ -269,6 +290,11 @@ 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 +msgid "Could not open Applications: {0}" +msgstr "Nie można otworzyć menu aplikacji: {0}" + #: internal/app/emoji_menu.go:17 internal/app/emoji_menu.go:21 msgid "Could not open Emoji Menu: {0}" msgstr "Nie można otworzyć menu emoji: {0}" @@ -282,12 +308,17 @@ 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 +msgid "Could not restore application settings: {0}" +msgstr "Nie można przywrócić ustawień aplikacji: {0}" + #: internal/app/spellcheck_menu.go:155 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:164 internal/app/runtime_settings.go:191 +#: internal/app/runtime_settings.go:172 internal/app/runtime_settings.go:199 msgid "Could not save settings: {0}" msgstr "Nie można zapisać ustawień: {0}" @@ -307,6 +338,19 @@ msgstr "Sztylet" msgid "Date format" msgstr "Format daty" +#: internal/app/application_overrides.go:230 +msgid "Default" +msgstr "Domyślne" + +#: internal/app/application_overrides.go:129 +#: internal/app/application_overrides.go:182 +msgid "Default settings restored" +msgstr "Przywrócono ustawienia domyślne" + +#: internal/applicationoverrides/ninjam/ninjam.go:262 +msgid "Delete channel" +msgstr "Usuń kanał" + #: internal/emoji/catalog.go:123 msgid "Denmark" msgstr "Dania" @@ -393,7 +437,7 @@ msgstr "Wyjście z trybu nauki. Aby ponownie go włączyć, naciśnij Fenrir+F1" msgid "Exploding head" msgstr "Eksplodująca głowa" -#: internal/command/descriptions.go:113 +#: internal/command/descriptions.go:114 msgid "Export current fenrir clipboard to X or GUI clipboard" msgstr "" "Eksportuj bieżący schowek Fenrira do schowka X lub interfejsu graficznego" @@ -402,7 +446,7 @@ msgstr "" msgid "Face with symbols" msgstr "Twarz z symbolami" -#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:151 +#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:155 msgid "Failed to save settings." msgstr "Nie udało się zapisać ustawień." @@ -410,7 +454,7 @@ msgstr "Nie udało się zapisać ustawień." msgid "Fenrir Menu" msgstr "Menu Fenrira" -#: internal/app/commands.go:83 +#: internal/app/commands.go:98 msgid "Fenrir screen reader version {0}-{1}" msgstr "Czytnik ekranu Fenrir, wersja {0}-{1}" @@ -446,7 +490,7 @@ msgstr "Złożone dłonie" msgid "Food" msgstr "Jedzenie" -#: internal/app/commands.go:126 +#: internal/app/commands.go:145 msgid "Forward next keypress" msgstr "Przepuść następny klawisz" @@ -526,6 +570,14 @@ msgstr "Wcięcia" msgid "India" msgstr "Indie" +#: internal/applicationoverrides/ninjam/ninjam.go:259 +msgid "Input {0}" +msgstr "Wejście {0}" + +#: internal/applicationoverrides/ninjam/ninjam.go:141 +msgid "Interval position {0} of {1}" +msgstr "Pozycja interwału {0} z {1}" + #: internal/emoji/catalog.go:129 msgid "Ireland" msgstr "Irlandia" @@ -554,6 +606,10 @@ msgstr "Klawiatura" msgid "Last clipboard " msgstr "Ostatni showek " +#: internal/applicationoverrides/ninjam/ninjam.go:145 +msgid "Latest chat message" +msgstr "Ostatnia wiadomość czatu" + #: internal/emoji/catalog.go:37 msgid "Laughing" msgstr "Śmiech" @@ -566,7 +622,7 @@ msgstr "Układ klawiatury" msgid "Line number for cursor" msgstr "Numer wiersza kursora" -#: internal/command/descriptions.go:75 +#: internal/command/descriptions.go:76 msgid "List reading regions" msgstr "Wyświetl obszary czytania" @@ -594,10 +650,22 @@ msgstr "Obszar dynamiczny {0}: {1}" msgid "Live regions cleared" msgstr "Wyczyszczono obszary dynamiczne" +#: internal/applicationoverrides/ninjam/ninjam.go:232 +msgid "Local channel {0}" +msgstr "Kanał lokalny {0}" + #: internal/emoji/catalog.go:87 msgid "Mage" msgstr "Mag" +#: internal/applicationoverrides/ninjam/ninjam.go:222 +msgid "Master" +msgstr "Główne wyjście" + +#: internal/applicationoverrides/ninjam/ninjam.go:224 +msgid "Metronome" +msgstr "Metronom" + #: internal/emoji/catalog.go:133 msgid "Mexico" msgstr "Meksyk" @@ -662,6 +730,34 @@ msgstr "Nudności" msgid "Netherlands" msgstr "Holandia" +#: internal/app/application_overrides.go:230 +msgid "NetworkManager" +msgstr "NetworkManager" + +#: internal/applicationoverrides/ninjam/ninjam.go:265 +msgid "New channel" +msgstr "Nowy kanał" + +#: internal/app/application_overrides.go:230 +msgid "Ninjam" +msgstr "Ninjam" + +#: internal/applicationoverrides/ninjam/ninjam.go:79 +msgid "Ninjam information unavailable" +msgstr "Informacje o Ninjam są niedostępne" + +#: internal/applicationoverrides/ninjam/ninjam.go:63 +msgid "" +"Ninjam regions: 1 latest chat message; 2 selected channel or control; 3 " +"connection, tempo and interval" +msgstr "" +"Regiony Ninjam: 1 ostatnia wiadomość czatu; 2 wybrany kanał lub element " +"sterujący; 3 połączenie, tempo i interwał" + +#: internal/applicationoverrides/ninjam/ninjam.go:77 +msgid "No chat message received" +msgstr "Nie odebrano wiadomości czatu" + #: internal/interaction/grid.go:43 internal/interaction/grid.go:79 msgid "No matches" msgstr "Brak dopasowań" @@ -670,6 +766,10 @@ msgstr "Brak dopasowań" msgid "No reading regions set" msgstr "Nie ustawiono obszarów czytania" +#: internal/applicationoverrides/ninjam/ninjam.go:268 +msgid "No remote users" +msgstr "Brak użytkowników zdalnych" + #: internal/app/clipboard.go:416 msgid "No text found in clipboard or no accessible display" msgstr "Brak tekstu w schowku lub dostępnego ekranu" @@ -746,16 +846,16 @@ msgstr "Odczytaj używaną wersję Fenrira." msgid "Presentation" msgstr "Prezentacja" -#: internal/command/descriptions.go:100 +#: internal/command/descriptions.go:101 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:112 +#: internal/app/commands.go:131 msgid "Progress monitoring disabled" msgstr "Monitorowanie postępu wyłączone" -#: internal/app/commands.go:110 +#: internal/app/commands.go:129 msgid "Progress monitoring enabled" msgstr "Monitorowanie postępu włączone" @@ -775,6 +875,10 @@ msgstr "Profil interpunkcji" msgid "Quick menu not available" msgstr "Menu podręczne niedostępne" +#: internal/applicationoverrides/ninjam/ninjam.go:271 +msgid "Quit Ninjam" +msgstr "Zakończ Ninjam" + #: internal/emoji/catalog.go:56 msgid "Rainbow" msgstr "Tęcza" @@ -791,43 +895,43 @@ msgstr "Szybkość" msgid "Raves" msgstr "Imprezy rave" -#: internal/command/descriptions.go:78 +#: internal/command/descriptions.go:79 msgid "Read live region 1" msgstr "Czytaj obszar dynamiczny 1" -#: internal/command/descriptions.go:96 +#: internal/command/descriptions.go:97 msgid "Read live region 10" msgstr "Czytaj obszar dynamiczny 10" -#: internal/command/descriptions.go:80 +#: internal/command/descriptions.go:81 msgid "Read live region 2" msgstr "Czytaj obszar dynamiczny 2" -#: internal/command/descriptions.go:82 +#: internal/command/descriptions.go:83 msgid "Read live region 3" msgstr "Czytaj obszar dynamiczny 3" -#: internal/command/descriptions.go:84 +#: internal/command/descriptions.go:85 msgid "Read live region 4" msgstr "Czytaj obszar dynamiczny 4" -#: internal/command/descriptions.go:86 +#: internal/command/descriptions.go:87 msgid "Read live region 5" msgstr "Czytaj obszar dynamiczny 5" -#: internal/command/descriptions.go:88 +#: internal/command/descriptions.go:89 msgid "Read live region 6" msgstr "Czytaj obszar dynamiczny 6" -#: internal/command/descriptions.go:90 +#: internal/command/descriptions.go:91 msgid "Read live region 7" msgstr "Czytaj obszar dynamiczny 7" -#: internal/command/descriptions.go:92 +#: internal/command/descriptions.go:93 msgid "Read live region 8" msgstr "Czytaj obszar dynamiczny 8" -#: internal/command/descriptions.go:94 +#: internal/command/descriptions.go:95 msgid "Read live region 9" msgstr "Czytaj obszar dynamiczny 9" @@ -855,14 +959,22 @@ msgstr "Odczytuje atrybuty bieżącej pozycji kursora" msgid "Reads from the top of the screen to the cursor position" msgstr "Czyta od góry ekranu do pozycji kursora." +#: internal/applicationoverrides/ninjam/ninjam.go:229 +msgid "Remote {0}, channel {1}" +msgstr "Zdalny użytkownik {0}, kanał {1}" + #: internal/app/clipboard.go:264 msgid "Remove marks" msgstr "Usuń znaczniki" -#: internal/command/descriptions.go:99 +#: internal/command/descriptions.go:100 msgid "Removes marks from selected text" msgstr "Usuwa znaczniki z zaznaczonego tekstu" +#: internal/applicationoverrides/ninjam/ninjam.go:111 +msgid "Rename channel" +msgstr "Zmień nazwę kanału" + #: internal/config/config.go:161 msgid "Repeated symbols" msgstr "Powtarzane symbole" @@ -895,7 +1007,11 @@ msgstr "Smutek" msgid "Santa" msgstr "Święty Mikołaj" -#: internal/command/descriptions.go:64 +#: internal/app/runtime_settings.go:151 internal/app/runtime_settings.go:168 +msgid "Save unavailable." +msgstr "Zapisywanie niedostępne." + +#: internal/command/descriptions.go:65 msgid "Saves your current Fenrir settings so they are the default." msgstr "Zapisuje bieżące ustawienia Fenrira jako domyślne." @@ -915,7 +1031,7 @@ msgstr "Mów „błędna pisownia”" msgid "Screaming" msgstr "Krzyk" -#: internal/app/commands.go:135 internal/app/scripts.go +#: internal/app/commands.go:154 internal/app/scripts.go msgid "Script failed" msgstr "Skrypt nie powiódł się" @@ -939,15 +1055,23 @@ msgstr "Wyszukiwanie wyczyszczone" msgid "Search {0}" msgstr "Szukaj {0}" +#: internal/command/descriptions.go:56 +msgid "Select a temporary application override" +msgstr "Wybierz tymczasowe ustawienia aplikacji" + #: internal/config/config.go:121 msgid "Select sound theme" msgstr "Wybierz motyw dźwiękowy" +#: internal/applicationoverrides/ninjam/ninjam.go:145 +msgid "Selected channel or control" +msgstr "Wybrany kanał lub element sterujący" + #: internal/config/config.go:128 msgid "Serial port" msgstr "Port szeregowy" -#: internal/command/descriptions.go:74 +#: internal/command/descriptions.go:75 msgid "Set a live region from one or two marks" msgstr "Ustaw obszar dynamiczny za pomocą jednego lub dwóch znaczników" @@ -955,6 +1079,11 @@ 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 +msgid "Settings are busy" +msgstr "Ustawienia są zajęte" + #: internal/app/quick_settings.go:298 msgid "Settings saved." msgstr "Ustawienia zapisane." @@ -1027,7 +1156,7 @@ msgstr "Mowa włączona" msgid "Speech language" msgstr "Język mowy" -#: internal/app/screen_updates.go:72 +#: internal/app/screen_updates.go:80 msgid "Speech restored" msgstr "Mowa przywrócona" @@ -1119,7 +1248,7 @@ msgstr "Czas i data" msgid "Time format" msgstr "Format czasu" -#: internal/command/descriptions.go:67 +#: internal/command/descriptions.go:68 msgid "Toggle progress bar monitoring with ascending tones" msgstr "Przełącz monitorowanie paska postępu z rosnącymi tonami" @@ -1139,7 +1268,7 @@ msgstr "Drzewo" msgid "Turkey" msgstr "Indyk" -#: internal/command/descriptions.go:73 +#: internal/command/descriptions.go:74 msgid "Turn off window mode" msgstr "Wyłącz tryb okna" @@ -1188,6 +1317,10 @@ msgstr "Test głosu: Szybki brązowy lis przeskakuje nad leniwym psem." msgid "Volume" msgstr "Głośność" +#: internal/applicationoverrides/ninjam/ninjam.go:253 +msgid "Volume and pan {0}" +msgstr "Głośność i panorama {0}" + #: internal/punctuation/emoticons.go:32 msgid "WTF?" msgstr "Co do diabła?" @@ -1279,11 +1412,11 @@ msgstr "strzałka w prawo" msgid "at" msgstr "małpa" -#: internal/reader/reader.go:197 +#: internal/reader/reader.go:247 msgid "autoread disabled" msgstr "Wyłączono automatyczne ogłaszanie dynamicznych zmian treści." -#: internal/reader/reader.go:195 +#: internal/reader/reader.go:245 msgid "autoread enabled" msgstr "Włączono ogłaszanie dynamicznych zmian treści." @@ -1367,7 +1500,7 @@ msgstr "znak {0}" msgid "checked" msgstr "zaznaczone" -#: internal/command/descriptions.go:108 +#: internal/command/descriptions.go:109 msgid "clears the currently selected clipboard" msgstr "Czyści aktualnie zaznaczony showek." @@ -1401,11 +1534,11 @@ msgstr "dezorientacja" msgid "cool" msgstr "wyluzowanie" -#: internal/command/descriptions.go:102 +#: internal/command/descriptions.go:103 msgid "copies last presented text to the clipboard" msgstr "kopiuje ostatnio odczytany tekst do schowka" -#: internal/command/descriptions.go:101 +#: internal/command/descriptions.go:102 msgid "copies marked text to the currently selected clipboard" msgstr "Kopiuje zaznaczony tekst do aktualnie wybranego schowka." @@ -1429,7 +1562,7 @@ msgstr "Aktualna linia" msgid "current word." msgstr "Aktualne słowo" -#: internal/config/definitions.go:225 internal/reader/reader.go:190 +#: internal/config/definitions.go:225 internal/reader/reader.go:240 msgid "cursor tracking" msgstr "Śledzenie kursora." @@ -1454,7 +1587,7 @@ msgstr "łącznik" msgid "default" msgstr "domyślne" -#: internal/command/descriptions.go:66 +#: internal/command/descriptions.go:67 msgid "disables speech until next keypress" msgstr "Wyłącza mowę do następnego naciśnięcia klawisza." @@ -1472,11 +1605,11 @@ msgstr "dolar" msgid "dot" msgstr "kropka" -#: internal/command/descriptions.go:68 +#: internal/command/descriptions.go:69 msgid "enables or disables automatic reading of new text as it appears" msgstr "Włącza lub wyłącza informowanie o dynamicznych zmianach treści." -#: internal/command/descriptions.go:65 +#: internal/command/descriptions.go:66 msgid "enables or disables speech" msgstr "Włącza lub wyłącza mowę." @@ -1484,7 +1617,7 @@ msgstr "Włącza lub wyłącza mowę." msgid "enables or disables the announcement of attributes" msgstr "włącza lub wyłącza odczytywanie atrybutów" -#: internal/command/descriptions.go:69 +#: internal/command/descriptions.go:70 msgid "enables or disables tracking of highlighted text" msgstr "Włącza lub wyłącza śledzenie podświetlonego tekstu." @@ -1513,7 +1646,7 @@ msgstr "równa się" msgid "evil smile" msgstr "złowrogi uśmiech" -#: internal/command/descriptions.go:71 +#: internal/command/descriptions.go:72 msgid "exits Fenrir" msgstr "Zamyka Fenrir." @@ -1521,7 +1654,7 @@ msgstr "Zamyka Fenrir." msgid "exits review mode" msgstr "Wychodzi z trybu przeglądu." -#: internal/command/descriptions.go:111 +#: internal/command/descriptions.go:112 msgid "export the current fenrir clipboard to a file" msgstr "Wyeksportuj aktualny showek Fenrir do pliku." @@ -1558,35 +1691,35 @@ msgstr "niezadowolenie" msgid "frustrated" msgstr "frustracja" -#: internal/command/descriptions.go:58 +#: internal/command/descriptions.go:59 msgid "get current quick menu entry" msgstr "odczytaj bieżącą pozycję menu podręcznego" -#: internal/command/descriptions.go:59 +#: internal/command/descriptions.go:60 msgid "get current quick menu value" msgstr "odczytaj bieżącą wartość menu podręcznego" -#: internal/command/descriptions.go:56 +#: internal/command/descriptions.go:57 msgid "get next quick menu category" msgstr "wybierz następną kategorię menu podręcznego" -#: internal/command/descriptions.go:60 +#: internal/command/descriptions.go:61 msgid "get next quick menu entry" msgstr "wybierz następną pozycję menu podręcznego" -#: internal/command/descriptions.go:62 +#: internal/command/descriptions.go:63 msgid "get next quick menu value" msgstr "wybierz następną wartość menu podręcznego" -#: internal/command/descriptions.go:57 +#: internal/command/descriptions.go:58 msgid "get previous quick menu category" msgstr "wybierz poprzednią kategorię menu podręcznego" -#: internal/command/descriptions.go:61 +#: internal/command/descriptions.go:62 msgid "get previous quick menu entry" msgstr "wybierz poprzednią pozycję menu podręcznego" -#: internal/command/descriptions.go:63 +#: internal/command/descriptions.go:64 msgid "get previous quick menu value" msgstr "wybierz poprzednią wartość menu podręcznego" @@ -1625,7 +1758,7 @@ msgstr "ustawiono wiersz nagłówka" msgid "heart" msgstr "serce" -#: internal/config/definitions.go:225 internal/reader/reader.go:187 +#: internal/config/definitions.go:225 internal/reader/reader.go:237 msgid "highlight tracking" msgstr "Śledzenie podświetlenia." @@ -1633,11 +1766,11 @@ msgstr "Śledzenie podświetlenia." msgid "hurray" msgstr "hurra" -#: internal/command/descriptions.go:110 +#: internal/command/descriptions.go:111 msgid "imports text from clipboard file to the clipboard" msgstr "Importuje tekst z pliku do schowka." -#: internal/command/descriptions.go:112 +#: internal/command/descriptions.go:113 msgid "imports the graphical clipboard to Fenrir's clipboard" msgstr "importuje schowek graficzny do schowka Fenrira" @@ -1782,6 +1915,10 @@ msgstr "przenosi kursor przeglądu do poprzedniego znaku" msgid "moves review to the previous line " msgstr "przenosi kursor przeglądu do poprzedniego wiersza " +#: internal/applicationoverrides/ninjam/ninjam.go:239 +msgid "muted" +msgstr "wyciszony" + #: internal/punctuation/emoticons.go:19 msgid "neutral" msgstr "neutralność" @@ -1809,6 +1946,14 @@ msgstr "brak konsoli tekstowej" msgid "none" msgstr "brak" +#: internal/applicationoverrides/ninjam/ninjam.go:247 +msgid "not receiving" +msgstr "odbiór wyłączony" + +#: internal/applicationoverrides/ninjam/ninjam.go:242 +msgid "not transmitting" +msgstr "nadawanie wyłączone" + #: internal/punctuation/text_processing.go msgid "number" msgstr "numer" @@ -1817,7 +1962,7 @@ msgstr "numer" msgid "paste clipboard" msgstr "wklej schowek" -#: internal/command/descriptions.go:109 +#: internal/command/descriptions.go:110 msgid "pastes the text from the currently selected clipboard" msgstr "Wkleja tekst z aktualnie wybranego schowka." @@ -1826,7 +1971,7 @@ msgstr "Wkleja tekst z aktualnie wybranego schowka." msgid "percent" msgstr "procent" -#: internal/command/descriptions.go:98 +#: internal/command/descriptions.go:99 msgid "places marks to select text to copy to the clipboard" msgstr "Ustawia znaczniki w tekście do skopiowania do schowka." @@ -1885,6 +2030,10 @@ msgstr "Czyta od pozycji kursora do końca ekranu." msgid "reads the contents of the current screen" msgstr "Wypowiada treść aktualnego ekranu." +#: internal/applicationoverrides/ninjam/ninjam.go:249 +msgid "receiving" +msgstr "odbiór włączony" + #: internal/presentation/attributes.go msgid "red" msgstr "Czerwony" @@ -1920,7 +2069,7 @@ msgstr "wiersz {0}, kolumna {1} do wiersza {2}, kolumny {3}" msgid "screen is empty" msgstr "Ekran jest pusty." -#: internal/reader/reader.go:132 +#: internal/reader/reader.go:155 msgid "screen {0}" msgstr "Ekran {0}" @@ -1928,19 +2077,19 @@ msgstr "Ekran {0}" msgid "selected" msgstr "zaznaczone" -#: internal/command/descriptions.go:106 +#: internal/command/descriptions.go:107 msgid "selects the first clipboard" msgstr "Wybiera pierwszy schowek." -#: internal/command/descriptions.go:107 +#: internal/command/descriptions.go:108 msgid "selects the last clipboard" msgstr "Wybiera ostatni showek." -#: internal/command/descriptions.go:105 +#: internal/command/descriptions.go:106 msgid "selects the next clipboard" msgstr "Wybiera następny showek." -#: internal/command/descriptions.go:104 +#: internal/command/descriptions.go:105 msgid "selects the previous clipboard" msgstr "Wybiera poprzedni showek." @@ -1949,11 +2098,11 @@ msgstr "Wybiera poprzedni showek." msgid "semicolon" msgstr "średnik" -#: internal/command/descriptions.go:70 +#: internal/command/descriptions.go:71 msgid "sends the following keypress to the terminal or application" msgstr "przekazuje następne naciśnięcie klawisza do terminala lub aplikacji" -#: internal/command/descriptions.go:72 +#: internal/command/descriptions.go:73 msgid "set Window Mode, needs 2 marks " msgstr "ustaw tryb okna, wymagane dwa znaczniki. " @@ -1995,7 +2144,7 @@ msgstr "niektóre" msgid "space" msgstr "spacja" -#: internal/command/descriptions.go:103 +#: internal/command/descriptions.go:104 msgid "speaks the contents of the currently selected clipboard" msgstr "Wypowiada treść aktualnie wybranego showka" @@ -2007,7 +2156,7 @@ msgstr "mowa" msgid "speech and beeps" msgstr "mowa i sygnały" -#: internal/app/commands.go:59 internal/app/feedback.go:45 +#: internal/app/commands.go:64 internal/app/feedback.go:45 msgid "speech enabled" msgstr "Mowa włączona." @@ -2023,7 +2172,7 @@ msgstr "wysokość głosu" msgid "speech rate" msgstr "szybkość mowy" -#: internal/app/commands.go:121 +#: internal/app/commands.go:140 msgid "speech temporary disabled" msgstr "Mowa tymczasowo wyłączona." @@ -2064,7 +2213,7 @@ msgstr "zaskoczenie" msgid "tab" msgstr "tabulator" -#: internal/config/definitions.go:225 internal/reader/reader.go:184 +#: internal/config/definitions.go:225 internal/reader/reader.go:234 msgid "table mode" msgstr "tryb tabeli" @@ -2081,12 +2230,16 @@ msgstr "Przełącza tryb pomocy." msgid "tongue out" msgstr "wystawiony język" +#: internal/applicationoverrides/ninjam/ninjam.go:244 +msgid "transmitting" +msgstr "nadawanie włączone" + #: internal/interaction/presentation.go:62 msgid "unavailable" msgstr "niedostępne" #: internal/app/tutorial.go:107 internal/app/tutorial.go:161 -#: internal/reader/reader.go:199 internal/review/review.go:140 +#: internal/reader/reader.go:249 internal/review/review.go:140 msgid "unbound" msgstr "nieprzypisane" @@ -2098,6 +2251,10 @@ msgstr "niezaznaczone" msgid "underline" msgstr "podkreślenie" +#: internal/applicationoverrides/ninjam/ninjam.go:237 +msgid "unmuted" +msgstr "niewyciszony" + #: internal/presentation/attributes.go internal/presentation/attributes.go:27 msgid "white" msgstr "Biały" @@ -2110,6 +2267,10 @@ msgstr "mrugnięcie" msgid "{0} added to dictionary" msgstr "Dodano {0} do słownika" +#: internal/app/application_overrides.go:147 +msgid "{0} application override selected" +msgstr "Wybrano ustawienia aplikacji {0}" + #: 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 1a5a7824..0964627f 100644 --- a/internal/i18n/locale/pt/LC_MESSAGES/fenrir.po +++ b/internal/i18n/locale/pt/LC_MESSAGES/fenrir.po @@ -66,6 +66,14 @@ msgstr "Zangado" msgid "Apple" msgstr "Maçã" +#: internal/app/application_overrides.go:237 +msgid "Application" +msgstr "Aplicativo" + +#: internal/app/application_overrides.go:236 +msgid "Applications" +msgstr "Aplicativos" + #: internal/emoji/catalog.go:117 msgid "Argentina" msgstr "Argentina" @@ -154,6 +162,7 @@ msgstr "Borboleta" msgid "Canada" msgstr "Canadá" +#: internal/app/application_overrides.go:238 #: internal/preferences/settings.go:79 msgid "Cancel" msgstr "Cancelar" @@ -166,10 +175,18 @@ msgstr "Gato" msgid "Chains" msgstr "Correntes" +#: internal/applicationoverrides/ninjam/ninjam.go:256 +msgid "Channel name {0}" +msgstr "Nome do canal {0}" + #: internal/config/config.go:140 msgid "Character echo" msgstr "Eco de caracteres" +#: internal/applicationoverrides/ninjam/ninjam.go:103 +msgid "Chat message" +msgstr "Mensagem do chat" + #: internal/emoji/catalog.go:102 msgid "Checkmark" msgstr "Marca de verificação" @@ -186,51 +203,51 @@ msgstr "China" msgid "Christmas tree" msgstr "Árvore de Natal" -#: internal/command/descriptions.go:76 +#: internal/command/descriptions.go:77 msgid "Clear all live regions" msgstr "Limpar todas as regiões dinâmicas" -#: internal/command/descriptions.go:77 +#: internal/command/descriptions.go:78 msgid "Clear all reading regions" msgstr "Limpar todas as regiões de leitura" -#: internal/command/descriptions.go:79 +#: internal/command/descriptions.go:80 msgid "Clear live region 1" msgstr "Limpar região dinâmica 1" -#: internal/command/descriptions.go:97 +#: internal/command/descriptions.go:98 msgid "Clear live region 10" msgstr "Limpar região dinâmica 10" -#: internal/command/descriptions.go:81 +#: internal/command/descriptions.go:82 msgid "Clear live region 2" msgstr "Limpar região dinâmica 2" -#: internal/command/descriptions.go:83 +#: internal/command/descriptions.go:84 msgid "Clear live region 3" msgstr "Limpar região dinâmica 3" -#: internal/command/descriptions.go:85 +#: internal/command/descriptions.go:86 msgid "Clear live region 4" msgstr "Limpar região dinâmica 4" -#: internal/command/descriptions.go:87 +#: internal/command/descriptions.go:88 msgid "Clear live region 5" msgstr "Limpar região dinâmica 5" -#: internal/command/descriptions.go:89 +#: internal/command/descriptions.go:90 msgid "Clear live region 6" msgstr "Limpar região dinâmica 6" -#: internal/command/descriptions.go:91 +#: internal/command/descriptions.go:92 msgid "Clear live region 7" msgstr "Limpar região dinâmica 7" -#: internal/command/descriptions.go:93 +#: internal/command/descriptions.go:94 msgid "Clear live region 8" msgstr "Limpar região dinâmica 8" -#: internal/command/descriptions.go:95 +#: internal/command/descriptions.go:96 msgid "Clear live region 9" msgstr "Limpar região dinâmica 9" @@ -238,7 +255,7 @@ msgstr "Limpar região dinâmica 9" msgid "Clipboard" msgstr "Área de transferência" -#: internal/app/application.go:136 internal/app/application.go:139 +#: internal/app/application.go:142 internal/app/application.go:145 #: internal/app/clipboard.go:218 internal/app/clipboard.go:374 msgid "Clipboard import failed" msgstr "Falha ao importar a área de transferência" @@ -259,6 +276,10 @@ msgstr "Caixão" msgid "Column number for cursor" msgstr "Número da coluna do cursor" +#: internal/applicationoverrides/ninjam/ninjam.go:145 +msgid "Connection, tempo and interval" +msgstr "Conexão, andamento e intervalo" + #: internal/emoji/catalog.go:26 msgid "Cool" msgstr "Legal" @@ -267,6 +288,11 @@ 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 +msgid "Could not open Applications: {0}" +msgstr "Não foi possível abrir Aplicativos: {0}" + #: internal/app/emoji_menu.go:17 internal/app/emoji_menu.go:21 msgid "Could not open Emoji Menu: {0}" msgstr "Não foi possível abrir o menu de emojis: {0}" @@ -280,12 +306,17 @@ 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 +msgid "Could not restore application settings: {0}" +msgstr "Não foi possível restaurar as configurações do aplicativo: {0}" + #: internal/app/spellcheck_menu.go:155 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:164 internal/app/runtime_settings.go:191 +#: internal/app/runtime_settings.go:172 internal/app/runtime_settings.go:199 msgid "Could not save settings: {0}" msgstr "Não foi possível salvar as configurações: {0}" @@ -305,6 +336,19 @@ msgstr "Adaga" msgid "Date format" msgstr "Formato da data" +#: internal/app/application_overrides.go:230 +msgid "Default" +msgstr "Padrão" + +#: internal/app/application_overrides.go:129 +#: internal/app/application_overrides.go:182 +msgid "Default settings restored" +msgstr "Configurações padrão restauradas" + +#: internal/applicationoverrides/ninjam/ninjam.go:262 +msgid "Delete channel" +msgstr "Excluir canal" + #: internal/emoji/catalog.go:123 msgid "Denmark" msgstr "Dinamarca" @@ -392,7 +436,7 @@ msgstr "" msgid "Exploding head" msgstr "Cabeça explodindo" -#: internal/command/descriptions.go:113 +#: internal/command/descriptions.go:114 msgid "Export current fenrir clipboard to X or GUI clipboard" msgstr "" "Exportar a área de transferência atual do Fenrir para a área de " @@ -402,7 +446,7 @@ msgstr "" msgid "Face with symbols" msgstr "Rosto com símbolos" -#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:151 +#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:155 msgid "Failed to save settings." msgstr "Falha ao salvar as configurações." @@ -410,7 +454,7 @@ msgstr "Falha ao salvar as configurações." msgid "Fenrir Menu" msgstr "Menu do Fenrir" -#: internal/app/commands.go:83 +#: internal/app/commands.go:98 msgid "Fenrir screen reader version {0}-{1}" msgstr "Leitor de tela Fenrir versão {0}-{1}" @@ -446,7 +490,7 @@ msgstr "Mãos juntas" msgid "Food" msgstr "Comida" -#: internal/app/commands.go:126 +#: internal/app/commands.go:145 msgid "Forward next keypress" msgstr "Avançar próxima tecla pressionada" @@ -526,6 +570,14 @@ msgstr "Recuo" msgid "India" msgstr "Índia" +#: internal/applicationoverrides/ninjam/ninjam.go:259 +msgid "Input {0}" +msgstr "Entrada {0}" + +#: internal/applicationoverrides/ninjam/ninjam.go:141 +msgid "Interval position {0} of {1}" +msgstr "Posição do intervalo {0} de {1}" + #: internal/emoji/catalog.go:129 msgid "Ireland" msgstr "Irlanda" @@ -554,6 +606,10 @@ msgstr "Teclado" msgid "Last clipboard " msgstr "Última área de transferência " +#: internal/applicationoverrides/ninjam/ninjam.go:145 +msgid "Latest chat message" +msgstr "Última mensagem do chat" + #: internal/emoji/catalog.go:37 msgid "Laughing" msgstr "Rindo" @@ -566,7 +622,7 @@ msgstr "Layout do teclado" msgid "Line number for cursor" msgstr "Número da linha do cursor" -#: internal/command/descriptions.go:75 +#: internal/command/descriptions.go:76 msgid "List reading regions" msgstr "Listar regiões de leitura" @@ -594,10 +650,22 @@ msgstr "Região dinâmica {0}: {1}" msgid "Live regions cleared" msgstr "Regiões dinâmicas limpas" +#: internal/applicationoverrides/ninjam/ninjam.go:232 +msgid "Local channel {0}" +msgstr "Canal local {0}" + #: internal/emoji/catalog.go:87 msgid "Mage" msgstr "Mago" +#: internal/applicationoverrides/ninjam/ninjam.go:222 +msgid "Master" +msgstr "Principal" + +#: internal/applicationoverrides/ninjam/ninjam.go:224 +msgid "Metronome" +msgstr "Metrônomo" + #: internal/emoji/catalog.go:133 msgid "Mexico" msgstr "México" @@ -664,6 +732,34 @@ msgstr "Enjoado" msgid "Netherlands" msgstr "Países Baixos" +#: internal/app/application_overrides.go:230 +msgid "NetworkManager" +msgstr "NetworkManager" + +#: internal/applicationoverrides/ninjam/ninjam.go:265 +msgid "New channel" +msgstr "Novo canal" + +#: internal/app/application_overrides.go:230 +msgid "Ninjam" +msgstr "Ninjam" + +#: internal/applicationoverrides/ninjam/ninjam.go:79 +msgid "Ninjam information unavailable" +msgstr "Informações do Ninjam indisponíveis" + +#: internal/applicationoverrides/ninjam/ninjam.go:63 +msgid "" +"Ninjam regions: 1 latest chat message; 2 selected channel or control; 3 " +"connection, tempo and interval" +msgstr "" +"Regiões do Ninjam: 1 última mensagem do chat; 2 canal ou controle " +"selecionado; 3 conexão, andamento e intervalo" + +#: internal/applicationoverrides/ninjam/ninjam.go:77 +msgid "No chat message received" +msgstr "Nenhuma mensagem de chat recebida" + #: internal/interaction/grid.go:43 internal/interaction/grid.go:79 msgid "No matches" msgstr "Nenhum resultado" @@ -672,6 +768,10 @@ msgstr "Nenhum resultado" msgid "No reading regions set" msgstr "Nenhuma região de leitura definida" +#: internal/applicationoverrides/ninjam/ninjam.go:268 +msgid "No remote users" +msgstr "Nenhum usuário remoto" + #: internal/app/clipboard.go:416 msgid "No text found in clipboard or no accessible display" msgstr "" @@ -749,17 +849,17 @@ msgstr "Anunciar a versão do Fenrir em uso." msgid "Presentation" msgstr "Apresentação" -#: internal/command/descriptions.go:100 +#: internal/command/descriptions.go:101 msgid "" "Presents the currently selected text that will be copied to the clipboard" msgstr "" "Apresenta o texto selecionado que será copiado para a área de transferência" -#: internal/app/commands.go:112 +#: internal/app/commands.go:131 msgid "Progress monitoring disabled" msgstr "Monitoramento de progresso desativado" -#: internal/app/commands.go:110 +#: internal/app/commands.go:129 msgid "Progress monitoring enabled" msgstr "Monitoramento de progresso ativado" @@ -779,6 +879,10 @@ msgstr "Perfil de pontuação" msgid "Quick menu not available" msgstr "Menu rápido indisponível" +#: internal/applicationoverrides/ninjam/ninjam.go:271 +msgid "Quit Ninjam" +msgstr "Sair do Ninjam" + #: internal/emoji/catalog.go:56 msgid "Rainbow" msgstr "Arco-íris" @@ -795,43 +899,43 @@ msgstr "Velocidade" msgid "Raves" msgstr "Festas rave" -#: internal/command/descriptions.go:78 +#: internal/command/descriptions.go:79 msgid "Read live region 1" msgstr "Ler região dinâmica 1" -#: internal/command/descriptions.go:96 +#: internal/command/descriptions.go:97 msgid "Read live region 10" msgstr "Ler região dinâmica 10" -#: internal/command/descriptions.go:80 +#: internal/command/descriptions.go:81 msgid "Read live region 2" msgstr "Ler região dinâmica 2" -#: internal/command/descriptions.go:82 +#: internal/command/descriptions.go:83 msgid "Read live region 3" msgstr "Ler região dinâmica 3" -#: internal/command/descriptions.go:84 +#: internal/command/descriptions.go:85 msgid "Read live region 4" msgstr "Ler região dinâmica 4" -#: internal/command/descriptions.go:86 +#: internal/command/descriptions.go:87 msgid "Read live region 5" msgstr "Ler região dinâmica 5" -#: internal/command/descriptions.go:88 +#: internal/command/descriptions.go:89 msgid "Read live region 6" msgstr "Ler região dinâmica 6" -#: internal/command/descriptions.go:90 +#: internal/command/descriptions.go:91 msgid "Read live region 7" msgstr "Ler região dinâmica 7" -#: internal/command/descriptions.go:92 +#: internal/command/descriptions.go:93 msgid "Read live region 8" msgstr "Ler região dinâmica 8" -#: internal/command/descriptions.go:94 +#: internal/command/descriptions.go:95 msgid "Read live region 9" msgstr "Ler região dinâmica 9" @@ -859,14 +963,22 @@ msgstr "Lê os atributos da posição atual do cursor" msgid "Reads from the top of the screen to the cursor position" msgstr "Lê da parte superior da tela até a posição do cursor" +#: internal/applicationoverrides/ninjam/ninjam.go:229 +msgid "Remote {0}, channel {1}" +msgstr "Remoto {0}, canal {1}" + #: internal/app/clipboard.go:264 msgid "Remove marks" msgstr "Remove marcas" -#: internal/command/descriptions.go:99 +#: internal/command/descriptions.go:100 msgid "Removes marks from selected text" msgstr "Remove as marcas do texto selecionado" +#: internal/applicationoverrides/ninjam/ninjam.go:111 +msgid "Rename channel" +msgstr "Renomear canal" + #: internal/config/config.go:161 msgid "Repeated symbols" msgstr "Símbolos repetidos" @@ -899,7 +1011,11 @@ msgstr "Triste" msgid "Santa" msgstr "Papai Noel" -#: internal/command/descriptions.go:64 +#: internal/app/runtime_settings.go:151 internal/app/runtime_settings.go:168 +msgid "Save unavailable." +msgstr "Salvamento indisponível." + +#: internal/command/descriptions.go:65 msgid "Saves your current Fenrir settings so they are the default." msgstr "Salva as configurações atuais do Fenrir como padrão." @@ -919,7 +1035,7 @@ msgstr "Dizer “erro ortográfico”" msgid "Screaming" msgstr "Gritando" -#: internal/app/commands.go:135 internal/app/scripts.go +#: internal/app/commands.go:154 internal/app/scripts.go msgid "Script failed" msgstr "O script falhou" @@ -943,15 +1059,23 @@ msgstr "Pesquisa limpa" msgid "Search {0}" msgstr "Pesquisar {0}" +#: internal/command/descriptions.go:56 +msgid "Select a temporary application override" +msgstr "Selecionar configurações temporárias de um aplicativo" + #: internal/config/config.go:121 msgid "Select sound theme" msgstr "Selecionar tema de som" +#: internal/applicationoverrides/ninjam/ninjam.go:145 +msgid "Selected channel or control" +msgstr "Canal ou controle selecionado" + #: internal/config/config.go:128 msgid "Serial port" msgstr "Porta serial" -#: internal/command/descriptions.go:74 +#: internal/command/descriptions.go:75 msgid "Set a live region from one or two marks" msgstr "Definir uma região dinâmica a partir de uma ou duas marcas" @@ -959,6 +1083,11 @@ 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 +msgid "Settings are busy" +msgstr "As configurações estão ocupadas" + #: internal/app/quick_settings.go:298 msgid "Settings saved." msgstr "Configurações salvas." @@ -1031,7 +1160,7 @@ msgstr "Fala ativada" msgid "Speech language" msgstr "Idioma da fala" -#: internal/app/screen_updates.go:72 +#: internal/app/screen_updates.go:80 msgid "Speech restored" msgstr "Fala restaurada" @@ -1123,7 +1252,7 @@ msgstr "Hora e data" msgid "Time format" msgstr "Formato da hora" -#: internal/command/descriptions.go:67 +#: internal/command/descriptions.go:68 msgid "Toggle progress bar monitoring with ascending tones" msgstr "Alternar o monitoramento do progresso com tons ascendentes" @@ -1143,7 +1272,7 @@ msgstr "Árvore" msgid "Turkey" msgstr "Peru" -#: internal/command/descriptions.go:73 +#: internal/command/descriptions.go:74 msgid "Turn off window mode" msgstr "Desativar o modo de janela" @@ -1193,6 +1322,10 @@ msgstr "" msgid "Volume" msgstr "Volume" +#: internal/applicationoverrides/ninjam/ninjam.go:253 +msgid "Volume and pan {0}" +msgstr "Volume e panorama {0}" + #: internal/punctuation/emoticons.go:32 msgid "WTF?" msgstr "Mas que diabos?" @@ -1284,11 +1417,11 @@ msgstr "seta para a direita" msgid "at" msgstr "arroba" -#: internal/reader/reader.go:197 +#: internal/reader/reader.go:247 msgid "autoread disabled" msgstr "leitura automática desativada" -#: internal/reader/reader.go:195 +#: internal/reader/reader.go:245 msgid "autoread enabled" msgstr "leitura automática ativada" @@ -1372,7 +1505,7 @@ msgstr "caractere {0}" msgid "checked" msgstr "marcado" -#: internal/command/descriptions.go:108 +#: internal/command/descriptions.go:109 msgid "clears the currently selected clipboard" msgstr "limpa a área de transferência atualmente selecionada" @@ -1406,11 +1539,11 @@ msgstr "confuso" msgid "cool" msgstr "legal" -#: internal/command/descriptions.go:102 +#: internal/command/descriptions.go:103 msgid "copies last presented text to the clipboard" msgstr "copia o último texto apresentado para a área de transferência" -#: internal/command/descriptions.go:101 +#: internal/command/descriptions.go:102 msgid "copies marked text to the currently selected clipboard" msgstr "" "copia o texto marcado para a área de transferência atualmente selecionada" @@ -1435,7 +1568,7 @@ msgstr "linha atual" msgid "current word." msgstr "palavra atual." -#: internal/config/definitions.go:225 internal/reader/reader.go:190 +#: internal/config/definitions.go:225 internal/reader/reader.go:240 msgid "cursor tracking" msgstr "rastreamento do cursor" @@ -1460,7 +1593,7 @@ msgstr "hífen" msgid "default" msgstr "padrão" -#: internal/command/descriptions.go:66 +#: internal/command/descriptions.go:67 msgid "disables speech until next keypress" msgstr "desativa a fala até a próxima tecla pressionada" @@ -1478,11 +1611,11 @@ msgstr "cifrão" msgid "dot" msgstr "ponto" -#: internal/command/descriptions.go:68 +#: internal/command/descriptions.go:69 msgid "enables or disables automatic reading of new text as it appears" msgstr "ativa ou desativa a leitura automática de novo texto como aparece" -#: internal/command/descriptions.go:65 +#: internal/command/descriptions.go:66 msgid "enables or disables speech" msgstr "ativa ou desativa a fala" @@ -1490,7 +1623,7 @@ msgstr "ativa ou desativa a fala" msgid "enables or disables the announcement of attributes" msgstr "ativa ou desativa o anúncio de atributos" -#: internal/command/descriptions.go:69 +#: internal/command/descriptions.go:70 msgid "enables or disables tracking of highlighted text" msgstr "ativa ou desativa o acompanhamento do texto destacado" @@ -1519,7 +1652,7 @@ msgstr "igual" msgid "evil smile" msgstr "sorriso malvado" -#: internal/command/descriptions.go:71 +#: internal/command/descriptions.go:72 msgid "exits Fenrir" msgstr "sai do Fenrir" @@ -1527,7 +1660,7 @@ msgstr "sai do Fenrir" msgid "exits review mode" msgstr "sai do modo de revisão" -#: internal/command/descriptions.go:111 +#: internal/command/descriptions.go:112 msgid "export the current fenrir clipboard to a file" msgstr "exporta a área de transferência atual do Fenrir para um arquivo" @@ -1565,35 +1698,35 @@ msgstr "carranca" msgid "frustrated" msgstr "frustrado" -#: internal/command/descriptions.go:58 +#: internal/command/descriptions.go:59 msgid "get current quick menu entry" msgstr "anunciar a entrada atual do menu rápido" -#: internal/command/descriptions.go:59 +#: internal/command/descriptions.go:60 msgid "get current quick menu value" msgstr "anunciar o valor atual do menu rápido" -#: internal/command/descriptions.go:56 +#: internal/command/descriptions.go:57 msgid "get next quick menu category" msgstr "selecionar a próxima categoria do menu rápido" -#: internal/command/descriptions.go:60 +#: internal/command/descriptions.go:61 msgid "get next quick menu entry" msgstr "selecionar a próxima entrada do menu rápido" -#: internal/command/descriptions.go:62 +#: internal/command/descriptions.go:63 msgid "get next quick menu value" msgstr "selecionar o próximo valor do menu rápido" -#: internal/command/descriptions.go:57 +#: internal/command/descriptions.go:58 msgid "get previous quick menu category" msgstr "selecionar a categoria anterior do menu rápido" -#: internal/command/descriptions.go:61 +#: internal/command/descriptions.go:62 msgid "get previous quick menu entry" msgstr "selecionar a entrada anterior do menu rápido" -#: internal/command/descriptions.go:63 +#: internal/command/descriptions.go:64 msgid "get previous quick menu value" msgstr "selecionar o valor anterior do menu rápido" @@ -1632,7 +1765,7 @@ msgstr "linha de cabeçalho definida" msgid "heart" msgstr "coração" -#: internal/config/definitions.go:225 internal/reader/reader.go:187 +#: internal/config/definitions.go:225 internal/reader/reader.go:237 msgid "highlight tracking" msgstr "rastreamento em destaque" @@ -1640,13 +1773,13 @@ msgstr "rastreamento em destaque" msgid "hurray" msgstr "viva" -#: internal/command/descriptions.go:110 +#: internal/command/descriptions.go:111 msgid "imports text from clipboard file to the clipboard" msgstr "" "importa texto do arquivo da área de transferência para a área de " "transferência" -#: internal/command/descriptions.go:112 +#: internal/command/descriptions.go:113 msgid "imports the graphical clipboard to Fenrir's clipboard" msgstr "" "importa a área de transferência gráfica para a área de transferência do " @@ -1793,6 +1926,10 @@ msgstr "move o cursor de revisão para o caractere anterior" msgid "moves review to the previous line " msgstr "move o cursor de revisão para a linha anterior " +#: internal/applicationoverrides/ninjam/ninjam.go:239 +msgid "muted" +msgstr "silenciado" + #: internal/punctuation/emoticons.go:19 msgid "neutral" msgstr "neutro" @@ -1820,6 +1957,14 @@ msgstr "nenhum console de texto" msgid "none" msgstr "nenhum" +#: internal/applicationoverrides/ninjam/ninjam.go:247 +msgid "not receiving" +msgstr "não recebendo" + +#: internal/applicationoverrides/ninjam/ninjam.go:242 +msgid "not transmitting" +msgstr "não transmitindo" + #: internal/punctuation/text_processing.go msgid "number" msgstr "número" @@ -1828,7 +1973,7 @@ msgstr "número" msgid "paste clipboard" msgstr "colar área de transferência" -#: internal/command/descriptions.go:109 +#: internal/command/descriptions.go:110 msgid "pastes the text from the currently selected clipboard" msgstr "cola o texto da área de transferência atualmente selecionada" @@ -1837,7 +1982,7 @@ msgstr "cola o texto da área de transferência atualmente selecionada" msgid "percent" msgstr "por cento" -#: internal/command/descriptions.go:98 +#: internal/command/descriptions.go:99 msgid "places marks to select text to copy to the clipboard" msgstr "" "coloca marcas para selecionar texto para copiar para a área de transferência" @@ -1897,6 +2042,10 @@ msgstr "lê a partir do cursor até a parte inferior da tela" msgid "reads the contents of the current screen" msgstr "lê o conteúdo da tela atual" +#: internal/applicationoverrides/ninjam/ninjam.go:249 +msgid "receiving" +msgstr "recebendo" + #: internal/presentation/attributes.go msgid "red" msgstr "vermelho" @@ -1932,7 +2081,7 @@ msgstr "linha {0}, coluna {1} até a linha {2}, coluna {3}" msgid "screen is empty" msgstr "a tela está vazia" -#: internal/reader/reader.go:132 +#: internal/reader/reader.go:155 msgid "screen {0}" msgstr "tela {0}" @@ -1940,19 +2089,19 @@ msgstr "tela {0}" msgid "selected" msgstr "selecionado" -#: internal/command/descriptions.go:106 +#: internal/command/descriptions.go:107 msgid "selects the first clipboard" msgstr "seleciona a primeira área de transferência" -#: internal/command/descriptions.go:107 +#: internal/command/descriptions.go:108 msgid "selects the last clipboard" msgstr "seleciona a última área de transferência" -#: internal/command/descriptions.go:105 +#: internal/command/descriptions.go:106 msgid "selects the next clipboard" msgstr "seleciona a próxima área de transferência" -#: internal/command/descriptions.go:104 +#: internal/command/descriptions.go:105 msgid "selects the previous clipboard" msgstr "seleciona a área de transferência anterior" @@ -1961,11 +2110,11 @@ msgstr "seleciona a área de transferência anterior" msgid "semicolon" msgstr "ponto e vírgula" -#: internal/command/descriptions.go:70 +#: internal/command/descriptions.go:71 msgid "sends the following keypress to the terminal or application" msgstr "envia a próxima tecla ao terminal ou aplicativo" -#: internal/command/descriptions.go:72 +#: internal/command/descriptions.go:73 msgid "set Window Mode, needs 2 marks " msgstr "definir o Modo de Janela, precisa de 2 marcas " @@ -2008,7 +2157,7 @@ msgstr "alguns" msgid "space" msgstr "espaço" -#: internal/command/descriptions.go:103 +#: internal/command/descriptions.go:104 msgid "speaks the contents of the currently selected clipboard" msgstr "fala o conteúdo da área de transferência atualmente selecionada" @@ -2020,7 +2169,7 @@ msgstr "fala" msgid "speech and beeps" msgstr "fala e sinais sonoros" -#: internal/app/commands.go:59 internal/app/feedback.go:45 +#: internal/app/commands.go:64 internal/app/feedback.go:45 msgid "speech enabled" msgstr "fala ativada" @@ -2036,7 +2185,7 @@ msgstr "tom da fala" msgid "speech rate" msgstr "velocidade da fala" -#: internal/app/commands.go:121 +#: internal/app/commands.go:140 msgid "speech temporary disabled" msgstr "fala temporariamente desativada" @@ -2077,7 +2226,7 @@ msgstr "surpreso" msgid "tab" msgstr "tabulação" -#: internal/config/definitions.go:225 internal/reader/reader.go:184 +#: internal/config/definitions.go:225 internal/reader/reader.go:234 msgid "table mode" msgstr "modo de tabela" @@ -2094,12 +2243,16 @@ msgstr "alterna o modo de aprendizado" msgid "tongue out" msgstr "língua de fora" +#: internal/applicationoverrides/ninjam/ninjam.go:244 +msgid "transmitting" +msgstr "transmitindo" + #: internal/interaction/presentation.go:62 msgid "unavailable" msgstr "indisponível" #: internal/app/tutorial.go:107 internal/app/tutorial.go:161 -#: internal/reader/reader.go:199 internal/review/review.go:140 +#: internal/reader/reader.go:249 internal/review/review.go:140 msgid "unbound" msgstr "sem atalho" @@ -2111,6 +2264,10 @@ msgstr "desmarcado" msgid "underline" msgstr "sublinhado" +#: internal/applicationoverrides/ninjam/ninjam.go:237 +msgid "unmuted" +msgstr "não silenciado" + #: internal/presentation/attributes.go internal/presentation/attributes.go:27 msgid "white" msgstr "branco" @@ -2123,6 +2280,10 @@ msgstr "piscadela" msgid "{0} added to dictionary" msgstr "{0} adicionado ao dicionário" +#: internal/app/application_overrides.go:147 +msgid "{0} application override selected" +msgstr "Configurações do aplicativo {0} selecionadas" + #: 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 70c07e1e..99f00a14 100644 --- a/internal/i18n/locale/ru/LC_MESSAGES/fenrir.po +++ b/internal/i18n/locale/ru/LC_MESSAGES/fenrir.po @@ -65,6 +65,14 @@ msgstr "Злость" msgid "Apple" msgstr "Яблоко" +#: internal/app/application_overrides.go:237 +msgid "Application" +msgstr "Приложение" + +#: internal/app/application_overrides.go:236 +msgid "Applications" +msgstr "Приложения" + #: internal/emoji/catalog.go:117 msgid "Argentina" msgstr "Аргентина" @@ -153,6 +161,7 @@ msgstr "Бабочка" msgid "Canada" msgstr "Канада" +#: internal/app/application_overrides.go:238 #: internal/preferences/settings.go:79 msgid "Cancel" msgstr "Отмена" @@ -165,10 +174,18 @@ msgstr "Кошка" msgid "Chains" msgstr "Цепи" +#: internal/applicationoverrides/ninjam/ninjam.go:256 +msgid "Channel name {0}" +msgstr "Название канала {0}" + #: internal/config/config.go:140 msgid "Character echo" msgstr "Эхо символов" +#: internal/applicationoverrides/ninjam/ninjam.go:103 +msgid "Chat message" +msgstr "Сообщение чата" + #: internal/emoji/catalog.go:102 msgid "Checkmark" msgstr "Галочка" @@ -185,51 +202,51 @@ msgstr "Китай" msgid "Christmas tree" msgstr "Новогодняя ёлка" -#: internal/command/descriptions.go:76 +#: internal/command/descriptions.go:77 msgid "Clear all live regions" msgstr "Очистить все динамические области" -#: internal/command/descriptions.go:77 +#: internal/command/descriptions.go:78 msgid "Clear all reading regions" msgstr "Очистить все области чтения" -#: internal/command/descriptions.go:79 +#: internal/command/descriptions.go:80 msgid "Clear live region 1" msgstr "Очистить динамическую область 1" -#: internal/command/descriptions.go:97 +#: internal/command/descriptions.go:98 msgid "Clear live region 10" msgstr "Очистить динамическую область 10" -#: internal/command/descriptions.go:81 +#: internal/command/descriptions.go:82 msgid "Clear live region 2" msgstr "Очистить динамическую область 2" -#: internal/command/descriptions.go:83 +#: internal/command/descriptions.go:84 msgid "Clear live region 3" msgstr "Очистить динамическую область 3" -#: internal/command/descriptions.go:85 +#: internal/command/descriptions.go:86 msgid "Clear live region 4" msgstr "Очистить динамическую область 4" -#: internal/command/descriptions.go:87 +#: internal/command/descriptions.go:88 msgid "Clear live region 5" msgstr "Очистить динамическую область 5" -#: internal/command/descriptions.go:89 +#: internal/command/descriptions.go:90 msgid "Clear live region 6" msgstr "Очистить динамическую область 6" -#: internal/command/descriptions.go:91 +#: internal/command/descriptions.go:92 msgid "Clear live region 7" msgstr "Очистить динамическую область 7" -#: internal/command/descriptions.go:93 +#: internal/command/descriptions.go:94 msgid "Clear live region 8" msgstr "Очистить динамическую область 8" -#: internal/command/descriptions.go:95 +#: internal/command/descriptions.go:96 msgid "Clear live region 9" msgstr "Очистить динамическую область 9" @@ -237,7 +254,7 @@ msgstr "Очистить динамическую область 9" msgid "Clipboard" msgstr "Буфер обмена" -#: internal/app/application.go:136 internal/app/application.go:139 +#: internal/app/application.go:142 internal/app/application.go:145 #: internal/app/clipboard.go:218 internal/app/clipboard.go:374 msgid "Clipboard import failed" msgstr "Не удалось импортировать буфер обмена" @@ -258,6 +275,10 @@ msgstr "Гроб" msgid "Column number for cursor" msgstr "Номер столбца для курсора." +#: internal/applicationoverrides/ninjam/ninjam.go:145 +msgid "Connection, tempo and interval" +msgstr "Соединение, темп и интервал" + #: internal/emoji/catalog.go:26 msgid "Cool" msgstr "Круто" @@ -266,6 +287,11 @@ msgstr "Круто" msgid "Could not check spelling: {0}" msgstr "Не удалось проверить правописание: {0}" +#: internal/app/application_overrides.go:241 +#: internal/app/application_overrides.go:245 +msgid "Could not open Applications: {0}" +msgstr "Не удалось открыть меню приложений: {0}" + #: internal/app/emoji_menu.go:17 internal/app/emoji_menu.go:21 msgid "Could not open Emoji Menu: {0}" msgstr "Не удалось открыть меню эмодзи: {0}" @@ -279,12 +305,17 @@ msgstr "Не удалось открыть меню Fenrir: {0}" msgid "Could not open Spellcheck Menu: {0}" msgstr "Не удалось открыть меню проверки правописания: {0}" +#: internal/app/application_overrides.go:104 +#: internal/app/application_overrides.go:208 +msgid "Could not restore application settings: {0}" +msgstr "Не удалось восстановить настройки приложения: {0}" + #: internal/app/spellcheck_menu.go:155 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:164 internal/app/runtime_settings.go:191 +#: internal/app/runtime_settings.go:172 internal/app/runtime_settings.go:199 msgid "Could not save settings: {0}" msgstr "Не удалось сохранить настройки: {0}" @@ -304,6 +335,19 @@ msgstr "Кинжал" msgid "Date format" msgstr "Формат даты" +#: internal/app/application_overrides.go:230 +msgid "Default" +msgstr "По умолчанию" + +#: internal/app/application_overrides.go:129 +#: internal/app/application_overrides.go:182 +msgid "Default settings restored" +msgstr "Настройки по умолчанию восстановлены" + +#: internal/applicationoverrides/ninjam/ninjam.go:262 +msgid "Delete channel" +msgstr "Удалить канал" + #: internal/emoji/catalog.go:123 msgid "Denmark" msgstr "Дания" @@ -390,7 +434,7 @@ msgstr "Выход из режима обучения. Для повторног msgid "Exploding head" msgstr "Взрывающаяся голова" -#: internal/command/descriptions.go:113 +#: internal/command/descriptions.go:114 msgid "Export current fenrir clipboard to X or GUI clipboard" msgstr "Экспорт текущего буфера обмена fenrir в буфер обмена X или GUI" @@ -398,7 +442,7 @@ msgstr "Экспорт текущего буфера обмена fenrir в бу msgid "Face with symbols" msgstr "Лицо с символами" -#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:151 +#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:155 msgid "Failed to save settings." msgstr "Не удалось сохранить настройки." @@ -406,7 +450,7 @@ msgstr "Не удалось сохранить настройки." msgid "Fenrir Menu" msgstr "Меню Fenrir" -#: internal/app/commands.go:83 +#: internal/app/commands.go:98 msgid "Fenrir screen reader version {0}-{1}" msgstr "Программа экранного доступа Fenrir, версия {0}-{1}" @@ -442,7 +486,7 @@ msgstr "Сложенные ладони" msgid "Food" msgstr "Еда" -#: internal/app/commands.go:126 +#: internal/app/commands.go:145 msgid "Forward next keypress" msgstr "Переслать следующую клавишу." @@ -522,6 +566,14 @@ msgstr "Отступы" msgid "India" msgstr "Индия" +#: internal/applicationoverrides/ninjam/ninjam.go:259 +msgid "Input {0}" +msgstr "Вход {0}" + +#: internal/applicationoverrides/ninjam/ninjam.go:141 +msgid "Interval position {0} of {1}" +msgstr "Позиция интервала {0} из {1}" + #: internal/emoji/catalog.go:129 msgid "Ireland" msgstr "Ирландия" @@ -550,6 +602,10 @@ msgstr "Клавиатура" msgid "Last clipboard " msgstr "Последний буфер обмена. " +#: internal/applicationoverrides/ninjam/ninjam.go:145 +msgid "Latest chat message" +msgstr "Последнее сообщение чата" + #: internal/emoji/catalog.go:37 msgid "Laughing" msgstr "Смех" @@ -562,7 +618,7 @@ msgstr "Раскладка клавиатуры" msgid "Line number for cursor" msgstr "Номер строки для курсора." -#: internal/command/descriptions.go:75 +#: internal/command/descriptions.go:76 msgid "List reading regions" msgstr "Перечислить области чтения" @@ -590,10 +646,22 @@ msgstr "Динамическая область {0}: {1}" msgid "Live regions cleared" msgstr "Динамические области очищены" +#: internal/applicationoverrides/ninjam/ninjam.go:232 +msgid "Local channel {0}" +msgstr "Локальный канал {0}" + #: internal/emoji/catalog.go:87 msgid "Mage" msgstr "Маг" +#: internal/applicationoverrides/ninjam/ninjam.go:222 +msgid "Master" +msgstr "Основной выход" + +#: internal/applicationoverrides/ninjam/ninjam.go:224 +msgid "Metronome" +msgstr "Метроном" + #: internal/emoji/catalog.go:133 msgid "Mexico" msgstr "Мексика" @@ -657,6 +725,34 @@ msgstr "Тошнота" msgid "Netherlands" msgstr "Нидерланды" +#: internal/app/application_overrides.go:230 +msgid "NetworkManager" +msgstr "NetworkManager" + +#: internal/applicationoverrides/ninjam/ninjam.go:265 +msgid "New channel" +msgstr "Новый канал" + +#: internal/app/application_overrides.go:230 +msgid "Ninjam" +msgstr "Ninjam" + +#: internal/applicationoverrides/ninjam/ninjam.go:79 +msgid "Ninjam information unavailable" +msgstr "Информация Ninjam недоступна" + +#: internal/applicationoverrides/ninjam/ninjam.go:63 +msgid "" +"Ninjam regions: 1 latest chat message; 2 selected channel or control; 3 " +"connection, tempo and interval" +msgstr "" +"Области Ninjam: 1 последнее сообщение чата; 2 выбранный канал или элемент " +"управления; 3 соединение, темп и интервал" + +#: internal/applicationoverrides/ninjam/ninjam.go:77 +msgid "No chat message received" +msgstr "Сообщения чата не получены" + #: internal/interaction/grid.go:43 internal/interaction/grid.go:79 msgid "No matches" msgstr "Совпадений нет" @@ -665,6 +761,10 @@ msgstr "Совпадений нет" msgid "No reading regions set" msgstr "Области чтения не заданы" +#: internal/applicationoverrides/ninjam/ninjam.go:268 +msgid "No remote users" +msgstr "Нет удалённых пользователей" + #: internal/app/clipboard.go:416 msgid "No text found in clipboard or no accessible display" msgstr "В буфере обмена нет текста или нет доступного дисплея" @@ -741,17 +841,17 @@ msgstr "Назвать используемую версию Fenrir." msgid "Presentation" msgstr "Представление" -#: internal/command/descriptions.go:100 +#: internal/command/descriptions.go:101 msgid "" "Presents the currently selected text that will be copied to the clipboard" msgstr "" "Сообщает текущий выбранный текст, который будет скопирован в буфер обмена." -#: internal/app/commands.go:112 +#: internal/app/commands.go:131 msgid "Progress monitoring disabled" msgstr "Отслеживание хода выполнения отключено" -#: internal/app/commands.go:110 +#: internal/app/commands.go:129 msgid "Progress monitoring enabled" msgstr "Отслеживание хода выполнения включено" @@ -771,6 +871,10 @@ msgstr "Профиль пунктуации" msgid "Quick menu not available" msgstr "Быстрое меню недоступно." +#: internal/applicationoverrides/ninjam/ninjam.go:271 +msgid "Quit Ninjam" +msgstr "Выйти из Ninjam" + #: internal/emoji/catalog.go:56 msgid "Rainbow" msgstr "Радуга" @@ -787,43 +891,43 @@ msgstr "Скорость" msgid "Raves" msgstr "Рейвы" -#: internal/command/descriptions.go:78 +#: internal/command/descriptions.go:79 msgid "Read live region 1" msgstr "Прочитать динамическую область 1" -#: internal/command/descriptions.go:96 +#: internal/command/descriptions.go:97 msgid "Read live region 10" msgstr "Прочитать динамическую область 10" -#: internal/command/descriptions.go:80 +#: internal/command/descriptions.go:81 msgid "Read live region 2" msgstr "Прочитать динамическую область 2" -#: internal/command/descriptions.go:82 +#: internal/command/descriptions.go:83 msgid "Read live region 3" msgstr "Прочитать динамическую область 3" -#: internal/command/descriptions.go:84 +#: internal/command/descriptions.go:85 msgid "Read live region 4" msgstr "Прочитать динамическую область 4" -#: internal/command/descriptions.go:86 +#: internal/command/descriptions.go:87 msgid "Read live region 5" msgstr "Прочитать динамическую область 5" -#: internal/command/descriptions.go:88 +#: internal/command/descriptions.go:89 msgid "Read live region 6" msgstr "Прочитать динамическую область 6" -#: internal/command/descriptions.go:90 +#: internal/command/descriptions.go:91 msgid "Read live region 7" msgstr "Прочитать динамическую область 7" -#: internal/command/descriptions.go:92 +#: internal/command/descriptions.go:93 msgid "Read live region 8" msgstr "Прочитать динамическую область 8" -#: internal/command/descriptions.go:94 +#: internal/command/descriptions.go:95 msgid "Read live region 9" msgstr "Прочитать динамическую область 9" @@ -851,14 +955,22 @@ msgstr "Читает атрибуты текущей позиции курсор msgid "Reads from the top of the screen to the cursor position" msgstr "Чтение сначала экрана до курсора." +#: internal/applicationoverrides/ninjam/ninjam.go:229 +msgid "Remote {0}, channel {1}" +msgstr "Удалённый пользователь {0}, канал {1}" + #: internal/app/clipboard.go:264 msgid "Remove marks" msgstr "Удалить метки." -#: internal/command/descriptions.go:99 +#: internal/command/descriptions.go:100 msgid "Removes marks from selected text" msgstr "Удалить метки из выбранного текста." +#: internal/applicationoverrides/ninjam/ninjam.go:111 +msgid "Rename channel" +msgstr "Переименовать канал" + #: internal/config/config.go:161 msgid "Repeated symbols" msgstr "Повторяющиеся символы" @@ -891,7 +1003,11 @@ msgstr "Грусть" msgid "Santa" msgstr "Дед Мороз" -#: internal/command/descriptions.go:64 +#: internal/app/runtime_settings.go:151 internal/app/runtime_settings.go:168 +msgid "Save unavailable." +msgstr "Сохранение недоступно." + +#: internal/command/descriptions.go:65 msgid "Saves your current Fenrir settings so they are the default." msgstr "Сохранить текущие настройки fenrir как настройки по умолчанию." @@ -911,7 +1027,7 @@ msgstr "Говорить «ошибка»" msgid "Screaming" msgstr "Крик" -#: internal/app/commands.go:135 internal/app/scripts.go +#: internal/app/commands.go:154 internal/app/scripts.go msgid "Script failed" msgstr "Ошибка сценария" @@ -935,15 +1051,23 @@ msgstr "Поиск очищен" msgid "Search {0}" msgstr "Поиск {0}" +#: internal/command/descriptions.go:56 +msgid "Select a temporary application override" +msgstr "Выбрать временные настройки приложения" + #: internal/config/config.go:121 msgid "Select sound theme" msgstr "Выбрать звуковую тему" +#: internal/applicationoverrides/ninjam/ninjam.go:145 +msgid "Selected channel or control" +msgstr "Выбранный канал или элемент управления" + #: internal/config/config.go:128 msgid "Serial port" msgstr "Последовательный порт" -#: internal/command/descriptions.go:74 +#: internal/command/descriptions.go:75 msgid "Set a live region from one or two marks" msgstr "Задать динамическую область по одной или двум меткам" @@ -951,6 +1075,11 @@ 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 +msgid "Settings are busy" +msgstr "Настройки заняты" + #: internal/app/quick_settings.go:298 msgid "Settings saved." msgstr "Настройки сохранены" @@ -1023,7 +1152,7 @@ msgstr "Речь включена" msgid "Speech language" msgstr "Язык речи" -#: internal/app/screen_updates.go:72 +#: internal/app/screen_updates.go:80 msgid "Speech restored" msgstr "Речь восстановлена" @@ -1115,7 +1244,7 @@ msgstr "Время и дата" msgid "Time format" msgstr "Формат времени" -#: internal/command/descriptions.go:67 +#: internal/command/descriptions.go:68 msgid "Toggle progress bar monitoring with ascending tones" msgstr "Переключить отслеживание хода выполнения с повышающимися тонами" @@ -1135,7 +1264,7 @@ msgstr "Дерево" msgid "Turkey" msgstr "Индейка" -#: internal/command/descriptions.go:73 +#: internal/command/descriptions.go:74 msgid "Turn off window mode" msgstr "Выключить оконный режим" @@ -1185,6 +1314,10 @@ msgstr "" msgid "Volume" msgstr "Громкость" +#: internal/applicationoverrides/ninjam/ninjam.go:253 +msgid "Volume and pan {0}" +msgstr "Громкость и панорама {0}" + #: internal/punctuation/emoticons.go:32 msgid "WTF?" msgstr "Какого чёрта?" @@ -1276,11 +1409,11 @@ msgstr "стрелка вправо" msgid "at" msgstr "собака" -#: internal/reader/reader.go:197 +#: internal/reader/reader.go:247 msgid "autoread disabled" msgstr "Автоматическое чтение нового текста выключено" -#: internal/reader/reader.go:195 +#: internal/reader/reader.go:245 msgid "autoread enabled" msgstr "Автоматическое чтение нового текста включено" @@ -1364,7 +1497,7 @@ msgstr "символ {0}" msgid "checked" msgstr "отмечено" -#: internal/command/descriptions.go:108 +#: internal/command/descriptions.go:109 msgid "clears the currently selected clipboard" msgstr "Очищает текущий выбранный буфер обмена." @@ -1398,11 +1531,11 @@ msgstr "недоумение" msgid "cool" msgstr "круто" -#: internal/command/descriptions.go:102 +#: internal/command/descriptions.go:103 msgid "copies last presented text to the clipboard" msgstr "Копирует последний представленный текст в буфер обмена." -#: internal/command/descriptions.go:101 +#: internal/command/descriptions.go:102 msgid "copies marked text to the currently selected clipboard" msgstr "Копирует помеченный текст в выбранный буфер обмена." @@ -1426,7 +1559,7 @@ msgstr "Текущая строка." msgid "current word." msgstr "Текущее слово." -#: internal/config/definitions.go:225 internal/reader/reader.go:190 +#: internal/config/definitions.go:225 internal/reader/reader.go:240 msgid "cursor tracking" msgstr "Отслеживание курсора" @@ -1451,7 +1584,7 @@ msgstr "дефис" msgid "default" msgstr "По умолчанию" -#: internal/command/descriptions.go:66 +#: internal/command/descriptions.go:67 msgid "disables speech until next keypress" msgstr "Отключить речь пока не нажата следующая клавиша" @@ -1469,11 +1602,11 @@ msgstr "доллар" msgid "dot" msgstr "точка" -#: internal/command/descriptions.go:68 +#: internal/command/descriptions.go:69 msgid "enables or disables automatic reading of new text as it appears" msgstr "включить или отключить чтения нового текста" -#: internal/command/descriptions.go:65 +#: internal/command/descriptions.go:66 msgid "enables or disables speech" msgstr "Включение или отключение речи" @@ -1481,7 +1614,7 @@ msgstr "Включение или отключение речи" msgid "enables or disables the announcement of attributes" msgstr "Включение или отключение чтения атрибутов" -#: internal/command/descriptions.go:69 +#: internal/command/descriptions.go:70 msgid "enables or disables tracking of highlighted text" msgstr "Включает или отключает отслеживание выделенного текста" @@ -1510,7 +1643,7 @@ msgstr "равно" msgid "evil smile" msgstr "зловещая улыбка" -#: internal/command/descriptions.go:71 +#: internal/command/descriptions.go:72 msgid "exits Fenrir" msgstr "Выход из fenrir." @@ -1518,7 +1651,7 @@ msgstr "Выход из fenrir." msgid "exits review mode" msgstr "Выход из режима просмотра." -#: internal/command/descriptions.go:111 +#: internal/command/descriptions.go:112 msgid "export the current fenrir clipboard to a file" msgstr "Экспортировать текущий буфер обмена Fenrir в файл." @@ -1554,35 +1687,35 @@ msgstr "хмурое лицо" msgid "frustrated" msgstr "разочарование" -#: internal/command/descriptions.go:58 +#: internal/command/descriptions.go:59 msgid "get current quick menu entry" msgstr "Получить текущий быстрый пункт меню" -#: internal/command/descriptions.go:59 +#: internal/command/descriptions.go:60 msgid "get current quick menu value" msgstr "Получить текущее значение быстрого меню." -#: internal/command/descriptions.go:56 +#: internal/command/descriptions.go:57 msgid "get next quick menu category" msgstr "выбрать следующую категорию быстрого меню" -#: internal/command/descriptions.go:60 +#: internal/command/descriptions.go:61 msgid "get next quick menu entry" msgstr "Получить следующий быстрый пункт меню." -#: internal/command/descriptions.go:62 +#: internal/command/descriptions.go:63 msgid "get next quick menu value" msgstr "Получить следующее значение быстрого меню." -#: internal/command/descriptions.go:57 +#: internal/command/descriptions.go:58 msgid "get previous quick menu category" msgstr "выбрать предыдущую категорию быстрого меню" -#: internal/command/descriptions.go:61 +#: internal/command/descriptions.go:62 msgid "get previous quick menu entry" msgstr "Получить предыдущий пункт быстрого меню." -#: internal/command/descriptions.go:63 +#: internal/command/descriptions.go:64 msgid "get previous quick menu value" msgstr "Получить предыдущее значение быстрого меню." @@ -1621,7 +1754,7 @@ msgstr "строка заголовков задана" msgid "heart" msgstr "сердце" -#: internal/config/definitions.go:225 internal/reader/reader.go:187 +#: internal/config/definitions.go:225 internal/reader/reader.go:237 msgid "highlight tracking" msgstr "Отслеживание выделения" @@ -1629,11 +1762,11 @@ msgstr "Отслеживание выделения" msgid "hurray" msgstr "ура" -#: internal/command/descriptions.go:110 +#: internal/command/descriptions.go:111 msgid "imports text from clipboard file to the clipboard" msgstr "импортирует текст из буфера файла в буфер обмена" -#: internal/command/descriptions.go:112 +#: internal/command/descriptions.go:113 msgid "imports the graphical clipboard to Fenrir's clipboard" msgstr "Импортирует графический буфер обмена в буфер обмена Фенрира." @@ -1778,6 +1911,10 @@ msgstr "перемещает курсор обзора к предыдущему msgid "moves review to the previous line " msgstr "Переместить курсор просмотра к предыдущей строке " +#: internal/applicationoverrides/ninjam/ninjam.go:239 +msgid "muted" +msgstr "звук выключен" + #: internal/punctuation/emoticons.go:19 msgid "neutral" msgstr "нейтральное лицо" @@ -1805,6 +1942,14 @@ msgstr "нет текстовой консоли" msgid "none" msgstr "нет" +#: internal/applicationoverrides/ninjam/ninjam.go:247 +msgid "not receiving" +msgstr "приём выключен" + +#: internal/applicationoverrides/ninjam/ninjam.go:242 +msgid "not transmitting" +msgstr "передача выключена" + #: internal/punctuation/text_processing.go msgid "number" msgstr "номер" @@ -1813,7 +1958,7 @@ msgstr "номер" msgid "paste clipboard" msgstr "вставить буфер обмена" -#: internal/command/descriptions.go:109 +#: internal/command/descriptions.go:110 msgid "pastes the text from the currently selected clipboard" msgstr "Вставляет текст из текущего выбранного буфера обмена." @@ -1822,7 +1967,7 @@ msgstr "Вставляет текст из текущего выбранного msgid "percent" msgstr "процент" -#: internal/command/descriptions.go:98 +#: internal/command/descriptions.go:99 msgid "places marks to select text to copy to the clipboard" msgstr "Ставит метки для выбора текста для копирования в буфер обмена." @@ -1881,6 +2026,10 @@ msgstr "Чтение от курсора до конца экрана." msgid "reads the contents of the current screen" msgstr "Чтение содержимого текущего экрана." +#: internal/applicationoverrides/ninjam/ninjam.go:249 +msgid "receiving" +msgstr "приём включён" + #: internal/presentation/attributes.go msgid "red" msgstr "красный" @@ -1916,7 +2065,7 @@ msgstr "от строки {0}, столбца {1} до строки {2}, сто msgid "screen is empty" msgstr "Экран пустой." -#: internal/reader/reader.go:132 +#: internal/reader/reader.go:155 msgid "screen {0}" msgstr "Экран {0}" @@ -1924,19 +2073,19 @@ msgstr "Экран {0}" msgid "selected" msgstr "выделено" -#: internal/command/descriptions.go:106 +#: internal/command/descriptions.go:107 msgid "selects the first clipboard" msgstr "Выбирает первый буфер обмена." -#: internal/command/descriptions.go:107 +#: internal/command/descriptions.go:108 msgid "selects the last clipboard" msgstr "Выбирает последний буфер обмена." -#: internal/command/descriptions.go:105 +#: internal/command/descriptions.go:106 msgid "selects the next clipboard" msgstr "Выбирает следующий буфер обмена." -#: internal/command/descriptions.go:104 +#: internal/command/descriptions.go:105 msgid "selects the previous clipboard" msgstr "Выбирает предыдущий буфер обмена." @@ -1945,11 +2094,11 @@ msgstr "Выбирает предыдущий буфер обмена." msgid "semicolon" msgstr "точка с запятой" -#: internal/command/descriptions.go:70 +#: internal/command/descriptions.go:71 msgid "sends the following keypress to the terminal or application" msgstr "Отправляет следующее нажатие на терминал или приложение." -#: internal/command/descriptions.go:72 +#: internal/command/descriptions.go:73 msgid "set Window Mode, needs 2 marks " msgstr "Установка режима окна нуждается в двух метках " @@ -1992,7 +2141,7 @@ msgstr "некоторые" msgid "space" msgstr "пробел" -#: internal/command/descriptions.go:103 +#: internal/command/descriptions.go:104 msgid "speaks the contents of the currently selected clipboard" msgstr "Говорит содержимое выбранного буфера обмена." @@ -2004,7 +2153,7 @@ msgstr "речь" msgid "speech and beeps" msgstr "речь и звуковые сигналы" -#: internal/app/commands.go:59 internal/app/feedback.go:45 +#: internal/app/commands.go:64 internal/app/feedback.go:45 msgid "speech enabled" msgstr "Речь включена" @@ -2020,7 +2169,7 @@ msgstr "высота голоса" msgid "speech rate" msgstr "скорость речи" -#: internal/app/commands.go:121 +#: internal/app/commands.go:140 msgid "speech temporary disabled" msgstr "Речь временно отключена" @@ -2061,7 +2210,7 @@ msgstr "удивление" msgid "tab" msgstr "табуляция" -#: internal/config/definitions.go:225 internal/reader/reader.go:184 +#: internal/config/definitions.go:225 internal/reader/reader.go:234 msgid "table mode" msgstr "режим таблицы" @@ -2078,12 +2227,16 @@ msgstr "Переключение в режим обучения" msgid "tongue out" msgstr "высунутый язык" +#: internal/applicationoverrides/ninjam/ninjam.go:244 +msgid "transmitting" +msgstr "передача включена" + #: internal/interaction/presentation.go:62 msgid "unavailable" msgstr "недоступно" #: internal/app/tutorial.go:107 internal/app/tutorial.go:161 -#: internal/reader/reader.go:199 internal/review/review.go:140 +#: internal/reader/reader.go:249 internal/review/review.go:140 msgid "unbound" msgstr "не назначено" @@ -2095,6 +2248,10 @@ msgstr "не отмечено" msgid "underline" msgstr "Подчеркнутый" +#: internal/applicationoverrides/ninjam/ninjam.go:237 +msgid "unmuted" +msgstr "звук включён" + #: internal/presentation/attributes.go internal/presentation/attributes.go:27 msgid "white" msgstr "белый" @@ -2107,6 +2264,10 @@ msgstr "подмигивание" msgid "{0} added to dictionary" msgstr "{0} добавлено в словарь" +#: internal/app/application_overrides.go:147 +msgid "{0} application override selected" +msgstr "Выбраны настройки приложения {0}" + #: internal/interaction/presentation.go:53 msgid "{0} button" msgstr "{0}, кнопка" diff --git a/internal/input/application_overrides_test.go b/internal/input/application_overrides_test.go new file mode 100644 index 00000000..57bcb1be --- /dev/null +++ b/internal/input/application_overrides_test.go @@ -0,0 +1,48 @@ +// Copyright (c) 2026 Storm Dragon +// SPDX-License-Identifier: GPL-3.0-or-later + +package input + +import ( + "git.stormux.org/storm/fenrir/internal/command" + "testing" +) + +func TestApplicationsChordConsumesRepeatsAndBothReleaseOrders(t *testing.T) { + for _, layout := range []string{"desktop", "laptop"} { + bindings, _, err := Load_bindings([]string{"../../config/keyboard/" + layout + ".toml"}) + if err != nil { + t.Fatal(err) + } + for _, numLock := range []bool{false, true} { + for _, modifierFirst := range []bool{false, true} { + r, err := New_router(bindings) + if err != nil { + t.Fatal(err) + } + r.Key(0, KeyInsert, 1, numLock, false) + if got := r.Key(0, 30, 1, numLock, false); got.Command != command.ApplicationsMenu || got.Forward { + t.Fatal(got) + } + if got := r.Key(0, 30, 2, numLock, false); got.Command != "" || got.Forward { + t.Fatal(got) + } + releases := []uint16{30, KeyInsert} + if modifierFirst { + releases = []uint16{KeyInsert, 30} + } + for _, key := range releases { + if got := r.Key(0, key, 0, numLock, false); got.Forward || got.Command != "" { + t.Fatal(got) + } + } + if r.Held(0) { + t.Fatal("held applications chord") + } + if got := r.Key(0, 48, 1, numLock, false); !got.Forward || got.Code != 48 { + t.Fatal("subsequent application input", got) + } + } + } + } +} diff --git a/internal/input/attributes_test.go b/internal/input/attributes_test.go index b9b08d96..dcbcf3c9 100644 --- a/internal/input/attributes_test.go +++ b/internal/input/attributes_test.go @@ -24,6 +24,7 @@ func TestAttributeBindingsNumLockRepeatsAndRelease(t *testing.T) { if layout == "laptop" { code = 30 router.Key(0, KeyInsert, 1, numLock, false) + router.Key(0, KeyShift, 1, numLock, false) } for range 2 { got := router.Key(0, code, 1, numLock, false) @@ -45,6 +46,9 @@ func TestAttributeBindingsNumLockRepeatsAndRelease(t *testing.T) { if !modifierFirst && layout == "laptop" { router.Key(0, KeyInsert, 0, numLock, false) } + if layout == "laptop" { + router.Key(0, KeyShift, 0, numLock, false) + } if router.Held(0) { t.Fatal("stuck attributes chord", layout) } diff --git a/internal/input/router.go b/internal/input/router.go index 6a483a25..26f39136 100644 --- a/internal/input/router.go +++ b/internal/input/router.go @@ -66,6 +66,7 @@ const ( ) type Decision struct { + Code uint16 // physical key code, for forwarded application press/repeat hooks Forward bool Interrupt bool Keypress bool // a fresh physical press, excluding repeats and state repair @@ -191,7 +192,7 @@ func (r *Router) key(device int, key uint16, value int32, numLock, syncEvent, re } if state.forwarded { typing := !syncEvent && r.typing_key(key, numLock) - return Decision{Forward: true, Deletion: !syncEvent && deletion_key(key, numLock), Typing: typing, WordBoundary: typing && key == KeySpace, Interrupt: !syncEvent && !pure_modifier(key)} + return Decision{Code: key, Forward: true, Deletion: !syncEvent && deletion_key(key, numLock), Typing: typing, WordBoundary: typing && key == KeySpace, Interrupt: !syncEvent && !pure_modifier(key)} } commandID := state.commandID if value == 2 && repeatable(commandID) && !syncEvent { @@ -237,7 +238,7 @@ func (r *Router) key(device int, key uint16, value int32, numLock, syncEvent, re return Decision{Forward: forwarded} } typing := forwarded && r.typing_key(key, numLock) - return Decision{Keypress: true, Deletion: forwarded && deletion_key(key, numLock), Typing: typing, WordBoundary: typing && key == KeySpace, Forward: forwarded, Command: commandID, Interrupt: commandID != "" || forwarded && (!pure_modifier(key) || key == KeyCtrl || key == KeyRightCtrl)} + return Decision{Code: key, Keypress: true, Deletion: forwarded && deletion_key(key, numLock), Typing: typing, WordBoundary: typing && key == KeySpace, Forward: forwarded, Command: commandID, Interrupt: commandID != "" || forwarded && (!pure_modifier(key) || key == KeyCtrl || key == KeyRightCtrl)} } func (r *Router) typing_key(key uint16, numLock bool) bool { diff --git a/internal/prompt/shell.go b/internal/prompt/shell.go new file mode 100644 index 00000000..134f6fb3 --- /dev/null +++ b/internal/prompt/shell.go @@ -0,0 +1,37 @@ +// Copyright (c) 2026 Storm Dragon +// SPDX-License-Identifier: GPL-3.0-or-later + +package prompt + +import ( + "regexp" + "strings" + + "git.stormux.org/storm/fenrir/internal/screen" +) + +// Unlike Detector, ShellDetector excludes passwords, questions and completion +// messages. It observes raw screens, independently of application speech filters. +type ShellDetector struct { + previous map[string]int + terminal int +} + +var shellPattern = regexp.MustCompile(`^(?:[$#>]|\[[^\r\n]+\]\s*[$#>]|[a-zA-Z0-9._-]+[$#>]|[a-zA-Z0-9._-]+@[^\s<>]+.*[$#>])\s*$`) + +func (d *ShellDetector) Observe(next screen.Snapshot) bool { + if !next.Active || next.Rows <= 0 || next.Cols <= 0 || len(next.Cells) != next.Rows*next.Cols { + return false + } + counts := make(map[string]int) + for row := range next.Rows { + counts[strings.TrimSpace(next.Text(row))]++ + } + text := "" + if next.Y >= 0 && next.Y < next.Rows { + text = strings.TrimSpace(next.Text(next.Y)) + } + fresh := d.previous != nil && d.terminal == next.Terminal && shellPattern.MatchString(text) && counts[text] > d.previous[text] + d.previous, d.terminal = counts, next.Terminal + return fresh +} diff --git a/internal/prompt/shell_test.go b/internal/prompt/shell_test.go new file mode 100644 index 00000000..5e87578d --- /dev/null +++ b/internal/prompt/shell_test.go @@ -0,0 +1,46 @@ +// Copyright (c) 2026 Storm Dragon +// SPDX-License-Identifier: GPL-3.0-or-later + +package prompt + +import ( + "git.stormux.org/storm/fenrir/internal/screen" + "testing" +) + +func TestShellDetectorRequiresFreshCursorPrompt(t *testing.T) { + makeScreen := func(text string, cursor int) screen.Snapshot { + s := screen.Snapshot{Terminal: 1, Active: true, Rows: 3, Cols: 80, Y: cursor, Cells: make([]rune, 240)} + for i := range s.Cells { + s.Cells[i] = ' ' + } + copy(s.Line(1), []rune(text)) + return s + } + for _, text := range []string{"[Username@host directory] $", "Username@host:~$", "$", "#"} { + var detector ShellDetector + detector.Observe(makeScreen("application", 1)) + s := makeScreen(text, 1) + s.Generation = 1 + if !detector.Observe(s) { + t.Fatal("new shell prompt missed", text) + } + if detector.Observe(s) { + t.Fatal("unchanged prompt repeated", text) + } + } + for _, text := range []string{"Password:", "Confirm? [y/n]", " $", " $", "press any key to continue", "Worked for 2s • 1:00 PM"} { + var detector ShellDetector + detector.Observe(makeScreen("application", 1)) + if detector.Observe(makeScreen(text, 1)) { + t.Fatal("non-shell prompt", text) + } + } + var detector ShellDetector + s := makeScreen("$", 0) + detector.Observe(s) + s.Y = 1 + if detector.Observe(s) { + t.Fatal("old prompt became fresh after cursor motion") + } +} diff --git a/internal/reader/application_override.go b/internal/reader/application_override.go new file mode 100644 index 00000000..4ed7551d --- /dev/null +++ b/internal/reader/application_override.go @@ -0,0 +1,74 @@ +// Copyright (c) 2026 Storm Dragon +// SPDX-License-Identifier: GPL-3.0-or-later + +package reader + +import ( + "git.stormux.org/storm/fenrir/internal/screen" + "slices" +) + +// SessionState preserves shortcut-only state as well as configuration. Screen, +// input timestamps and detached review positions are deliberately not restored. +type SessionState struct { + automatic bool + mode, attributeConfigured, attributeMode, attributeResumeMode string + regions *terminalRegions +} + +func clone_regions(source *terminalRegions) *terminalRegions { + if source == nil { + return nil + } + result := *source + clone := func(region *screen.Region) *screen.Region { + if region == nil { + return nil + } + value := *region + return &value + } + result.window = clone(source.window) + for index, region := range source.live { + result.live[index] = clone(region) + } + return &result +} + +func (r *Reader) Capture_session() SessionState { + return r.Capture_terminal_session(r.screen.Terminal) +} + +func (r *Reader) Capture_terminal_session(terminal int) SessionState { + return SessionState{r.automatic, r.trackingMode, r.attributeConfigured, r.attributeMode, r.attributeResumeMode, clone_regions(r.regions[terminal])} +} + +func (r *Reader) Restore_session(state SessionState, terminal int) { + r.automatic = state.automatic + r.Configure_tracking(state.mode) + r.attributeConfigured, r.attributeMode, r.attributeResumeMode = state.attributeConfigured, state.attributeMode, state.attributeResumeMode + if r.regions == nil { + r.regions = make(map[int]*terminalRegions) + } + r.regions[terminal] = clone_regions(state.regions) + r.regionRevision++ + r.attributeKnown, r.indentKnown = false, false + r.Reset_progress() +} + +func (r *Reader) Clear_session_regions(terminal int) { + delete(r.regions, terminal) + r.regionRevision++ +} + +func input_snapshot(snapshot screen.Snapshot, region screen.Region) screen.Snapshot { + snapshot.Cells = slices.Clone(snapshot.Cells) + for row := range snapshot.Rows { + for col := range snapshot.Cols { + if !region.Contains(col, row, snapshot.Cols) { + snapshot.Cells[row*snapshot.Cols+col] = ' ' + } + } + } + return snapshot +} diff --git a/internal/reader/reader.go b/internal/reader/reader.go index 844eb4bd..d2c6261e 100644 --- a/internal/reader/reader.go +++ b/internal/reader/reader.go @@ -22,6 +22,7 @@ import ( "strconv" "time" + "git.stormux.org/storm/fenrir/internal/applicationoverrides" "git.stormux.org/storm/fenrir/internal/command" "git.stormux.org/storm/fenrir/internal/diagnostic" "git.stormux.org/storm/fenrir/internal/i18n" @@ -85,14 +86,18 @@ func (r *Reader) Set_mark() presentation.Message { return r.review.Set_ func (r *Reader) Clear_marks() { r.review.Clear_marks() } func (r *Reader) Marked_text(copying bool) (string, bool) { return r.review.Marked_text(copying) } -func (r *Reader) Update(next screen.Snapshot) (output []presentation.Message) { +func (r *Reader) Update(next screen.Snapshot) []presentation.Message { + return r.Update_application(next, applicationoverrides.Frame{}) +} + +func (r *Reader) Update_application(next screen.Snapshot, frame applicationoverrides.Frame) (output []presentation.Message) { r.completedWords = nil previous := r.screen // Python runs cursor-change commands only after recent direct application // input (300 ms). Reader commands and passive output must preserve review. if !r.keepReviewOnCursorChange && r.review.Active() && !r.applicationInputAt.IsZero() && time.Since(r.applicationInputAt) < 300*time.Millisecond && previous.Active && next.Active && previous.Terminal == next.Terminal && previous.Generation == next.Generation && - previous.Rows == next.Rows && previous.Cols == next.Cols && (previous.X != next.X || previous.Y != next.Y) { + previous.Rows == next.Rows && previous.Cols == next.Cols && (previous.X != next.X || previous.Y != next.Y || frame.Recognized && frame.FocusChanged) { r.review.Resume_tracking() } switched := r.review.Update(next) @@ -102,7 +107,22 @@ func (r *Reader) Update(next screen.Snapshot) (output []presentation.Message) { pendingInput := r.inputCount defer func() { r.trace_update(previous, next, switched, pendingInput, path, output) }() } + if frame.Recognized && frame.OwnsPresentation { + path = "application" + defer func() { + if !next.Active || !r.automatic { + return + } + if !r.review.Active() { + output = append(output, frame.Focus...) + } + output = append(output, frame.Updates...) + }() + } defer func() { + if frame.OwnsPresentation { + return + } if message := r.automatic_attributes(previous, next, switched); message != nil { output = append([]presentation.Message{*message}, output...) } @@ -129,6 +149,9 @@ func (r *Reader) Update(next screen.Snapshot) (output []presentation.Message) { path = "automatic-disabled" return nil } + if frame.OwnsPresentation { + return nil + } output := []presentation.Message{{Text: i18n.Format("screen {0}", strconv.Itoa(next.Terminal)), Cue: presentation.TTYChanged}} if r.region_state().allows(next.X, next.Y, next.Cols, true, true) { output = append(output, presentation.Line_message(r.Automatic_snapshot(), next.Y)) @@ -138,9 +161,17 @@ func (r *Reader) Update(next screen.Snapshot) (output []presentation.Message) { regions := r.region_state() typingPrevious := regions.filtered_snapshot(previous, false, true) typingNext := regions.filtered_snapshot(next, false, true) + if frame.OwnsPresentation { + if frame.Input == nil { + r.inputCount = 0 + } else { + typingPrevious = input_snapshot(typingPrevious, *frame.Input) + typingNext = input_snapshot(typingNext, *frame.Input) + } + } if echo, matched := r.typing_echo(typingPrevious, typingNext); matched { r.progress.Reset() - if r.progressEnabled && regions.has_live() { + if !frame.OwnsPresentation && r.progressEnabled && regions.has_live() { // Typing can update a ruler/progress region in the same snapshot. // Inspect only live regions here, so typed percentages elsewhere // cannot become progress cues, even on the same physical row. @@ -150,7 +181,13 @@ func (r *Reader) Update(next screen.Snapshot) (output []presentation.Message) { return echo } deleted := r.match_deletion(previous, next) - if r.progressEnabled { + if frame.OwnsPresentation && frame.Input == nil { + deleted = nil + } + if frame.OwnsPresentation { + r.progress.Reset() + } + if !frame.OwnsPresentation && r.progressEnabled { deletionRow := -1 if len(deleted) > 0 { deletionRow = next.Y @@ -165,6 +202,9 @@ func (r *Reader) Update(next screen.Snapshot) (output []presentation.Message) { } return nil } + if frame.OwnsPresentation { + return r.deletion_echo(deleted, next) + } readingPrevious := regions.filtered_snapshot(previous, true, true) readingNext := r.Automatic_snapshot() if r.trackingMode == "highlight" && !r.review.Active() { diff --git a/internal/terminal/application_override_linux_test.go b/internal/terminal/application_override_linux_test.go new file mode 100644 index 00000000..6c04a1ea --- /dev/null +++ b/internal/terminal/application_override_linux_test.go @@ -0,0 +1,72 @@ +// Copyright (c) 2026 Storm Dragon +// SPDX-License-Identifier: GPL-3.0-or-later + +//go:build linux && cgo + +package terminal + +import ( + "fmt" + "slices" + "testing" + + "git.stormux.org/storm/fenrir/internal/applicationoverrides/ninjam" + "git.stormux.org/storm/fenrir/internal/command" + "git.stormux.org/storm/fenrir/internal/reader" +) + +// Exercise actual libvterm attribute decoding, including ncurses' monochrome +// standout path. These are source-derived terminal writes, not a running client. +func TestNinjamOverrideTerminalPresentation(t *testing.T) { + for _, monochrome := range []bool{false, true} { + t.Run(fmt.Sprint(monochrome), func(t *testing.T) { + e, err := new_emulator(24, 100) + if err != nil { + t.Fatal(err) + } + defer e.close() + r, a := reader.New(), &ninjam.Adapter{} + paint := func(selected int, volume, chat string) []string { + e.feed([]byte("\x1b[0m\x1b[2J\x1b[HLOCAL\x1b[2;1H")) + field := 0 + for _, char := range " master: [ ]mute [" + volume + "] | metronome: [ ]mute [0.0dB center]" { + if char == ']' { + e.feed([]byte("\x1b[0m")) + } + e.feed([]byte(string(char))) + if char == '[' { + style := "\x1b[1m" + if field == selected { + style = "\x1b[0;30;47m" + if monochrome { + style = "\x1b[0;7m" + } + } + e.feed([]byte(style)) + field++ + } + } + e.feed([]byte("\x1b[17;1HCHAT\x1b[20;1H old\x1b[21;1H" + chat + "\x1b[23;1H#####\x1b[24;1H[QUIT NINJAM] : fixture.invalid : 120.0BPM 16BPI\x1b[24;1H")) + s := e.snapshot(true, false) + var result []string + for _, message := range r.Update_application(s, a.Observe(s)) { + if message.Text != "" { + result = append(result, message.Text) + } + } + return result + } + if got := paint(0, "0.0dB center", ""); !slices.Equal(got, []string{"Master unmuted"}) { + t.Fatal(got) + } + r.Command(command.CurrentLine) + r.Note_input(true, false, false) + if got := paint(1, "0.0dB center", " new"); !slices.Equal(got, []string{"Master Volume and pan 0.0dB center", " new"}) { + t.Fatal(got) + } + if got := paint(1, "-0.5dB center", " new"); !slices.Equal(got, []string{"Master Volume and pan -0.5dB center"}) { + t.Fatal(got) + } + }) + } +} diff --git a/internal/terminal/application_override_native_linux_test.go b/internal/terminal/application_override_native_linux_test.go new file mode 100644 index 00000000..ca744585 --- /dev/null +++ b/internal/terminal/application_override_native_linux_test.go @@ -0,0 +1,113 @@ +// Copyright (c) 2026 Storm Dragon +// SPDX-License-Identifier: GPL-3.0-or-later + +//go:build linux && cgo + +package terminal + +import ( + "errors" + "os" + "os/exec" + "slices" + "strings" + "syscall" + "testing" + "time" + + "git.stormux.org/storm/fenrir/internal/applicationoverrides/networkmanager" + "git.stormux.org/storm/fenrir/internal/screen" +) + +// Opt-in read-only application check: open nmtui's main menu, move its +// selection, and exit. No connection, hostname or network setting is changed. +func TestNetworkManagerOverrideNative(t *testing.T) { + if os.Getenv("FENRIR_APPLICATION_OVERRIDE_NATIVE") != "1" { + t.Skip("set FENRIR_APPLICATION_OVERRIDE_NATIVE=1 for private nmtui/Screen PTY checks") + } + nmtui, err := exec.LookPath("nmtui") + if err != nil { + t.Skip("nmtui unavailable") + } + for _, multiplexed := range []bool{false, true} { + name := "direct" + if multiplexed { + name = "screen" + } + t.Run(name, func(t *testing.T) { + env := append(os.Environ(), "TERM=xterm-256color", "LANG=C", "LC_ALL=C", "NEWT_COLORS=") + cmd := exec.Command(nmtui) + if multiplexed { + path, err := exec.LookPath("screen") + if err != nil { + t.Skip("GNU Screen unavailable") + } + socketDirectory := t.TempDir() + if err := os.Chmod(socketDirectory, 0700); err != nil { + t.Fatal(err) + } + env = append(env, "SCREENDIR="+socketDirectory) + cmd = exec.Command(path, "-c", "/dev/null", "-m", "-S", "fenrir-override-test", nmtui) + defer func() { + quit := exec.Command(path, "-S", "fenrir-override-test", "-X", "quit") + quit.Env = env + _ = quit.Run() + }() + } + master, slave, err := open_pty(24, 100) + if err != nil { + t.Fatal(err) + } + defer syscall.Close(master) + slaveFile := os.NewFile(uintptr(slave), "private nmtui PTY") + defer slaveFile.Close() + cmd.Stdin, cmd.Stdout, cmd.Stderr = slaveFile, slaveFile, slaveFile + cmd.Env = env + cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true, Setctty: true, Ctty: 0} + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + defer func() { _ = cmd.Process.Kill(); _ = cmd.Wait() }() + if err := syscall.SetNonblock(master, true); err != nil { + t.Fatal(err) + } + emulator, err := new_emulator(24, 100) + if err != nil { + t.Fatal(err) + } + defer emulator.close() + adapter := &networkmanager.Adapter{} + var before []screen.Attribute + waitForMenu := func() { + t.Helper() + deadline := time.Now().Add(4 * time.Second) + var buffer [32768]byte + for time.Now().Before(deadline) { + n, err := syscall.Read(master, buffer[:]) + if n > 0 { + emulator.feed(buffer[:n]) + } + if err != nil && !errors.Is(err, syscall.EAGAIN) && !errors.Is(err, syscall.EINTR) { + t.Fatalf("nmtui PTY: %v; screen: %s", err, string(emulator.snapshot(true, false).Cells)) + } + snapshot := emulator.snapshot(true, false) + if adapter.Observe(snapshot).Recognized && (before == nil || !slices.Equal(before, snapshot.Attributes)) { + return + } + time.Sleep(10 * time.Millisecond) + } + t.Fatalf("nmtui menu not recognized: %s", strings.TrimSpace(string(emulator.snapshot(true, false).Cells))) + } + waitForMenu() + before = slices.Clone(emulator.snapshot(true, false).Attributes) + // GNU Screen and xterm both forward the normal arrow sequence. + if _, err := syscall.Write(master, []byte("\x1bOB")); err != nil { + t.Fatal(err) + } + waitForMenu() + if _, err := syscall.Write(master, []byte("\x1b")); err != nil { + t.Fatal(err) + } + }) + } +} diff --git a/internal/terminal/ninjam_live_linux_test.go b/internal/terminal/ninjam_live_linux_test.go new file mode 100644 index 00000000..d9fb65d6 --- /dev/null +++ b/internal/terminal/ninjam_live_linux_test.go @@ -0,0 +1,463 @@ +// Copyright (c) 2026 Storm Dragon +// SPDX-License-Identifier: GPL-3.0-or-later + +//go:build linux && cgo + +package terminal + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + "testing" + "time" + "unsafe" + + "git.stormux.org/storm/fenrir/internal/applicationoverrides/ninjam" + "git.stormux.org/storm/fenrir/internal/command" + "git.stormux.org/storm/fenrir/internal/prompt" + "git.stormux.org/storm/fenrir/internal/reader" +) + +// Live server addresses and acceptance of the server's connection license are +// explicit opt-ins. Chat is a private message to this fixture's own account. +// The required preload clocks/zeros ALSA null capture; no real audio is used. +func TestNinjamLiveOverride(t *testing.T) { + host, port := os.Getenv("FENRIR_NINJAM_TEST_HOST"), os.Getenv("FENRIR_NINJAM_TEST_PORT") + preload := os.Getenv("FENRIR_NINJAM_NULL_AUDIO") + if host == "" || port == "" || preload == "" || os.Getenv("FENRIR_NINJAM_TEST_LICENSE") != "1" { + t.Skip("set FENRIR_NINJAM_TEST_HOST/PORT, FENRIR_NINJAM_NULL_AUDIO and FENRIR_NINJAM_TEST_LICENSE=1 for an authorized live server test") + } + program, err := exec.LookPath("cninjam") + if err != nil { + t.Fatal(err) + } + root := os.Getenv("FENRIR_NINJAM_TEST_ARTIFACTS") + if root == "" { + root, err = os.MkdirTemp("", "fenrir-ninjam-live-") + if err != nil { + t.Fatal(err) + } + } + if err := os.MkdirAll(root, 0700); err != nil { + t.Fatal(err) + } + t.Log("Retained live-test artifacts:", root) + // Every invocation gets a distinct private client directory and identity. + clientDir, err := os.MkdirTemp(root, "client-") + if err != nil { + t.Fatal(err) + } + username := fmt.Sprintf("Username%04x", time.Now().UnixNano()&0xffff) + configuration := "master mastervol 1 masterpan 0 metrovol 0 metropan 0 mastermute 0 metromute 1\nlocal 0 source 0 bc 0 mute 1 volume 0 pan 0 name \"Fixture Channel\"\n" + if err := os.WriteFile(filepath.Join(clientDir, "ninjam.config"), []byte(configuration), 0600); err != nil { + t.Fatal(err) + } + raw, err := os.OpenFile(filepath.Join(clientDir, "terminal.ansi"), os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + t.Fatal(err) + } + defer raw.Close() + trace, err := os.OpenFile(filepath.Join(clientDir, "presentation.jsonl"), os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + t.Fatal(err) + } + defer trace.Close() + encoder := json.NewEncoder(trace) + master, slave, err := open_pty(24, 100) + if err != nil { + t.Fatal(err) + } + defer syscall.Close(master) + slaveFile := os.NewFile(uintptr(slave), "private cninjam PTY") + defer slaveFile.Close() + args := []string{"-c", `"$@"; result=$?; printf '%s' '[Username@host test] $ '; exit "$result"`, "fixture", program, host + ":" + port, "-user", "anonymous:" + username, "-alsaconfig", "in null out null srate 48000 nch 2 bps 16 bsize 2048 nblock 4", "-nosavesourcefiles", "-nosavelocal", "-nowritelog", "-sessiondir", filepath.Join(clientDir, "session")} + child := exec.Command("/bin/sh", args...) + child.Dir = clientDir + child.Env = append(os.Environ(), "HOME="+clientDir, "TERM=xterm-256color", "LANG=C", "LC_ALL=C", "ALSA_CONFIG_PATH=/usr/share/alsa/alsa.conf", "LD_PRELOAD="+preload) + child.Stdin, child.Stdout, child.Stderr = slaveFile, slaveFile, slaveFile + child.SysProcAttr = &syscall.SysProcAttr{Setsid: true, Setctty: true, Ctty: 0} + if err := child.Start(); err != nil { + t.Fatal(err) + } + defer func() { _ = syscall.Kill(-child.Process.Pid, syscall.SIGTERM); _ = child.Wait() }() + if err := syscall.SetNonblock(master, true); err != nil { + t.Fatal(err) + } + emulator, err := new_emulator(24, 100) + if err != nil { + t.Fatal(err) + } + defer emulator.close() + engine := reader.New() + var adapter *ninjam.Adapter + var output []string + peerPump := func() {} + pump := func() { + peerPump() + var buffer [32768]byte + for total := 0; total < 131072; { + count, err := syscall.Read(master, buffer[:]) + if count > 0 { + total += count + emulator.feed(buffer[:count]) + if _, err := raw.Write(buffer[:count]); err != nil { + t.Fatal(err) + } + } + if errors.Is(err, syscall.EAGAIN) || errors.Is(err, syscall.EIO) || count == 0 { + break + } + if err != nil && !errors.Is(err, syscall.EINTR) { + t.Fatal(err) + } + } + snapshot := emulator.snapshot(true, false) + if adapter != nil { + frame := adapter.Observe(snapshot) + for _, message := range engine.Update_application(snapshot, frame) { + if message.Text != "" { + output = append(output, message.Text) + if err := encoder.Encode(map[string]any{"at": time.Now().UTC(), "text": message.Text}); err != nil { + t.Fatal(err) + } + } + } + } + } + screenText := func() string { + snapshot := emulator.snapshot(true, false) + var lines []string + for row := range snapshot.Rows { + lines = append(lines, snapshot.Text(row)) + } + return strings.Join(lines, "\n") + } + saveScreen := func(name string) { + if err := os.WriteFile(filepath.Join(clientDir, name+".txt"), []byte(screenText()), 0600); err != nil { + t.Fatal(err) + } + } + defer saveScreen("final-screen") + waitFor := func(label string, timeout time.Duration, predicate func() bool) { + t.Helper() + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + pump() + if predicate() { + return + } + time.Sleep(10 * time.Millisecond) + } + saveScreen("failure-screen") + t.Fatalf("%s not observed; see retained screen and presentation trace", label) + } + write := func(data string) { + t.Helper() + if _, err := syscall.Write(master, []byte(data)); err != nil { + t.Fatal(err) + } + } + focus := func() string { + if adapter == nil { + return "" + } + result, _ := adapter.Command(command.ReadLiveRegion2) + return result.Text + } + waitFor("server license or connection", 12*time.Second, func() bool { + return strings.Contains(screenText(), "Hit Y to agree") || strings.Contains(screenText(), "Use the arrows or pagedown to scroll down") + }) + for page := 0; page < 20 && !strings.Contains(screenText(), "Hit Y to agree"); page++ { + write("\x1b[6~") + deadline := time.Now().Add(100 * time.Millisecond) + for time.Now().Before(deadline) { + pump() + time.Sleep(10 * time.Millisecond) + } + } + saveScreen("license") + if !strings.Contains(screenText(), "Hit Y to agree") { + t.Fatal("server license could not be fully displayed") + } + write("y") + waitFor("connected main view", 12*time.Second, func() bool { + return strings.Contains(screenText(), "[QUIT NINJAM]") && !strings.Contains(screenText(), ": 0.0BPM") + }) + saveScreen("connected-screen") + adapter = &ninjam.Adapter{} + engine.Configure_tracking("highlight") + pump() + if focus() != "Master unmuted" { + t.Fatal("initial master control:", focus()) + } + send := func(data string) { engine.Note_input(true, false, false); write(data) } + send("\r") + waitFor("master muted", 2*time.Second, func() bool { return focus() == "Master muted" }) + send("\r") + waitFor("master unmuted", 2*time.Second, func() bool { return focus() == "Master unmuted" }) + engine.Command(command.CurrentLine) + start := len(output) + send("\x1bOC") + waitFor("volume focus after manual review", 2*time.Second, func() bool { + return strings.Contains(focus(), "Master Volume and pan") && strings.Contains(strings.Join(output[start:], "\n"), focus()) + }) + start = len(output) + send("\r") + waitFor("volume editing instructions", 2*time.Second, func() bool { return strings.Contains(strings.Join(output[start:], "\n"), "USE UP AND DOWN FOR VOLUME") }) + oldValue := focus() + start = len(output) + send("\x1bOA") + waitFor("edited volume", 2*time.Second, func() bool { + return focus() != oldValue && strings.Contains(focus(), "Master Volume and pan") && strings.Contains(strings.Join(output[start:], "\n"), focus()) + }) + oldValue = focus() + start = len(output) + send("\x1bOD") + waitFor("edited pan", 2*time.Second, func() bool { + return focus() != oldValue && strings.Contains(focus(), "Master Volume and pan") && strings.Contains(strings.Join(output[start:], "\n"), focus()) + }) + send("\r") + start = len(output) + deadline := time.Now().Add(1300 * time.Millisecond) + for time.Now().Before(deadline) { + pump() + time.Sleep(10 * time.Millisecond) + } + for _, text := range output[start:] { + if strings.Contains(text, "###") || strings.Contains(text, "<-120.0dB>") { + t.Fatal("meter chatter:", text) + } + } + send("\x1bOB") + waitFor("local channel", 2*time.Second, func() bool { return strings.Contains(focus(), "Local channel Fixture Channel") }) + send("\x1bOD") + waitFor("local channel name", 2*time.Second, func() bool { return strings.Contains(focus(), "Channel name Fixture Channel") }) + send("\r") + waitFor("rename prompt", 2*time.Second, func() bool { return focus() == "Rename channel" }) + write("Fixture Renamed\r") + waitFor("renamed local channel", 2*time.Second, func() bool { return strings.Contains(focus(), "Channel name Fixture Renamed") }) + send("\x1bOC") + waitFor("local transmit control", 2*time.Second, func() bool { return strings.HasSuffix(focus(), " not transmitting") }) + send("\x1bOC") + waitFor("local input control", 2*time.Second, func() bool { return strings.Contains(focus(), " Input ") }) + start = len(output) + send("\r") + waitFor("input selection prompt", 2*time.Second, func() bool { + return strings.Contains(strings.Join(output[start:], "\n"), "USE ARROW KEYS TO SELECT THE INPUT CHANNEL") + }) + oldValue = focus() + send("\x1bOB") + waitFor("changed input selection", 2*time.Second, func() bool { return focus() != oldValue && strings.Contains(focus(), " Input ") }) + send("\r") + send("\x1bOC") + waitFor("local mute control", 2*time.Second, func() bool { return strings.HasSuffix(focus(), " muted") && !strings.HasSuffix(focus(), " unmuted") }) + send("\r") + waitFor("local unmuted", 2*time.Second, func() bool { return strings.HasSuffix(focus(), " unmuted") }) + send("\r") + waitFor("local muted", 2*time.Second, func() bool { return strings.HasSuffix(focus(), " muted") && !strings.HasSuffix(focus(), " unmuted") }) + if os.Getenv("FENRIR_NINJAM_TEST_PEER") == "1" { + peerStep, peerFocus, peerWrite := start_ninjam_live_peer(t, program, host+":"+port, preload, root) + peerPump = peerStep + waitFor("silent test peer channel", 12*time.Second, func() bool { return strings.Contains(screenText(), "Peer Fixture") }) + for count := 0; count < 64 && !strings.Contains(focus(), "channel Peer Fixture"); count++ { + send("\x1bOB") + deadline := time.Now().Add(60 * time.Millisecond) + for time.Now().Before(deadline) { + pump() + time.Sleep(10 * time.Millisecond) + } + } + if !strings.Contains(focus(), "channel Peer Fixture") { + t.Fatal("remote test channel not reached") + } + for count := 0; count < 3; count++ { + send("\x1bOD") + deadline := time.Now().Add(30 * time.Millisecond) + for time.Now().Before(deadline) { + pump() + time.Sleep(10 * time.Millisecond) + } + } + if !strings.HasSuffix(focus(), " receiving") { + t.Fatal("remote receive control", focus()) + } + send("\r") + waitFor("remote not receiving", 2*time.Second, func() bool { return strings.HasSuffix(focus(), " not receiving") }) + send("\r") + waitFor("remote receiving", 2*time.Second, func() bool { + return strings.HasSuffix(focus(), " receiving") && !strings.HasSuffix(focus(), " not receiving") + }) + send("\x1bOC") + waitFor("remote mute control", 2*time.Second, func() bool { return strings.HasSuffix(focus(), " unmuted") }) + send("\r") + waitFor("remote muted", 2*time.Second, func() bool { return strings.HasSuffix(focus(), " muted") && !strings.HasSuffix(focus(), " unmuted") }) + send("\r") + waitFor("remote unmuted", 2*time.Second, func() bool { return strings.HasSuffix(focus(), " unmuted") }) + for count := 0; count < 64 && peerFocus() != "Chat message"; count++ { + peerWrite("\x1bOB") + deadline := time.Now().Add(60 * time.Millisecond) + for time.Now().Before(deadline) { + pump() + time.Sleep(10 * time.Millisecond) + } + } + if peerFocus() != "Chat message" { + t.Fatal("peer chat input not reached") + } + engine.Command(command.CurrentLine) + marker := fmt.Sprintf("Fenrir-peer-test-%x", time.Now().UnixNano()&0xffff) + start = len(output) + peerWrite("/msg " + username + " " + marker + "\r") + waitFor("passive incoming peer chat", 5*time.Second, func() bool { return strings.Contains(strings.Join(output[start:], "\n"), marker) }) + if report := engine.Command(command.CursorLine); !strings.Contains(report.After.Text, "review cursor") { + t.Fatal("passive chat ended manual review", report) + } + result, _ := adapter.Command(command.ReadLiveRegion1) + if !strings.Contains(result.Text, marker) { + t.Fatal("peer chat missing from recall") + } + saveScreen("remote-peer-screen") + t.Log("PASS: remote receive/mute states and private peer chat during manual review; test peer transmits only clocked zeros") + } + // Match both the actual PTY and libvterm geometry. cninjam receives SIGWINCH + // from the kernel, then redraws its own dynamically positioned chat area. + dimensions := struct{ Rows, Cols, X, Y uint16 }{Rows: 30, Cols: 120} + if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(master), syscall.TIOCSWINSZ, uintptr(unsafe.Pointer(&dimensions))); errno != 0 { + t.Fatal(errno) + } + emulator.resize(30, 120) + waitFor("resized application regions", 3*time.Second, func() bool { + result, _ := adapter.Command(command.ReadLiveRegion3) + return strings.Contains(result.Text, "of 120") && strings.Contains(emulator.snapshot(true, false).Text(29), "[QUIT NINJAM]") + }) + saveScreen("resized-screen") + for count := 0; count < 64 && focus() != "Chat message"; count++ { + send("\x1bOB") + deadline := time.Now().Add(60 * time.Millisecond) + for time.Now().Before(deadline) { + pump() + time.Sleep(10 * time.Millisecond) + } + } + if focus() != "Chat message" { + t.Fatal("chat input not reached:", focus()) + } + marker := fmt.Sprintf("Fenrir-private-test-%x", time.Now().UnixNano()&0xffff) + start = len(output) + write("/msg " + username + " " + marker + "\r") + waitFor("server-delivered private self-message", 5*time.Second, func() bool { + result, _ := adapter.Command(command.ReadLiveRegion1) + return strings.Contains(result.Text, marker) && strings.HasPrefix(result.Text, "*") + }) + if !strings.Contains(strings.Join(output[start:], "\n"), marker) { + t.Fatal("chat updated recall without automatic output") + } + for _, id := range []command.ID{command.ReadLiveRegion1, command.ReadLiveRegion2, command.ReadLiveRegion3} { + result, handled := adapter.Command(id) + if !handled || result.Text == "" { + t.Fatal("empty semantic region", id) + } + } + saveScreen("chat-screen") + var shell prompt.ShellDetector + shell.Observe(emulator.snapshot(true, false)) + send("\x1bOB") + waitFor("quit control", 2*time.Second, func() bool { return focus() == "Quit Ninjam" }) + send("\r") + waitFor("real shell return", 5*time.Second, func() bool { return strings.Contains(screenText(), "[Username@host test] $") }) + if !shell.Observe(emulator.snapshot(true, false)) { + t.Fatal("fresh shell prompt after real cninjam exit was not recognized") + } + t.Log("PASS: live master/local mute, volume/pan, navigation after review, quiet meters, rename/input prompts, resize, private incoming chat, semantic region reads, and shell return") +} + +// start_ninjam_live_peer adds a clocked-silence test channel, never a microphone. +// It exists only in the explicit two-account test and is cleaned up by t.Cleanup. +func start_ninjam_live_peer(t *testing.T, program, address, preload, root string) (func(), func() string, func(string)) { + t.Helper() + directory, err := os.MkdirTemp(root, "peer-") + if err != nil { + t.Fatal(err) + } + configuration := "master mastervol 0 masterpan 0 metrovol 0 metropan 0 mastermute 1 metromute 1\nlocal 0 source 0 bc 1 mute 1 volume 0 pan 0 name \"Peer Fixture\"\n" + if err := os.WriteFile(filepath.Join(directory, "ninjam.config"), []byte(configuration), 0600); err != nil { + t.Fatal(err) + } + master, slave, err := open_pty(24, 100) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = syscall.Close(master) }) + slaveFile := os.NewFile(uintptr(slave), "private cninjam peer PTY") + t.Cleanup(func() { _ = slaveFile.Close() }) + name := fmt.Sprintf("UsernamePeer%04x", time.Now().UnixNano()&0xffff) + child := exec.Command(program, address, "-user", "anonymous:"+name, "-alsaconfig", "in null out null srate 48000 nch 2 bps 16 bsize 2048 nblock 4", "-nosavesourcefiles", "-nosavelocal", "-nowritelog", "-sessiondir", filepath.Join(directory, "session")) + child.Dir = directory + child.Env = append(os.Environ(), "HOME="+directory, "TERM=xterm-256color", "LANG=C", "LC_ALL=C", "ALSA_CONFIG_PATH=/usr/share/alsa/alsa.conf", "LD_PRELOAD="+preload) + child.Stdin, child.Stdout, child.Stderr = slaveFile, slaveFile, slaveFile + child.SysProcAttr = &syscall.SysProcAttr{Setsid: true, Setctty: true, Ctty: 0} + if err := child.Start(); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = syscall.Kill(-child.Process.Pid, syscall.SIGTERM); _ = child.Wait() }) + if err := syscall.SetNonblock(master, true); err != nil { + t.Fatal(err) + } + emulator, err := new_emulator(24, 100) + if err != nil { + t.Fatal(err) + } + t.Cleanup(emulator.close) + adapter := &ninjam.Adapter{} + raw, err := os.OpenFile(filepath.Join(directory, "terminal.ansi"), os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = raw.Close() }) + write := func(data string) { + t.Helper() + if _, err := syscall.Write(master, []byte(data)); err != nil { + t.Fatal(err) + } + } + accepted := false + lastPage := time.Time{} + pump := func() { + var buffer [32768]byte + for total := 0; total < 131072; { + count, err := syscall.Read(master, buffer[:]) + if count > 0 { + total += count + emulator.feed(buffer[:count]) + if _, err := raw.Write(buffer[:count]); err != nil { + t.Fatal(err) + } + } + if errors.Is(err, syscall.EAGAIN) || errors.Is(err, syscall.EIO) || count == 0 { + break + } + if err != nil && !errors.Is(err, syscall.EINTR) { + t.Fatal(err) + } + } + snapshot := emulator.snapshot(true, false) + text := string(snapshot.Cells) + if !accepted && strings.Contains(text, "Hit Y to agree") { + write("y") + accepted = true + } + if !accepted && strings.Contains(text, "Use the arrows or pagedown to scroll down") && time.Since(lastPage) > 100*time.Millisecond { + write("\x1b[6~") + lastPage = time.Now() + } + adapter.Observe(snapshot) + } + focus := func() string { result, _ := adapter.Command(command.ReadLiveRegion2); return result.Text } + return pump, focus, write +} diff --git a/tests/native_application_overrides.py b/tests/native_application_overrides.py new file mode 100644 index 00000000..a39e7b45 --- /dev/null +++ b/tests/native_application_overrides.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +"""Private X11 application-override fixture using source-derived Ninjam output. + +Run: xvfb-run -a env FENRIR_PRIVATE_X11=1 FENRIR_TEST_BINARY=/tmp/fenrir-test \ + python3 tests/native_application_overrides.py +Uses fake speech and a private PTY; does not establish audible/physical acceptance. +""" + +import json +import os +from pathlib import Path +import signal +import socket +import subprocess +import sys +import tempfile +import tomllib +import tty + + +def draw(selected, messages): + output = "\x1b[0m\x1b[2J\x1b[HLOCAL\x1b[2;1H" + field = 0 + for char in " master: [ ]mute [0.0dB center] | metronome: [ ]mute [0.0dB center]": + if char == "]": + output += "\x1b[0m" + output += char + if char == "[": + output += "\x1b[0;30;47m" if selected == field else "\x1b[1m" + field += 1 + output += "\x1b[17;1HCHAT" + for row, message in enumerate(messages[-4:], 21 - min(len(messages), 4)): + output += f"\x1b[{row + 1};1H{message}" + return (output + "\x1b[23;1H#####\x1b[24;1H[QUIT NINJAM] : fixture.invalid : " + "120.0BPM 16BPI\x1b[24;1H").encode() + + +def fixture_shell(): + root = Path(os.environ["FENRIR_FIXTURE_DIR"]) + tty.setraw(0) + (root / "shell-pid").write_text(str(os.getpid())) + os.write(1, draw(0, [" old"])) + with (root / "input").open("ab", buffering=0) as record: + while True: + data = os.read(0, 4096) + if not data: + return + record.write(data) + if b"q" in data: + os.write(1, b"\x1b[0m\x1b[2J\x1b[H[Username@host directory] $ ") + elif data in (b"\x1b[C", b"\x1bOC"): + os.write(1, draw(1, [" old", " incoming"])) + + +def main(): + from native_terminal import KeyboardFixture, SpeechServer, wait_for + + if os.environ.get("FENRIR_PRIVATE_X11") != "1" or not os.environ.get("DISPLAY"): + raise SystemExit("Use xvfb-run -a env FENRIR_PRIVATE_X11=1") + repo = Path(__file__).resolve().parents[1] + script = Path(__file__).resolve() + binary = os.environ.get("FENRIR_TEST_BINARY", str(repo / "fenrir")) + for layout in ("desktop", "laptop"): + with tempfile.TemporaryDirectory(prefix="fenrir-overrides-", dir="/tmp") as directory: + root = Path(directory) + (root / "keyboard").mkdir() + for name in ("custom", "laptop"): + source = "desktop" if name == "custom" else "laptop" + (root / "keyboard" / (name + ".toml")).write_bytes( + (repo / "config/keyboard" / (source + ".toml")).read_bytes()) + (root / "mode").write_text("laptop" if layout == "laptop" else "fixture") + (root / "home").mkdir() + (root / "settings.toml").write_text( + 'version=3\n[presentation.sound]\nenabled=false\n' + '[presentation.speech]\nrate=0\n' + f'[remote]\nsocket_file="{root / "remote.sock"}"\n') + shell = root / "shell" + shell.write_text(f'#!{sys.executable}\nimport runpy, sys\nsys.argv=[{str(script)!r}, "--shell"]\n' + f'runpy.run_path({str(script)!r}, run_name="__main__")\n') + shell.chmod(0o700) + server = SpeechServer(root / "speech.sock") + keyboard = KeyboardFixture() + env = dict(os.environ, HOME=str(root / "home"), TMPDIR=str(root), + FENRIR_FIXTURE_DIR=str(root), FENRIR_TEST_BINARY=binary, + SPEECHD_ADDRESS="unix_socket:" + str(root / "speech.sock"), + LOGNAME="Username", USER="Username", LC_ALL="C", LANGUAGE="C") + process = subprocess.Popen(["xterm", "-geometry", "100x24", "-e", sys.executable, + str(repo / "tests/native_terminal.py"), "--outer", str(root)], env=env) + state = None + try: + wait_for(lambda: (root / "started").exists(), "reader startup") + state = json.loads((root / "started").read_text()) + subprocess.run(["xdotool", "windowfocus", "--sync", str(state["window"])], check=True) + + def spoken(text, start): + wait_for(lambda: any(text in item for item in server.events[start:]), + f"speech {text!r}; recent={server.events[-12:]}") + + def action(chord, expected): + start = len(server.events) + keyboard.key(chord) + spoken(expected, start) + + def remote(text): + with socket.socket(socket.AF_UNIX) as connection: + connection.settimeout(3) + connection.connect(str(root / "remote.sock")) + connection.sendall((text + "\n").encode()) + connection.shutdown(socket.SHUT_WR) + return connection.recv(4096).decode().strip() + + wait_for(lambda: (root / "remote.sock").exists(), "remote ready") + action("Insert+Up", "10") + action("Insert+a", "Application Default") + action("Down", "NetworkManager") + action("Down", "Ninjam") + action("Escape", "Applications closed") + action("Insert+a", "Application Default") + action("Down", "NetworkManager") + action("Down", "Ninjam") + 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." + assert not (root / "other.toml").exists() + assert (root / "settings.toml").read_bytes() == original + # Start detached review, then deliver actual XTest application + # navigation. The fixture redraw includes focus and chat together. + action("Insert+i" if layout == "laptop" else "KP_Up", "QUIT NINJAM") + start = len(server.events) + keyboard.key("Right") + spoken("Master Volume and pan", start) + spoken("incoming", start) + action("Insert+1", "incoming") + action("Insert+2", "Master Volume and pan") + action("Insert+3", "120.0BPM") + action("Insert+a", "Application Ninjam") + action("Down", "Default") + action("Return", "Default settings restored") + action("Insert+space", "10") + action("Insert+a", "Application Default") + action("Down", "NetworkManager") + action("Down", "Ninjam") + action("Return", "Ninjam application override selected") + action("q", "Default settings restored") + action("Insert+ctrl+s", "Settings saved") + saved = tomllib.loads((root / "settings.toml").read_text()) + assert saved["presentation"]["speech"]["rate"] == 10, saved + assert (root / "input").read_bytes() in (b"\x1b[Cq", b"\x1bOCq"), (root / "input").read_bytes() + keyboard.key("Insert+q") + wait_for(lambda: (root / "finished").exists(), "reader exit") + 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") + except BaseException: + print("SSIP events:", server.events[-25:], file=sys.stderr) + for name in ("reader-errors", "timing.log"): + if (root / name).exists(): + print(name + ":", (root / name).read_text()[-4000:], file=sys.stderr) + raise + finally: + if state: + try: + os.kill(state["pid"], signal.SIGTERM) + except ProcessLookupError: + pass + process.terminate() + try: + process.wait(timeout=3) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + server.close() + keyboard.close() + + +if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == "--shell": + fixture_shell() + else: + main() diff --git a/tests/native_terminal.py b/tests/native_terminal.py index 1b25a719..edb94317 100644 --- a/tests/native_terminal.py +++ b/tests/native_terminal.py @@ -326,7 +326,7 @@ def main(): # This fixture intentionally remaps F1 for the multi-press trial. tutorialAlias = '\n[[bindings]]\nname = "Tutorial"\nkeys = ["FENRIR", "F1"]\ncommand = "help"\n' layoutPath.write_text(layoutPath.read_text().replace(tutorialAlias, "").replace( - '\n[[bindings]]\nname = "Attributes"\nkeys = ["KPMINUS"]\ncommand = "attribute_cursor"\n', '\n[[bindings]]\nname = "Attributes"\nkeys = ["FENRIR", "A"]\ncommand = "attribute_cursor"\n') + multiPressBindings) + '\n[[bindings]]\nname = "Attributes"\nkeys = ["KPMINUS"]\ncommand = "attribute_cursor"\n', '\n[[bindings]]\nname = "Attributes"\nkeys = ["FENRIR", "SHIFT", "A"]\ncommand = "attribute_cursor"\n') + multiPressBindings) server = SpeechServer(root / "speech.sock") (root / "settings.toml").write_text( 'version=3\n[presentation.sound]\nenabled=false\n' @@ -1856,7 +1856,7 @@ def main(): spoken("root", start) toggleKey = "Insert+KP_Subtract" nextLine = "KP_Prior" if layout == "custom" else "Insert+o" - attributeKey = "KP_Subtract" if layout == "custom" else "Insert+a" + attributeKey = "KP_Subtract" if layout == "custom" else "Insert+Shift_L+a" followKey = "Insert+KP_Delete" if layout == "custom" else "Insert+slash" start = len(server.events) key(toggleKey) @@ -1894,7 +1894,7 @@ def main(): spoken("speech", start) nextLine = "KP_Prior" if layout == "custom" else "Insert+o" currentLine = "KP_Up" if layout == "custom" else "Insert+i" - attributeKey = "KP_Subtract" if layout == "custom" else "Insert+a" + attributeKey = "KP_Subtract" if layout == "custom" else "Insert+Shift_L+a" followKey = "Insert+KP_Delete" if layout == "custom" else "Insert+slash" start = len(server.events) key(nextLine) diff --git a/tests/ninjam_null_audio.c b/tests/ninjam_null_audio.c new file mode 100644 index 00000000..2d5c2155 --- /dev/null +++ b/tests/ninjam_null_audio.c @@ -0,0 +1,35 @@ +/* Copyright (c) 2026 Storm Dragon + * SPDX-License-Identifier: GPL-3.0-or-later + * Test-only preload for cninjam configured with ALSA null input/output at 48kHz. + * Null capture is not clocked and need not initialize its buffer. Supply silence + * and real-time pacing without accessing a microphone or playback device. + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +static pthread_once_t readOnce = PTHREAD_ONCE_INIT; +static snd_pcm_sframes_t (*realRead)(snd_pcm_t *, void *, snd_pcm_uframes_t); + +static void resolve_read(void) { + *(void **)(&realRead) = dlsym(RTLD_NEXT, "snd_pcm_readi"); +} + +snd_pcm_sframes_t snd_pcm_readi(snd_pcm_t *pcm, void *buffer, snd_pcm_uframes_t frames) { + pthread_once(&readOnce, resolve_read); + if (!realRead) return -ENOSYS; + snd_pcm_sframes_t count = realRead(pcm, buffer, frames); + if (count > 0) { + memset(buffer, 0, snd_pcm_frames_to_bytes(pcm, count)); + struct timespec delay = { + .tv_sec = count / 48000, + .tv_nsec = (count % 48000) * 1000000000LL / 48000, + }; + while (nanosleep(&delay, &delay) < 0 && errno == EINTR) {} + } + return count; +}