From 15e04aa8f8587a433f3f38f1e2067853dc7793d3 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Thu, 29 Oct 2026 18:01:42 -0400 Subject: [PATCH] Default laptop key set to capslock. Keys that can be set to fenrir key or script key selectable via checkbox. --- README.md | 17 +- cmd/fenrir/main.go | 16 +- cmd/fenrir/main_test.go | 20 +++ config/keyboard/laptop.toml | 4 +- config/settings/settings.toml | 5 + docs/user.md | 23 ++- internal/app/deletion_test.go | 2 +- internal/app/fenrir_menu.go | 3 + internal/app/progress_test.go | 2 +- internal/app/startup.go | 17 +- internal/app/tutorial_test.go | 2 +- internal/config/config.go | 8 +- internal/config/definitions.go | 6 + internal/config/document.go | 7 +- internal/config/modifiers_test.go | 22 +++ internal/console/native_linux.go | 17 ++ internal/i18n/locale/de/LC_MESSAGES/fenrir.po | 108 +++++++----- internal/i18n/locale/es/LC_MESSAGES/fenrir.po | 108 +++++++----- internal/i18n/locale/fenrir.pot | 108 +++++++----- internal/i18n/locale/pl/LC_MESSAGES/fenrir.po | 108 +++++++----- internal/i18n/locale/pt/LC_MESSAGES/fenrir.po | 108 +++++++----- internal/i18n/locale/ru/LC_MESSAGES/fenrir.po | 108 +++++++----- internal/input/bindings.go | 50 ++++-- internal/input/bindings_test.go | 4 +- internal/input/capslock_test.go | 146 +++++++++++++++++ internal/input/double_press_test.go | 2 +- internal/input/layout.go | 10 +- internal/input/learn.go | 2 +- internal/input/learn_test.go | 2 +- internal/input/modifiers_linux.go | 154 ++++++++++++++++++ internal/input/modifiers_test.go | 96 +++++++++++ internal/input/multi_press_test.go | 14 +- internal/input/regions_test.go | 2 +- internal/input/router.go | 128 +++++++++++---- internal/input/script_keys_linux.go | 38 ++--- internal/input/script_keys_test.go | 2 +- internal/input/ui.go | 1 + internal/input/ui_test.go | 2 +- internal/preferences/modifiers.go | 114 +++++++++++++ internal/preferences/modifiers_test.go | 110 +++++++++++++ internal/preferences/settings.go | 8 + internal/preferences/settings_test.go | 50 ++++-- internal/terminal/keyboard_linux.go | 24 +++ internal/terminal/native.c | 13 +- internal/terminal/native.h | 2 + internal/terminal/session_linux.go | 7 +- tests/native_terminal.py | 111 +++++++++++++ 47 files changed, 1560 insertions(+), 351 deletions(-) create mode 100644 internal/config/modifiers_test.go create mode 100644 internal/input/capslock_test.go create mode 100644 internal/input/modifiers_linux.go create mode 100644 internal/input/modifiers_test.go create mode 100644 internal/preferences/modifiers.go create mode 100644 internal/preferences/modifiers_test.go diff --git a/README.md b/README.md index fff33abe..a1cb2932 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,10 @@ Use `--keyboard-layout laptop` with either mode to select the laptop layout. ## Basic controls -The Fenrir key is Insert, Super/Windows, or keypad 0 with NumLock off. +The Fenrir key is Insert, Super/Windows, or keypad 0 with NumLock off in the +desktop layout. The laptop layout uses Caps Lock instead of Super/Windows; +Insert and NumLock-off keypad 0 also work. Press Shift+Caps Lock on its own +to toggle capitalization in the laptop layout. Hold it while pressing the other keys in a shortcut. Bare keypad review keys also require NumLock off. @@ -213,6 +216,11 @@ Space toggles checkboxes; arrows change selections and numeric values. Activate The menu includes speech, sound, reading, keyboard, clipboard, and other settings. Speech rate, pitch, and volume use a scale from -100 to 100. For speech rate, Up and Down change the value by 10; Left and Right change it by 1. +The keyboard layout combo box offers Desktop and Laptop. The Keyboard tab also +has separate Fenrir-key and script-key checkboxes for keypad Insert, Insert, +Caps Lock, and Super, plus Compose for scripts. Uncheck a key in one group +before selecting it in the other. At least one Fenrir key must remain selected. +Key assignments take effect on the next launch. Backend, device, keyboard layout, sound theme, and other launch-only changes take effect the next time Fenrir starts. @@ -269,7 +277,12 @@ Keyboard layouts are separate TOML files. Named layouts are searched in the user's `keyboard/` directory beside `settings/`, then `/etc/fenrirscreenreader/keyboard/`, then the checkout's bundled layouts. Root uses the system and bundled locations. `--keyboard-layout PATH.toml` -selects a file directly. +selects a file directly. Layout files can set `fenrir_keys` before their bindings; +the available names are `INSERT`, `SUPER`, `CAPSLOCK`, and `KP0`. The bundled +laptop layout uses `["CAPSLOCK", "INSERT", "KP0"]`. An explicit +`keyboard.fenrir_keys` setting overrides those defaults; its names use the +`KEY_` form shown in [settings.toml](config/settings/settings.toml). Existing +script-key assignments are excluded from implicit Fenrir-key defaults. Speech drivers are `speech-dispatcher`, `dectalk`, `dectalk-usb`, `litetalk`, `doubletalk`, and `tripletalk`. Serial drivers need a device path. The Speech diff --git a/cmd/fenrir/main.go b/cmd/fenrir/main.go index b758a8f2..b2230816 100644 --- a/cmd/fenrir/main.go +++ b/cmd/fenrir/main.go @@ -127,6 +127,14 @@ func run_with_ready(args []string, stdout, stderr io.Writer, ready func() error) fmt.Fprintln(stderr, "fenrir: keyboard layout:", bindingPath, err) return 2 } + modifierRouter, modifierErr := input.New_router(bindings) + if modifierErr == nil { + modifierErr = modifierRouter.Configure_modifiers(settings.Keyboard.FenrirKeys, settings.General.ScriptKeys) + } + if modifierErr != nil { + fmt.Fprintln(stderr, "fenrir: keyboard modifiers:", modifierErr) + return 2 + } if *checkConfig { if _, profilePath, err := punctuation.Open(settings.Reading.PunctuationProfile); err != nil { fmt.Fprintln(stderr, "fenrir: punctuation profile:", profilePath, err) @@ -203,19 +211,19 @@ func termination_context() (context.Context, func()) { } } -func load_keyboard(layout string) ([]input.Binding, string, error) { +func load_keyboard(layout string) (input.Layout, string, error) { home, err := os.UserHomeDir() if err != nil { - return nil, "", err + return input.Layout{}, "", err } executable, err := os.Executable() if err != nil { - return nil, "", err + return input.Layout{}, "", err } bundledDirectory := filepath.Join(filepath.Dir(executable), "config", "keyboard") paths, err := input.Layout_paths(layout, home, os.Geteuid(), bundledDirectory) if err != nil { - return nil, "", err + return input.Layout{}, "", err } return input.Load_bindings(paths) } diff --git a/cmd/fenrir/main_test.go b/cmd/fenrir/main_test.go index 962ba8f0..e2d3c532 100644 --- a/cmd/fenrir/main_test.go +++ b/cmd/fenrir/main_test.go @@ -213,3 +213,23 @@ func TestStartupPreservesExplicitHome(t *testing.T) { t.Fatalf("HOME=%q, want explicit home %q", got, home) } } + +func TestCheckConfigValidatesEffectiveModifierRoles(t *testing.T) { + path := filepath.Join(t.TempDir(), "settings.toml") + for _, trial := range []struct { + reader string + want int + }{ + {"", 0}, {"fenrir_keys=['KEY_INSERT']", 0}, {"fenrir_keys=['KEY_META']", 2}, + } { + data := "version=3\n[keyboard]\n" + trial.reader + "\n[general]\nscript_keys=['KEY_RIGHTMETA']\n" + if err := os.WriteFile(path, []byte(data), 0600); err != nil { + t.Fatal(err) + } + var stdout, stderr bytes.Buffer + status := run([]string{"--config", path, "--keyboard-layout", "../../config/keyboard/desktop.toml", "--punctuation-profile", "../../config/punctuation/default.toml", "--check-config"}, &stdout, &stderr) + if status != trial.want { + t.Fatal(trial, status, stdout.String(), stderr.String()) + } + } +} diff --git a/config/keyboard/laptop.toml b/config/keyboard/laptop.toml index 6fb17a30..5ed112de 100644 --- a/config/keyboard/laptop.toml +++ b/config/keyboard/laptop.toml @@ -1,6 +1,8 @@ # Laptop layout for currently implemented reader commands. -# FENRIR is Insert, Super, or NumLock-off keypad 0. +# FENRIR is Caps Lock, Insert, or NumLock-off keypad 0. +# Shift+Caps Lock toggles capitalization. version = 1 +fenrir_keys = ["CAPSLOCK", "INSERT", "KP0"] [[bindings]] name = "Tracking mode" diff --git a/config/settings/settings.toml b/config/settings/settings.toml index 0e23f3cb..107d7c7c 100644 --- a/config/settings/settings.toml +++ b/config/settings/settings.toml @@ -47,6 +47,11 @@ date_format = "locale" multi_press_timeout_ms = 200 # Named layouts live in keyboard/ alongside settings/. Explicit TOML paths work too. layout = "desktop" +# Unset uses the layout's Fenrir keys, excluding configured script keys. +# The Keyboard tab checkboxes save an explicit selection that overrides the layout. +# Names: KEY_KP0 (NumLock off), KEY_INSERT, KEY_CAPSLOCK, KEY_META (both Super keys). +# A physical key cannot serve as both a Fenrir key and a script key. +# fenrir_keys = ["KEY_KP0", "KEY_INSERT", "KEY_META"] # Empty discovers all supported keyboards and numeric keypads. devices = [] character_echo = true diff --git a/docs/user.md b/docs/user.md index dbab6820..ab0fb3f6 100644 --- a/docs/user.md +++ b/docs/user.md @@ -47,13 +47,19 @@ on text consoles selected in `screen.ignored_vts`. ## Keyboard layouts and help -The **Fenrir key** is Insert, Super/Windows, or keypad 0 with NumLock off. +The **Fenrir key** is Insert, Super/Windows, or keypad 0 with NumLock off in +the desktop layout. The laptop layout uses Caps Lock instead of Super/Windows; +Insert and NumLock-off keypad 0 also work. Press Shift+Caps Lock on its own +to toggle capitalization in the laptop layout. Shifted Fenrir shortcuts do +not toggle capitalization. Hold it while pressing the other keys in a shortcut. Bare keypad review keys also require NumLock off. The desktop layout uses the numeric keypad. The laptop layout uses Fenrir chords on the main keyboard. Select a layout with `--keyboard-layout desktop` -or `--keyboard-layout laptop`, or set `keyboard.layout` in the settings file. +or `--keyboard-layout laptop`, choose Desktop or Laptop in the Keyboard tab +of the Fenrir Menu, or set `keyboard.layout` in the settings file. Menu layout +changes take effect on the next launch. Press **Fenrir+H** or **Fenrir+F1** to enter the tutorial. Command shortcuts are announced and described instead of executed. Up and Down browse commands; @@ -587,6 +593,19 @@ bundled `config/scripts/` beside the binary. Earlier directories take priority. A filename such as `example__-__key_y.sh` assigns the script key plus Y. The script key defaults to Compose and is configured by `general.script_keys`. +The Keyboard tab of the Fenrir Menu has separate **Fenrir keys** and **Script +keys** checkbox groups for keypad Insert, Insert, Caps Lock, and Super; Compose +is also available for scripts. Uncheck a key in one group before assigning it +to the other. A physical key cannot serve both roles, including through an +alias such as `KEY_META` and `KEY_RIGHTMETA`. At least one Fenrir key must remain +selected. Changes take effect on the next launch. + +The Fenrir-key checkboxes save `keyboard.fenrir_keys`, which overrides the +layout's defaults. When that setting is omitted, the layout supplies defaults +and explicitly configured script keys are excluded from them. Existing custom +script keys remain unchanged when editing the checkboxes. Keypad Insert acts +as a modifier only with NumLock off. Shift+Caps Lock toggles capitalization +when Caps Lock serves either role. Scripts must be executable to run. Fenrir invokes them directly and passes the current account name as the first argument. Commands appear in the tutorial's script list. diff --git a/internal/app/deletion_test.go b/internal/app/deletion_test.go index 713544e2..2e7590c2 100644 --- a/internal/app/deletion_test.go +++ b/internal/app/deletion_test.go @@ -19,7 +19,7 @@ func TestDeletionThroughSharedRouterWithAutoreadOff(t *testing.T) { for _, key := range []uint16{input.KeyBackspace, input.KeyDelete} { for _, mode := range []string{"enabled", "muted", "temporary silence", "speech disabled"} { t.Run(mode+"/"+map[uint16]string{input.KeyBackspace: "backspace", input.KeyDelete: "delete"}[key], func(t *testing.T) { - router, err := input.New_router([]input.Binding{{Name: "Quit", Keys: []string{"FENRIR", "Q"}, Command: command.Quit}}) + router, err := input.New_router(input.Layout{Bindings: []input.Binding{{Name: "Quit", Keys: []string{"FENRIR", "Q"}, Command: command.Quit}}}) if err != nil { t.Fatal(err) } diff --git a/internal/app/fenrir_menu.go b/internal/app/fenrir_menu.go index b8673f35..6f8300d1 100644 --- a/internal/app/fenrir_menu.go +++ b/internal/app/fenrir_menu.go @@ -69,6 +69,9 @@ func (ui *internalUI) open_settings_context(settings config.Settings, path strin ui.post(uiUpdate{generation: generation, close: true, speech: i18n.Format("Could not open Fenrir Menu: {0}", err.Error())}) return } + if doc.Settings.Keyboard.FenrirKeys == nil && settings.Keyboard.FenrirKeys != nil { + doc.Settings.Keyboard.FenrirKeys = append([]string(nil), settings.Keyboard.FenrirKeys...) + } if len(liveQuick) != 0 { copy_quick_settings(&doc.Settings, liveQuick[0]) } diff --git a/internal/app/progress_test.go b/internal/app/progress_test.go index 12163fcb..b8488019 100644 --- a/internal/app/progress_test.go +++ b/internal/app/progress_test.go @@ -212,7 +212,7 @@ func TestProgressLoopFlushesFinalValueWithoutAnotherScreenEvent(t *testing.T) { } func TestBackspaceThroughSharedRouterDoesNotBeep(t *testing.T) { - router, err := input.New_router([]input.Binding{{Name: "Quit", Keys: []string{"FENRIR", "Q"}, Command: command.Quit}}) + router, err := input.New_router(input.Layout{Bindings: []input.Binding{{Name: "Quit", Keys: []string{"FENRIR", "Q"}, Command: command.Quit}}}) if err != nil { t.Fatal(err) } diff --git a/internal/app/startup.go b/internal/app/startup.go index 8ace6c5b..7a42bf4f 100644 --- a/internal/app/startup.go +++ b/internal/app/startup.go @@ -52,22 +52,22 @@ func (disabledVoice) Toggle_mute() bool { return true } func (disabledVoice) Errors() <-chan error { return nil } func (disabledVoice) Close() {} -func Start(ctx context.Context, settings config.Settings, configPath string, bindings []input.Binding) error { +func Start(ctx context.Context, settings config.Settings, configPath string, bindings input.Layout) error { return Start_with_ready(ctx, settings, configPath, bindings, nil) } // Start_with_ready reports console startup after the first screen and input poll. // A failed notification tears down the reader and releases its keyboards. -func Start_with_ready(ctx context.Context, settings config.Settings, configPath string, bindings []input.Binding, ready func() error) error { +func Start_with_ready(ctx context.Context, settings config.Settings, configPath string, bindings input.Layout, ready func() error) error { return start(ctx, settings, false, configPath, bindings, ready) } // Start_terminal runs the same reader and command dispatcher in an X11 PTY. -func Start_terminal(ctx context.Context, settings config.Settings, configPath string, bindings []input.Binding) error { +func Start_terminal(ctx context.Context, settings config.Settings, configPath string, bindings input.Layout) error { return start(ctx, settings, true, configPath, bindings, nil) } -func start(ctx context.Context, settings config.Settings, terminalMode bool, configPath string, bindings []input.Binding, ready func() error) (result error) { +func start(ctx context.Context, settings config.Settings, terminalMode bool, configPath string, bindings input.Layout, ready func() error) (result error) { if err := settings.Validate(); err != nil { return err } @@ -80,6 +80,13 @@ func start(ctx context.Context, settings config.Settings, terminalMode bool, con if err != nil { return err } + if err := router.Configure_modifiers(settings.Keyboard.FenrirKeys, settings.General.ScriptKeys); err != nil { + return err + } + settings.Keyboard.FenrirKeys = router.Fenrir_keys() + if err := settings.Validate(); err != nil { + return err + } if err := router.Set_multi_press_timeout(time.Duration(settings.Keyboard.MultiPressTimeoutMS) * time.Millisecond); err != nil { return err } @@ -174,7 +181,7 @@ func start(ctx context.Context, settings config.Settings, terminalMode bool, con } transfers := clipboard.New_worker(ctx, paste) defer transfers.Close() - result = Run(ctx, source, keyboards, voices, RunOptions{Ready: ready, Spellcheck: spellChecker, Bindings: bindings, Settings: settings, EffectiveSpeech: effectiveSpeech, Punctuation: punctuationProcessor, Sounds: cues, ConfigPath: configPath, Clipboard: transfers, ClipboardHistory: historyStore, SettingsChoices: preferences.Discover_choices, Scripts: scriptCommands, ScriptRunner: scriptRunner, Remote: remote, TerminalMode: terminalMode}) + result = Run(ctx, source, keyboards, voices, RunOptions{Ready: ready, Spellcheck: spellChecker, Bindings: bindings.Bindings, Settings: settings, EffectiveSpeech: effectiveSpeech, Punctuation: punctuationProcessor, Sounds: cues, ConfigPath: configPath, Clipboard: transfers, ClipboardHistory: historyStore, SettingsChoices: preferences.Discover_choices, Scripts: scriptCommands, ScriptRunner: scriptRunner, Remote: remote, TerminalMode: terminalMode}) if result == nil && player != nil { if err := player.Finish(context.Background()); err != nil { slog.Warn("shutdown sound incomplete", "error", err, "at", time.Now()) diff --git a/internal/app/tutorial_test.go b/internal/app/tutorial_test.go index b3fd2b90..d0b1b6b7 100644 --- a/internal/app/tutorial_test.go +++ b/internal/app/tutorial_test.go @@ -125,7 +125,7 @@ func TestTutorialDispatcherBlocksActionsAndRestoresTyping(t *testing.T) { return nil } source := &fakeSource{snapshot: screen.Snapshot{Terminal: 1, Active: true}} - if err := run(context.Background(), source, keys, voice, nil, RunOptions{Bindings: bindings, Settings: config.Defaults()}); err != nil { + if err := run(context.Background(), source, keys, voice, nil, RunOptions{Bindings: bindings.Bindings, Settings: config.Defaults()}); err != nil { t.Fatal(err) } if voice.muted || keys.passed || !keys.closed || !reflect.DeepEqual(keys.captures, []bool{true, false}) || !reflect.DeepEqual(forwarded, []uint16{30}) { diff --git a/internal/config/config.go b/internal/config/config.go index 20e5f531..16a49153 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -142,7 +142,8 @@ type Speech struct { type Keyboard struct { MultiPressTimeoutMS int `toml:"multi_press_timeout_ms"` Devices []string `toml:"devices" label:"Devices"` - Layout string `toml:"layout" label:"Layout"` + FenrirKeys []string `toml:"fenrir_keys,omitempty"` + Layout string `toml:"layout" label:"Layout" choices:"keyboard-layout"` CharacterEcho bool `toml:"character_echo" label:"Character echo" quick:"Character echo"` WordEcho bool `toml:"word_echo" label:"Word echo" quick:"Word echo"` EchoOnCapsLock bool `toml:"echo_on_capslock" label:"Echo on CapsLock" quick:"Echo on CapsLock"` @@ -198,6 +199,11 @@ func (s Settings) Validate() error { if s.Scripts.Workers < 1 || s.Scripts.Workers > 32 || s.Scripts.QueueLimit < 1 || s.Scripts.QueueLimit > 1024 || s.Scripts.OutputBytes < 1 || s.Scripts.OutputBytes > 4<<20 || s.Scripts.TimeoutSeconds < 0 || s.Scripts.TimeoutSeconds > 86400 { return fmt.Errorf("invalid script execution limits") } + if s.Keyboard.FenrirKeys != nil { + if err := input.Validate_modifier_keys(s.Keyboard.FenrirKeys, s.General.ScriptKeys); err != nil { + return err + } + } for _, key := range s.General.ScriptKeys { if _, ok := input.Key_code(key); !ok { return fmt.Errorf("invalid script key %q", key) diff --git a/internal/config/definitions.go b/internal/config/definitions.go index 10c0740d..2ccc36b9 100644 --- a/internal/config/definitions.go +++ b/internal/config/definitions.go @@ -164,6 +164,12 @@ func (d Definition) Available(settings Settings) bool { func (d Definition) Choices(settings Settings) []Choice { switch d.ChoiceSource { + case "keyboard-layout": + choices := []Choice{{ID: "desktop", Label: "Desktop"}, {ID: "laptop", Label: "Laptop"}} + if settings.Keyboard.Layout != "desktop" && settings.Keyboard.Layout != "laptop" { + choices = append(choices, Choice{ID: settings.Keyboard.Layout, Label: settings.Keyboard.Layout}) + } + return choices case "sound-driver": var result []Choice for _, driver := range sound.Drivers() { diff --git a/internal/config/document.go b/internal/config/document.go index 537ab70e..dd1a736a 100644 --- a/internal/config/document.go +++ b/internal/config/document.go @@ -24,6 +24,7 @@ import ( "io" "os" "reflect" + "slices" "strings" "syscall" @@ -171,7 +172,11 @@ func (d *Document) Save(settings Settings) error { fields := make(map[string]any) for index := range value.NumField() { if value.Field(index).Kind() != reflect.Struct { - fields[value.Type().Field(index).Tag.Get("toml")] = value.Field(index).Interface() + tag := strings.Split(value.Type().Field(index).Tag.Get("toml"), ",") + if slices.Contains(tag[1:], "omitempty") && value.Field(index).IsZero() { + continue + } + fields[tag[0]] = value.Field(index).Interface() } } if len(fields) > 0 { diff --git a/internal/config/modifiers_test.go b/internal/config/modifiers_test.go new file mode 100644 index 00000000..c488f7c9 --- /dev/null +++ b/internal/config/modifiers_test.go @@ -0,0 +1,22 @@ +// Copyright (c) 2026 Storm Dragon +// SPDX-License-Identifier: GPL-3.0-or-later + +package config + +import "testing" + +func TestModifierFileValidation(t *testing.T) { + for _, data := range []string{ + "[keyboard]\nfenrir_keys=[]", + "[keyboard]\nfenrir_keys=['KEY_CTRL']", + "[keyboard]\nfenrir_keys=['KEY_META']\n[general]\nscript_keys=['KEY_RIGHTMETA']", + "[keyboard]\nfenrir_keys=['KEY_CAPSLOCK']\n[general]\nscript_keys=['KEY_CAPSLOCK']", + } { + if _, err := Decode([]byte("version=3\n" + data)); err == nil { + t.Fatal("invalid modifiers accepted", data) + } + } + if _, err := Decode([]byte("version=3\n[keyboard]\nfenrir_keys=['KEY_RIGHTMETA']\n[general]\nscript_keys=['KEY_LEFTMETA']")); err != nil { + t.Fatal(err) + } +} diff --git a/internal/console/native_linux.go b/internal/console/native_linux.go index 8f2e8e7d..a62e989b 100644 --- a/internal/console/native_linux.go +++ b/internal/console/native_linux.go @@ -37,6 +37,13 @@ static int console_state(int fd, int *mode, unsigned char *leds) { return 0; } +static int toggle_caps_lock(int fd) { + unsigned char leds; + if (ioctl(fd, KDGKBLED, &leds) < 0) return -errno; + if (ioctl(fd, KDSKBLED, (unsigned long)(leds ^ LED_CAP)) < 0) return -errno; + return 0; +} + static int console_usage(int fd, unsigned short *mask) { struct vt_stat state; if (ioctl(fd, VT_GETSTATE, &state) < 0) return -errno; @@ -398,6 +405,16 @@ func (k *Keyboards) drain(device *Device, numLock bool, handle func(input.Decisi return false, err } } + if decision.ToggleCapsLock { + file, err := os.OpenFile("/dev/tty0", os.O_RDONLY|syscall.O_NOCTTY, 0) + if err != nil { + return false, err + } + toggleErr := kernel_error(C.toggle_caps_lock(C.int(file.Fd()))) + if err := errors.Join(toggleErr, file.Close()); err != nil { + return false, err + } + } if decision.Forward { if err := device.forward(uint16(event._type), uint16(event.code), int32(event.value)); err != nil { return false, err diff --git a/internal/i18n/locale/de/LC_MESSAGES/fenrir.po b/internal/i18n/locale/de/LC_MESSAGES/fenrir.po index 8bd8e1fc..e96d492c 100644 --- a/internal/i18n/locale/de/LC_MESSAGES/fenrir.po +++ b/internal/i18n/locale/de/LC_MESSAGES/fenrir.po @@ -156,11 +156,11 @@ msgstr "Rüstung" msgid "Astronaut" msgstr "Astronaut" -#: internal/config/config.go:161 +#: internal/config/config.go:162 msgid "Attribute format" msgstr "Attributformat" -#: internal/config/config.go:160 +#: internal/config/config.go:161 msgid "Attributes" msgstr "Attribute" @@ -172,11 +172,11 @@ msgstr "Australien" msgid "Auto Rickshaw" msgstr "Autorikscha" -#: internal/config/config.go:151 +#: internal/config/config.go:152 msgid "Automatic reading" msgstr "Automatisches Lesen" -#: internal/config/config.go:162 +#: internal/config/config.go:163 msgid "Automatic spell checking" msgstr "Automatische Rechtschreibprüfung" @@ -272,7 +272,7 @@ msgstr "Biber" msgid "Bee" msgstr "Biene" -#: internal/config/config.go:157 +#: internal/config/config.go:158 msgid "Beep for capital letters" msgstr "Signalton bei Großbuchstaben" @@ -413,7 +413,7 @@ msgid "Canada" msgstr "Kanada" #: internal/app/application_overrides.go:360 -#: internal/preferences/settings.go:93 +#: internal/preferences/settings.go:96 msgid "Cancel" msgstr "Abbrechen" @@ -433,6 +433,10 @@ msgstr "Konserve" msgid "Canoe" msgstr "Kanu" +#: internal/preferences/modifiers.go:21 +msgid "Caps Lock" +msgstr "Feststelltaste" + #: internal/emoji/catalog.go:263 msgid "Carrot" msgstr "Karotte" @@ -449,7 +453,7 @@ msgstr "Ketten" msgid "Channel name {0}" msgstr "Kanalname {0}" -#: internal/config/config.go:146 +#: internal/config/config.go:147 msgid "Character echo" msgstr "Zeichenansage" @@ -638,6 +642,10 @@ msgstr "Spaltennummer des Cursors" msgid "Comet" msgstr "Komet" +#: internal/preferences/modifiers.go:102 internal/preferences/modifiers.go:67 +msgid "Compose" +msgstr "Compose" + #: internal/emoji/catalog.go:402 msgid "Computer mouse" msgstr "Computermaus" @@ -689,7 +697,7 @@ msgid "Could not open Emoji Menu: {0}" msgstr "Emoji-Menü konnte nicht geöffnet werden: {0}" #: internal/app/fenrir_menu.go:40 internal/app/fenrir_menu.go:69 -#: internal/app/fenrir_menu.go:89 internal/app/runtime_settings.go:134 +#: internal/app/fenrir_menu.go:92 internal/app/runtime_settings.go:134 msgid "Could not open Fenrir Menu: {0}" msgstr "Fenrir-Menü konnte nicht geöffnet werden: {0}" @@ -706,7 +714,7 @@ msgstr "Anwendungseinstellungen konnten nicht wiederhergestellt werden: {0}" msgid "Could not save exceptions: {0}" msgstr "Ausnahmen konnten nicht gespeichert werden: {0}" -#: internal/app/fenrir_menu.go:200 internal/app/fenrir_menu.go:206 +#: internal/app/fenrir_menu.go:203 internal/app/fenrir_menu.go:209 #: internal/app/runtime_settings.go:177 internal/app/runtime_settings.go:205 msgid "Could not save settings: {0}" msgstr "Einstellungen konnten nicht gespeichert werden: {0}" @@ -767,7 +775,7 @@ msgstr "Curlingstein" msgid "Curry Rice" msgstr "Reis mit curry" -#: internal/config/config.go:152 +#: internal/config/config.go:153 msgid "Cursor echo" msgstr "Cursoransage" @@ -812,6 +820,10 @@ msgstr "Kanal löschen" msgid "Denmark" msgstr "Dänemark" +#: internal/config/definitions.go:168 +msgid "Desktop" +msgstr "Desktop" + #: internal/emoji/catalog.go:399 msgid "Desktop computer" msgstr "Desktop-Computer" @@ -908,7 +920,7 @@ msgstr "Adler" msgid "Ear" msgstr "Ohr" -#: internal/config/config.go:148 +#: internal/config/config.go:149 msgid "Echo on CapsLock" msgstr "Zeichenansage bei Feststelltaste" @@ -940,7 +952,7 @@ msgstr "Emoji" msgid "Emoji Menu" msgstr "Emoji-Menü" -#: internal/config/config.go:166 +#: internal/config/config.go:167 msgid "Emoticons" msgstr "Emoticons" @@ -1031,6 +1043,10 @@ msgstr "Laub" msgid "Fenrir Menu" msgstr "Fenrir-Menü" +#: internal/preferences/modifiers.go:62 +msgid "Fenrir keys" +msgstr "Fenrir-Tasten" + #: internal/app/commands.go:98 msgid "Fenrir screen reader version {0}-{1}" msgstr "Fenrir-Bildschirmleser Version {0}-{1}" @@ -1367,7 +1383,7 @@ msgstr "Kobold" msgid "Import to Clipboard" msgstr "In Zwischenablage importieren" -#: internal/config/config.go:159 +#: internal/config/config.go:160 msgid "Indentation" msgstr "Einrückung" @@ -1383,6 +1399,10 @@ msgstr "Information nicht verfügbar" msgid "Input {0}" msgstr "Eingang {0}" +#: internal/preferences/modifiers.go:20 +msgid "Insert" +msgstr "Einfügen" + #: internal/config/config.go:60 msgid "Internal UI" msgstr "Interne Benutzeroberfläche" @@ -1423,6 +1443,10 @@ msgstr "Känguru" msgid "Keyboard" msgstr "Tastatur" +#: internal/preferences/modifiers.go:19 +msgid "Keypad Insert" +msgstr "Einfügen auf dem Ziffernblock" + #: internal/emoji/catalog.go:378 msgid "Kick Scooter" msgstr "Tretroller" @@ -1451,7 +1475,7 @@ msgstr "Lacrosse" msgid "Lady beetle" msgstr "Marienkäfer" -#: internal/emoji/catalog.go:398 +#: internal/config/definitions.go:168 internal/emoji/catalog.go:398 msgid "Laptop" msgstr "Laptop" @@ -1471,7 +1495,7 @@ msgstr "Letzte Nachricht" msgid "Laughing" msgstr "Lachend" -#: internal/config/config.go:145 +#: internal/config/config.go:146 msgid "Layout" msgstr "Tastaturbelegung" @@ -1628,7 +1652,7 @@ msgstr "Mikrofon" msgid "Minibus" msgstr "Kleinbus" -#: internal/config/config.go:165 +#: internal/config/config.go:166 msgid "Misspelling sound" msgstr "Signalton bei Rechtschreibfehlern" @@ -1826,7 +1850,7 @@ msgstr "Nicht im Lesemodus" msgid "Not in window Mode" msgstr "Nicht im Fenstermodus" -#: internal/preferences/settings.go:93 +#: internal/preferences/settings.go:96 msgid "OK" msgstr "OK" @@ -2073,11 +2097,11 @@ msgstr "Fortschrittsüberwachung aktiviert" msgid "PulseAudio" msgstr "PulseAudio" -#: internal/config/config.go:155 +#: internal/config/config.go:156 msgid "Punctuation level" msgstr "Satzzeichenstufe" -#: internal/config/config.go:154 +#: internal/config/config.go:155 msgid "Punctuation profile" msgstr "Satzzeichenprofil" @@ -2109,7 +2133,7 @@ msgstr "Eisenbahnwagen" msgid "Rainbow" msgstr "Regenbogen" -#: internal/config/config.go:156 +#: internal/config/config.go:157 msgid "Raise pitch for capital letters" msgstr "Tonhöhe bei Großbuchstaben anheben" @@ -2181,7 +2205,7 @@ msgstr "Live-Bereich 8 lesen" msgid "Read live region 9" msgstr "Live-Bereich 9 lesen" -#: internal/config/config.go:163 +#: internal/config/config.go:164 msgid "Read misspelled word" msgstr "Falsch geschriebenes Wort vorlesen" @@ -2221,11 +2245,11 @@ msgstr "Entfernt die Markierungen des ausgewählten Textes" msgid "Rename channel" msgstr "Kanal umbenennen" -#: internal/config/config.go:167 +#: internal/config/config.go:168 msgid "Repeated symbols" msgstr "Wiederholte Symbole" -#: internal/config/config.go:168 +#: internal/config/config.go:169 msgid "Repeated symbols minimum length" msgstr "Mindestanzahl wiederholter Symbole" @@ -2341,11 +2365,11 @@ msgstr "Essen genießend" msgid "Saxophone" msgstr "Saxofon" -#: internal/config/config.go:158 +#: internal/config/config.go:159 msgid "Say “cap” before capital letters" msgstr "„Groß“ vor Großbuchstaben ansagen" -#: internal/config/config.go:164 +#: internal/config/config.go:165 msgid "Say “misspelled”" msgstr "„Falsch geschrieben“ ansagen" @@ -2373,6 +2397,10 @@ msgstr "Skriptdatei ist nicht ausführbar" msgid "Script file not found" msgstr "Skriptdatei nicht gefunden" +#: internal/preferences/modifiers.go:63 +msgid "Script keys" +msgstr "Skript-Tasten" + #: internal/app/scripts.go msgid "Script source is not a valid file" msgstr "Skriptquelle ist keine gültige Datei" @@ -2694,6 +2722,10 @@ msgstr "Sonnenblume" msgid "Sunglasses" msgstr "Sonnenbrille" +#: internal/preferences/modifiers.go:22 +msgid "Super" +msgstr "Super" + #: internal/emoji/catalog.go:98 msgid "Superhero" msgstr "Superheld(in)" @@ -2766,11 +2798,11 @@ msgstr "Fernseher" msgid "Tennis ball" msgstr "Tennisball" -#: internal/preferences/settings.go:83 +#: internal/preferences/settings.go:86 msgid "Test synthesizer" msgstr "Sprachsynthesizer testen" -#: internal/app/fenrir_menu.go:165 +#: internal/app/fenrir_menu.go:168 msgid "Testing voice" msgstr "Stimme wird getestet" @@ -2818,7 +2850,7 @@ msgstr "Oben" msgid "Tornado" msgstr "Wirbelsturm" -#: internal/config/config.go:153 +#: internal/config/config.go:154 msgid "Tracking mode" msgstr "Verfolgungsmodus" @@ -3084,7 +3116,7 @@ msgstr "Wolf" msgid "Word" msgstr "Wort" -#: internal/config/config.go:147 +#: internal/config/config.go:148 msgid "Word echo" msgstr "Wortansage" @@ -3174,7 +3206,7 @@ msgstr "Ausrufezeichen" msgid "bar" msgstr "senkrechter Strich" -#: internal/config/definitions.go:206 +#: internal/config/definitions.go:212 msgid "beeps" msgstr "Signaltöne" @@ -3305,7 +3337,7 @@ msgstr "Aktuelle Zeile" msgid "current word." msgstr "Aktuelles Wort." -#: internal/config/definitions.go:225 internal/reader/reader.go:252 +#: internal/config/definitions.go:231 internal/reader/reader.go:252 msgid "cursor tracking" msgstr "Cursor-Verfolgung" @@ -3323,7 +3355,7 @@ msgid "dash" msgstr "Bindestrich" #: internal/app/quick_settings.go:185 internal/app/quick_settings.go:234 -#: internal/config/definitions.go:237 internal/preferences/discovery.go:45 +#: internal/config/definitions.go:243 internal/preferences/discovery.go:45 #: internal/preferences/discovery.go:55 internal/preferences/discovery.go:81 #: internal/presentation/attributes.go:18 #: internal/presentation/attributes.go:44 @@ -3506,7 +3538,7 @@ msgstr "Kopfzeile festgelegt" msgid "heart" msgstr "Herz" -#: internal/config/definitions.go:225 internal/reader/reader.go:249 +#: internal/config/definitions.go:231 internal/reader/reader.go:249 msgid "highlight tracking" msgstr "Hervorhebungen verfolgen" @@ -3694,7 +3726,7 @@ msgstr "keine Tabellendaten" msgid "no text console" msgstr "keine Textkonsole" -#: internal/config/definitions.go:206 internal/punctuation/profile.go +#: internal/config/definitions.go:212 internal/punctuation/profile.go msgid "none" msgstr "keine" @@ -3914,11 +3946,11 @@ msgstr "Leerzeichen" msgid "speaks the contents of the currently selected clipboard" msgstr "Liest den Inhalt der momentan gewählten Zwischenablage vor" -#: internal/config/definitions.go:206 +#: internal/config/definitions.go:212 msgid "speech" msgstr "Sprache" -#: internal/config/definitions.go:206 +#: internal/config/definitions.go:212 msgid "speech and beeps" msgstr "Sprache und Signaltöne" @@ -3979,7 +4011,7 @@ msgstr "überrascht" msgid "tab" msgstr "Tabulator" -#: internal/config/definitions.go:225 internal/reader/reader.go:246 +#: internal/config/definitions.go:231 internal/reader/reader.go:246 msgid "table mode" msgstr "Tabellenmodus" @@ -4093,7 +4125,7 @@ msgstr "{0} entfernt" msgid "{0} tab" msgstr "{0} Registerkarte" -#: internal/punctuation/text_processing.go:69 +#: internal/punctuation/text_processing.go:92 msgid "{0} {1}" msgstr "{0} {1}" diff --git a/internal/i18n/locale/es/LC_MESSAGES/fenrir.po b/internal/i18n/locale/es/LC_MESSAGES/fenrir.po index 6d5562f5..e399bbaf 100644 --- a/internal/i18n/locale/es/LC_MESSAGES/fenrir.po +++ b/internal/i18n/locale/es/LC_MESSAGES/fenrir.po @@ -156,11 +156,11 @@ msgstr "Armadura" msgid "Astronaut" msgstr "Astronauta" -#: internal/config/config.go:161 +#: internal/config/config.go:162 msgid "Attribute format" msgstr "Formato de atributos" -#: internal/config/config.go:160 +#: internal/config/config.go:161 msgid "Attributes" msgstr "Atributos" @@ -172,11 +172,11 @@ msgstr "Australia" msgid "Auto Rickshaw" msgstr "Mototaxi" -#: internal/config/config.go:151 +#: internal/config/config.go:152 msgid "Automatic reading" msgstr "Lectura automática" -#: internal/config/config.go:162 +#: internal/config/config.go:163 msgid "Automatic spell checking" msgstr "Revisión ortográfica automática" @@ -272,7 +272,7 @@ msgstr "Castor" msgid "Bee" msgstr "Abeja" -#: internal/config/config.go:157 +#: internal/config/config.go:158 msgid "Beep for capital letters" msgstr "Pitido para letras mayúsculas" @@ -413,7 +413,7 @@ msgid "Canada" msgstr "Canadá" #: internal/app/application_overrides.go:360 -#: internal/preferences/settings.go:93 +#: internal/preferences/settings.go:96 msgid "Cancel" msgstr "Cancelar" @@ -433,6 +433,10 @@ msgstr "Comida enlatada" msgid "Canoe" msgstr "Canoa" +#: internal/preferences/modifiers.go:21 +msgid "Caps Lock" +msgstr "Bloq Mayús" + #: internal/emoji/catalog.go:263 msgid "Carrot" msgstr "Zanahoria" @@ -449,7 +453,7 @@ msgstr "Cadenas" msgid "Channel name {0}" msgstr "Nombre del canal {0}" -#: internal/config/config.go:146 +#: internal/config/config.go:147 msgid "Character echo" msgstr "Eco de caracteres" @@ -640,6 +644,10 @@ msgstr "Número de columna del cursor" msgid "Comet" msgstr "Cometa espacial" +#: internal/preferences/modifiers.go:102 internal/preferences/modifiers.go:67 +msgid "Compose" +msgstr "Composición" + #: internal/emoji/catalog.go:402 msgid "Computer mouse" msgstr "Ratón de computadora" @@ -691,7 +699,7 @@ msgid "Could not open Emoji Menu: {0}" msgstr "No se pudo abrir el menú de emojis: {0}" #: internal/app/fenrir_menu.go:40 internal/app/fenrir_menu.go:69 -#: internal/app/fenrir_menu.go:89 internal/app/runtime_settings.go:134 +#: internal/app/fenrir_menu.go:92 internal/app/runtime_settings.go:134 msgid "Could not open Fenrir Menu: {0}" msgstr "No se pudo abrir el menú de Fenrir: {0}" @@ -708,7 +716,7 @@ msgstr "No se pudo restaurar la configuración de la aplicación: {0}" msgid "Could not save exceptions: {0}" msgstr "No se pudieron guardar las excepciones: {0}" -#: internal/app/fenrir_menu.go:200 internal/app/fenrir_menu.go:206 +#: internal/app/fenrir_menu.go:203 internal/app/fenrir_menu.go:209 #: internal/app/runtime_settings.go:177 internal/app/runtime_settings.go:205 msgid "Could not save settings: {0}" msgstr "No se pudo guardar la configuración: {0}" @@ -769,7 +777,7 @@ msgstr "Piedra de curling" msgid "Curry Rice" msgstr "Arroz con curry" -#: internal/config/config.go:152 +#: internal/config/config.go:153 msgid "Cursor echo" msgstr "Eco del cursor" @@ -814,6 +822,10 @@ msgstr "Eliminar canal" msgid "Denmark" msgstr "Dinamarca" +#: internal/config/definitions.go:168 +msgid "Desktop" +msgstr "Escritorio" + #: internal/emoji/catalog.go:399 msgid "Desktop computer" msgstr "Computadora de escritorio" @@ -910,7 +922,7 @@ msgstr "Águila" msgid "Ear" msgstr "Oreja" -#: internal/config/config.go:148 +#: internal/config/config.go:149 msgid "Echo on CapsLock" msgstr "Eco con Bloq Mayús" @@ -942,7 +954,7 @@ msgstr "Emoji" msgid "Emoji Menu" msgstr "Menú de emojis" -#: internal/config/config.go:166 +#: internal/config/config.go:167 msgid "Emoticons" msgstr "Emoticonos" @@ -1034,6 +1046,10 @@ msgstr "Hojas caídas" msgid "Fenrir Menu" msgstr "Menú de Fenrir" +#: internal/preferences/modifiers.go:62 +msgid "Fenrir keys" +msgstr "Teclas de Fenrir" + #: internal/app/commands.go:98 msgid "Fenrir screen reader version {0}-{1}" msgstr "Lector de pantalla Fenrir versión {0}-{1}" @@ -1370,7 +1386,7 @@ msgstr "Diablillo" msgid "Import to Clipboard" msgstr "Importar al portapapeles" -#: internal/config/config.go:159 +#: internal/config/config.go:160 msgid "Indentation" msgstr "Sangría" @@ -1386,6 +1402,10 @@ msgstr "Información no disponible" msgid "Input {0}" msgstr "Entrada {0}" +#: internal/preferences/modifiers.go:20 +msgid "Insert" +msgstr "Insertar" + #: internal/config/config.go:60 msgid "Internal UI" msgstr "Interfaz interna" @@ -1426,6 +1446,10 @@ msgstr "Canguro" msgid "Keyboard" msgstr "Teclado" +#: internal/preferences/modifiers.go:19 +msgid "Keypad Insert" +msgstr "Insertar del teclado numérico" + #: internal/emoji/catalog.go:378 msgid "Kick Scooter" msgstr "Patinete" @@ -1454,7 +1478,7 @@ msgstr "Lacrosse" msgid "Lady beetle" msgstr "Mariquita" -#: internal/emoji/catalog.go:398 +#: internal/config/definitions.go:168 internal/emoji/catalog.go:398 msgid "Laptop" msgstr "Computadora portátil" @@ -1474,7 +1498,7 @@ msgstr "Último mensaje" msgid "Laughing" msgstr "Riendo" -#: internal/config/config.go:145 +#: internal/config/config.go:146 msgid "Layout" msgstr "Distribución del teclado" @@ -1631,7 +1655,7 @@ msgstr "Micrófono" msgid "Minibus" msgstr "Minibús" -#: internal/config/config.go:165 +#: internal/config/config.go:166 msgid "Misspelling sound" msgstr "Sonido de error ortográfico" @@ -1837,7 +1861,7 @@ msgstr "Fuera del modo de revisión" msgid "Not in window Mode" msgstr "No está en modo ventana" -#: internal/preferences/settings.go:93 +#: internal/preferences/settings.go:96 msgid "OK" msgstr "Aceptar" @@ -2082,11 +2106,11 @@ msgstr "Seguimiento del progreso activado" msgid "PulseAudio" msgstr "PulseAudio" -#: internal/config/config.go:155 +#: internal/config/config.go:156 msgid "Punctuation level" msgstr "Nivel de puntuación" -#: internal/config/config.go:154 +#: internal/config/config.go:155 msgid "Punctuation profile" msgstr "Perfil de puntuación" @@ -2118,7 +2142,7 @@ msgstr "Vagón" msgid "Rainbow" msgstr "Arcoíris" -#: internal/config/config.go:156 +#: internal/config/config.go:157 msgid "Raise pitch for capital letters" msgstr "Elevar el tono para letras mayúsculas" @@ -2190,7 +2214,7 @@ msgstr "Leer la región dinámica 8" msgid "Read live region 9" msgstr "Leer la región dinámica 9" -#: internal/config/config.go:163 +#: internal/config/config.go:164 msgid "Read misspelled word" msgstr "Leer la palabra mal escrita" @@ -2230,11 +2254,11 @@ msgstr "Elimina las marcas del texto seleccionado" msgid "Rename channel" msgstr "Cambiar nombre del canal" -#: internal/config/config.go:167 +#: internal/config/config.go:168 msgid "Repeated symbols" msgstr "Símbolos repetidos" -#: internal/config/config.go:168 +#: internal/config/config.go:169 msgid "Repeated symbols minimum length" msgstr "Cantidad mínima de símbolos repetidos" @@ -2350,11 +2374,11 @@ msgstr "Saboreando comida" msgid "Saxophone" msgstr "Saxofón" -#: internal/config/config.go:158 +#: internal/config/config.go:159 msgid "Say “cap” before capital letters" msgstr "Decir «mayúscula» antes de las letras mayúsculas" -#: internal/config/config.go:164 +#: internal/config/config.go:165 msgid "Say “misspelled”" msgstr "Decir «mal escrita»" @@ -2382,6 +2406,10 @@ msgstr "El archivo de script no es ejecutable" msgid "Script file not found" msgstr "Archivo de script no encontrado" +#: internal/preferences/modifiers.go:63 +msgid "Script keys" +msgstr "Teclas de scripts" + #: internal/app/scripts.go msgid "Script source is not a valid file" msgstr "El origen del script no es un archivo válido" @@ -2703,6 +2731,10 @@ msgstr "Girasol" msgid "Sunglasses" msgstr "Gafas de sol" +#: internal/preferences/modifiers.go:22 +msgid "Super" +msgstr "Super" + #: internal/emoji/catalog.go:98 msgid "Superhero" msgstr "Personaje de superhéroe" @@ -2775,11 +2807,11 @@ msgstr "Televisión" msgid "Tennis ball" msgstr "Pelota de tenis" -#: internal/preferences/settings.go:83 +#: internal/preferences/settings.go:86 msgid "Test synthesizer" msgstr "Probar sintetizador" -#: internal/app/fenrir_menu.go:165 +#: internal/app/fenrir_menu.go:168 msgid "Testing voice" msgstr "Probando la voz" @@ -2827,7 +2859,7 @@ msgstr "Fin" msgid "Tornado" msgstr "Tornado" -#: internal/config/config.go:153 +#: internal/config/config.go:154 msgid "Tracking mode" msgstr "Modo de seguimiento" @@ -3091,7 +3123,7 @@ msgstr "Lobo" msgid "Word" msgstr "Palabra" -#: internal/config/config.go:147 +#: internal/config/config.go:148 msgid "Word echo" msgstr "Eco de palabras" @@ -3181,7 +3213,7 @@ msgstr "exclamación" msgid "bar" msgstr "barra vertical" -#: internal/config/definitions.go:206 +#: internal/config/definitions.go:212 msgid "beeps" msgstr "pitidos" @@ -3312,7 +3344,7 @@ msgstr "línea actual" msgid "current word." msgstr "Palabra actual." -#: internal/config/definitions.go:225 internal/reader/reader.go:252 +#: internal/config/definitions.go:231 internal/reader/reader.go:252 msgid "cursor tracking" msgstr "Seguimiento del cursor" @@ -3330,7 +3362,7 @@ msgid "dash" msgstr "guion" #: internal/app/quick_settings.go:185 internal/app/quick_settings.go:234 -#: internal/config/definitions.go:237 internal/preferences/discovery.go:45 +#: internal/config/definitions.go:243 internal/preferences/discovery.go:45 #: internal/preferences/discovery.go:55 internal/preferences/discovery.go:81 #: internal/presentation/attributes.go:18 #: internal/presentation/attributes.go:44 @@ -3515,7 +3547,7 @@ msgstr "fila de encabezado definida" msgid "heart" msgstr "corazón" -#: internal/config/definitions.go:225 internal/reader/reader.go:249 +#: internal/config/definitions.go:231 internal/reader/reader.go:249 msgid "highlight tracking" msgstr "Seguimiento del texto resaltado" @@ -3703,7 +3735,7 @@ msgstr "sin datos de tabla" msgid "no text console" msgstr "sin consola de texto" -#: internal/config/definitions.go:206 internal/punctuation/profile.go +#: internal/config/definitions.go:212 internal/punctuation/profile.go msgid "none" msgstr "ninguno" @@ -3922,11 +3954,11 @@ msgstr "espacio" msgid "speaks the contents of the currently selected clipboard" msgstr "Habla el contenido del portapapeles seleccionado" -#: internal/config/definitions.go:206 +#: internal/config/definitions.go:212 msgid "speech" msgstr "voz" -#: internal/config/definitions.go:206 +#: internal/config/definitions.go:212 msgid "speech and beeps" msgstr "voz y pitidos" @@ -3987,7 +4019,7 @@ msgstr "sorprendido" msgid "tab" msgstr "tabulación" -#: internal/config/definitions.go:225 internal/reader/reader.go:246 +#: internal/config/definitions.go:231 internal/reader/reader.go:246 msgid "table mode" msgstr "modo de tabla" @@ -4101,7 +4133,7 @@ msgstr "se ha eliminado {0}" msgid "{0} tab" msgstr "{0} pestaña" -#: internal/punctuation/text_processing.go:69 +#: internal/punctuation/text_processing.go:92 msgid "{0} {1}" msgstr "{0} {1}" diff --git a/internal/i18n/locale/fenrir.pot b/internal/i18n/locale/fenrir.pot index e818711b..3c55bd6f 100644 --- a/internal/i18n/locale/fenrir.pot +++ b/internal/i18n/locale/fenrir.pot @@ -145,11 +145,11 @@ msgstr "" msgid "Astronaut" msgstr "" -#: internal/config/config.go:161 +#: internal/config/config.go:162 msgid "Attribute format" msgstr "" -#: internal/config/config.go:160 +#: internal/config/config.go:161 msgid "Attributes" msgstr "" @@ -161,11 +161,11 @@ msgstr "" msgid "Auto Rickshaw" msgstr "" -#: internal/config/config.go:151 +#: internal/config/config.go:152 msgid "Automatic reading" msgstr "" -#: internal/config/config.go:162 +#: internal/config/config.go:163 msgid "Automatic spell checking" msgstr "" @@ -261,7 +261,7 @@ msgstr "" msgid "Bee" msgstr "" -#: internal/config/config.go:157 +#: internal/config/config.go:158 msgid "Beep for capital letters" msgstr "" @@ -401,7 +401,7 @@ msgstr "" msgid "Canada" msgstr "" -#: internal/app/application_overrides.go:360 internal/preferences/settings.go:93 +#: internal/app/application_overrides.go:360 internal/preferences/settings.go:96 msgid "Cancel" msgstr "" @@ -421,6 +421,10 @@ msgstr "" msgid "Canoe" msgstr "" +#: internal/preferences/modifiers.go:21 +msgid "Caps Lock" +msgstr "" + #: internal/emoji/catalog.go:263 msgid "Carrot" msgstr "" @@ -437,7 +441,7 @@ msgstr "" msgid "Channel name {0}" msgstr "" -#: internal/config/config.go:146 +#: internal/config/config.go:147 msgid "Character echo" msgstr "" @@ -625,6 +629,10 @@ msgstr "" msgid "Comet" msgstr "" +#: internal/preferences/modifiers.go:102 internal/preferences/modifiers.go:67 +msgid "Compose" +msgstr "" + #: internal/emoji/catalog.go:402 msgid "Computer mouse" msgstr "" @@ -673,7 +681,7 @@ msgstr "" msgid "Could not open Emoji Menu: {0}" msgstr "" -#: internal/app/fenrir_menu.go:40 internal/app/fenrir_menu.go:69 internal/app/fenrir_menu.go:89 internal/app/runtime_settings.go:134 +#: internal/app/fenrir_menu.go:40 internal/app/fenrir_menu.go:69 internal/app/fenrir_menu.go:92 internal/app/runtime_settings.go:134 msgid "Could not open Fenrir Menu: {0}" msgstr "" @@ -689,7 +697,7 @@ msgstr "" msgid "Could not save exceptions: {0}" msgstr "" -#: internal/app/fenrir_menu.go:200 internal/app/fenrir_menu.go:206 internal/app/runtime_settings.go:177 internal/app/runtime_settings.go:205 +#: internal/app/fenrir_menu.go:203 internal/app/fenrir_menu.go:209 internal/app/runtime_settings.go:177 internal/app/runtime_settings.go:205 msgid "Could not save settings: {0}" msgstr "" @@ -749,7 +757,7 @@ msgstr "" msgid "Curry Rice" msgstr "" -#: internal/config/config.go:152 +#: internal/config/config.go:153 msgid "Cursor echo" msgstr "" @@ -793,6 +801,10 @@ msgstr "" msgid "Denmark" msgstr "" +#: internal/config/definitions.go:168 +msgid "Desktop" +msgstr "" + #: internal/emoji/catalog.go:399 msgid "Desktop computer" msgstr "" @@ -889,7 +901,7 @@ msgstr "" msgid "Ear" msgstr "" -#: internal/config/config.go:148 +#: internal/config/config.go:149 msgid "Echo on CapsLock" msgstr "" @@ -921,7 +933,7 @@ msgstr "" msgid "Emoji Menu" msgstr "" -#: internal/config/config.go:166 +#: internal/config/config.go:167 msgid "Emoticons" msgstr "" @@ -1001,6 +1013,10 @@ msgstr "" msgid "Fenrir Menu" msgstr "" +#: internal/preferences/modifiers.go:62 +msgid "Fenrir keys" +msgstr "" + #: internal/app/commands.go:98 msgid "Fenrir screen reader version {0}-{1}" msgstr "" @@ -1337,7 +1353,7 @@ msgstr "" msgid "Import to Clipboard" msgstr "" -#: internal/config/config.go:159 +#: internal/config/config.go:160 msgid "Indentation" msgstr "" @@ -1353,6 +1369,10 @@ msgstr "" msgid "Input {0}" msgstr "" +#: internal/preferences/modifiers.go:20 +msgid "Insert" +msgstr "" + #: internal/config/config.go:60 msgid "Internal UI" msgstr "" @@ -1393,6 +1413,10 @@ msgstr "" msgid "Keyboard" msgstr "" +#: internal/preferences/modifiers.go:19 +msgid "Keypad Insert" +msgstr "" + #: internal/emoji/catalog.go:378 msgid "Kick Scooter" msgstr "" @@ -1421,7 +1445,7 @@ msgstr "" msgid "Lady beetle" msgstr "" -#: internal/emoji/catalog.go:398 +#: internal/config/definitions.go:168 internal/emoji/catalog.go:398 msgid "Laptop" msgstr "" @@ -1441,7 +1465,7 @@ msgstr "" msgid "Laughing" msgstr "" -#: internal/config/config.go:145 +#: internal/config/config.go:146 msgid "Layout" msgstr "" @@ -1597,7 +1621,7 @@ msgstr "" msgid "Minibus" msgstr "" -#: internal/config/config.go:165 +#: internal/config/config.go:166 msgid "Misspelling sound" msgstr "" @@ -1789,7 +1813,7 @@ msgstr "" msgid "Not in window Mode" msgstr "" -#: internal/preferences/settings.go:93 +#: internal/preferences/settings.go:96 msgid "OK" msgstr "" @@ -2033,11 +2057,11 @@ msgstr "" msgid "PulseAudio" msgstr "" -#: internal/config/config.go:155 +#: internal/config/config.go:156 msgid "Punctuation level" msgstr "" -#: internal/config/config.go:154 +#: internal/config/config.go:155 msgid "Punctuation profile" msgstr "" @@ -2069,7 +2093,7 @@ msgstr "" msgid "Rainbow" msgstr "" -#: internal/config/config.go:156 +#: internal/config/config.go:157 msgid "Raise pitch for capital letters" msgstr "" @@ -2141,7 +2165,7 @@ msgstr "" msgid "Read live region 9" msgstr "" -#: internal/config/config.go:163 +#: internal/config/config.go:164 msgid "Read misspelled word" msgstr "" @@ -2181,11 +2205,11 @@ msgstr "" msgid "Rename channel" msgstr "" -#: internal/config/config.go:167 +#: internal/config/config.go:168 msgid "Repeated symbols" msgstr "" -#: internal/config/config.go:168 +#: internal/config/config.go:169 msgid "Repeated symbols minimum length" msgstr "" @@ -2301,11 +2325,11 @@ msgstr "" msgid "Saxophone" msgstr "" -#: internal/config/config.go:158 +#: internal/config/config.go:159 msgid "Say “cap” before capital letters" msgstr "" -#: internal/config/config.go:164 +#: internal/config/config.go:165 msgid "Say “misspelled”" msgstr "" @@ -2333,6 +2357,10 @@ msgstr "" msgid "Script file not found" msgstr "" +#: internal/preferences/modifiers.go:63 +msgid "Script keys" +msgstr "" + #: internal/app/scripts.go msgid "Script source is not a valid file" msgstr "" @@ -2653,6 +2681,10 @@ msgstr "" msgid "Sunglasses" msgstr "" +#: internal/preferences/modifiers.go:22 +msgid "Super" +msgstr "" + #: internal/emoji/catalog.go:98 msgid "Superhero" msgstr "" @@ -2725,11 +2757,11 @@ msgstr "" msgid "Tennis ball" msgstr "" -#: internal/preferences/settings.go:83 +#: internal/preferences/settings.go:86 msgid "Test synthesizer" msgstr "" -#: internal/app/fenrir_menu.go:165 +#: internal/app/fenrir_menu.go:168 msgid "Testing voice" msgstr "" @@ -2777,7 +2809,7 @@ msgstr "" msgid "Tornado" msgstr "" -#: internal/config/config.go:153 +#: internal/config/config.go:154 msgid "Tracking mode" msgstr "" @@ -3033,7 +3065,7 @@ msgstr "" msgid "Word" msgstr "" -#: internal/config/config.go:147 +#: internal/config/config.go:148 msgid "Word echo" msgstr "" @@ -3117,7 +3149,7 @@ msgstr "" msgid "bar" msgstr "" -#: internal/config/definitions.go:206 +#: internal/config/definitions.go:212 msgid "beeps" msgstr "" @@ -3241,7 +3273,7 @@ msgstr "" msgid "current word." msgstr "" -#: internal/config/definitions.go:225 internal/reader/reader.go:252 +#: internal/config/definitions.go:231 internal/reader/reader.go:252 msgid "cursor tracking" msgstr "" @@ -3257,7 +3289,7 @@ msgstr "" msgid "dash" msgstr "" -#: internal/app/quick_settings.go:185 internal/app/quick_settings.go:234 internal/config/definitions.go:237 internal/preferences/discovery.go:45 internal/preferences/discovery.go:55 internal/preferences/discovery.go:81 internal/presentation/attributes.go:18 internal/presentation/attributes.go:44 +#: internal/app/quick_settings.go:185 internal/app/quick_settings.go:234 internal/config/definitions.go:243 internal/preferences/discovery.go:45 internal/preferences/discovery.go:55 internal/preferences/discovery.go:81 internal/presentation/attributes.go:18 internal/presentation/attributes.go:44 msgid "default" msgstr "" @@ -3429,7 +3461,7 @@ msgstr "" msgid "heart" msgstr "" -#: internal/config/definitions.go:225 internal/reader/reader.go:249 +#: internal/config/definitions.go:231 internal/reader/reader.go:249 msgid "highlight tracking" msgstr "" @@ -3609,7 +3641,7 @@ msgstr "" msgid "no text console" msgstr "" -#: internal/config/definitions.go:206 internal/punctuation/profile.go +#: internal/config/definitions.go:212 internal/punctuation/profile.go msgid "none" msgstr "" @@ -3809,11 +3841,11 @@ msgstr "" msgid "speaks the contents of the currently selected clipboard" msgstr "" -#: internal/config/definitions.go:206 +#: internal/config/definitions.go:212 msgid "speech" msgstr "" -#: internal/config/definitions.go:206 +#: internal/config/definitions.go:212 msgid "speech and beeps" msgstr "" @@ -3873,7 +3905,7 @@ msgstr "" msgid "tab" msgstr "" -#: internal/config/definitions.go:225 internal/reader/reader.go:246 +#: internal/config/definitions.go:231 internal/reader/reader.go:246 msgid "table mode" msgstr "" @@ -3985,7 +4017,7 @@ msgstr "" msgid "{0} tab" msgstr "" -#: internal/punctuation/text_processing.go:69 +#: internal/punctuation/text_processing.go:92 msgid "{0} {1}" msgstr "" diff --git a/internal/i18n/locale/pl/LC_MESSAGES/fenrir.po b/internal/i18n/locale/pl/LC_MESSAGES/fenrir.po index 95c19f22..1c929ef7 100644 --- a/internal/i18n/locale/pl/LC_MESSAGES/fenrir.po +++ b/internal/i18n/locale/pl/LC_MESSAGES/fenrir.po @@ -157,11 +157,11 @@ msgstr "Pancerz" msgid "Astronaut" msgstr "Astronauta" -#: internal/config/config.go:161 +#: internal/config/config.go:162 msgid "Attribute format" msgstr "Format atrybutów" -#: internal/config/config.go:160 +#: internal/config/config.go:161 msgid "Attributes" msgstr "Atrybuty" @@ -173,11 +173,11 @@ msgstr "Australia" msgid "Auto Rickshaw" msgstr "autoriksza" -#: internal/config/config.go:151 +#: internal/config/config.go:152 msgid "Automatic reading" msgstr "Automatyczne czytanie" -#: internal/config/config.go:162 +#: internal/config/config.go:163 msgid "Automatic spell checking" msgstr "Automatyczne sprawdzanie pisowni" @@ -273,7 +273,7 @@ msgstr "bóbr" msgid "Bee" msgstr "Pszczoła" -#: internal/config/config.go:157 +#: internal/config/config.go:158 msgid "Beep for capital letters" msgstr "Sygnał dla wielkich liter" @@ -414,7 +414,7 @@ msgid "Canada" msgstr "Kanada" #: internal/app/application_overrides.go:360 -#: internal/preferences/settings.go:93 +#: internal/preferences/settings.go:96 msgid "Cancel" msgstr "Anuluj" @@ -434,6 +434,10 @@ msgstr "jedzenie w puszce" msgid "Canoe" msgstr "kajak" +#: internal/preferences/modifiers.go:21 +msgid "Caps Lock" +msgstr "Caps Lock" + #: internal/emoji/catalog.go:263 msgid "Carrot" msgstr "Marchew" @@ -450,7 +454,7 @@ msgstr "Łańcuchy" msgid "Channel name {0}" msgstr "Nazwa kanału {0}" -#: internal/config/config.go:146 +#: internal/config/config.go:147 msgid "Character echo" msgstr "Echo znaków" @@ -640,6 +644,10 @@ msgstr "Numer kolumny kursora" msgid "Comet" msgstr "kometa" +#: internal/preferences/modifiers.go:102 internal/preferences/modifiers.go:67 +msgid "Compose" +msgstr "Compose" + #: internal/emoji/catalog.go:402 msgid "Computer mouse" msgstr "Mysz komputerowa" @@ -691,7 +699,7 @@ msgid "Could not open Emoji Menu: {0}" msgstr "Nie można otworzyć menu emoji: {0}" #: internal/app/fenrir_menu.go:40 internal/app/fenrir_menu.go:69 -#: internal/app/fenrir_menu.go:89 internal/app/runtime_settings.go:134 +#: internal/app/fenrir_menu.go:92 internal/app/runtime_settings.go:134 msgid "Could not open Fenrir Menu: {0}" msgstr "Nie można otworzyć menu Fenrira: {0}" @@ -708,7 +716,7 @@ msgstr "Nie można przywrócić ustawień aplikacji: {0}" msgid "Could not save exceptions: {0}" msgstr "Nie można zapisać wyjątków: {0}" -#: internal/app/fenrir_menu.go:200 internal/app/fenrir_menu.go:206 +#: internal/app/fenrir_menu.go:203 internal/app/fenrir_menu.go:209 #: internal/app/runtime_settings.go:177 internal/app/runtime_settings.go:205 msgid "Could not save settings: {0}" msgstr "Nie można zapisać ustawień: {0}" @@ -769,7 +777,7 @@ msgstr "kamień do curlingu" msgid "Curry Rice" msgstr "ryż curry" -#: internal/config/config.go:152 +#: internal/config/config.go:153 msgid "Cursor echo" msgstr "Echo kursora" @@ -814,6 +822,10 @@ msgstr "Usuń kanał" msgid "Denmark" msgstr "Dania" +#: internal/config/definitions.go:168 +msgid "Desktop" +msgstr "Komputer stacjonarny" + #: internal/emoji/catalog.go:399 msgid "Desktop computer" msgstr "Komputer stacjonarny" @@ -910,7 +922,7 @@ msgstr "orzeł" msgid "Ear" msgstr "ucho" -#: internal/config/config.go:148 +#: internal/config/config.go:149 msgid "Echo on CapsLock" msgstr "Echo przy CapsLock" @@ -942,7 +954,7 @@ msgstr "Emoji" msgid "Emoji Menu" msgstr "Menu emoji" -#: internal/config/config.go:166 +#: internal/config/config.go:167 msgid "Emoticons" msgstr "Emotikony" @@ -1033,6 +1045,10 @@ msgstr "spadające liście" msgid "Fenrir Menu" msgstr "Menu Fenrira" +#: internal/preferences/modifiers.go:62 +msgid "Fenrir keys" +msgstr "Klawisze Fenrira" + #: internal/app/commands.go:98 msgid "Fenrir screen reader version {0}-{1}" msgstr "Czytnik ekranu Fenrir, wersja {0}-{1}" @@ -1369,7 +1385,7 @@ msgstr "Diabełek" msgid "Import to Clipboard" msgstr "Importuj do schowka" -#: internal/config/config.go:159 +#: internal/config/config.go:160 msgid "Indentation" msgstr "Wcięcia" @@ -1385,6 +1401,10 @@ msgstr "Informacje niedostępne" msgid "Input {0}" msgstr "Wejście {0}" +#: internal/preferences/modifiers.go:20 +msgid "Insert" +msgstr "Insert" + #: internal/config/config.go:60 msgid "Internal UI" msgstr "Interfejs wewnętrzny" @@ -1425,6 +1445,10 @@ msgstr "kangur" msgid "Keyboard" msgstr "Klawiatura" +#: internal/preferences/modifiers.go:19 +msgid "Keypad Insert" +msgstr "Insert na klawiaturze numerycznej" + #: internal/emoji/catalog.go:378 msgid "Kick Scooter" msgstr "hulajnoga" @@ -1453,7 +1477,7 @@ msgstr "Lakros" msgid "Lady beetle" msgstr "Biedronka" -#: internal/emoji/catalog.go:398 +#: internal/config/definitions.go:168 internal/emoji/catalog.go:398 msgid "Laptop" msgstr "Laptop" @@ -1473,7 +1497,7 @@ msgstr "Ostatni komunikat" msgid "Laughing" msgstr "Śmiech" -#: internal/config/config.go:145 +#: internal/config/config.go:146 msgid "Layout" msgstr "Układ klawiatury" @@ -1630,7 +1654,7 @@ msgstr "mikrofon" msgid "Minibus" msgstr "minibus" -#: internal/config/config.go:165 +#: internal/config/config.go:166 msgid "Misspelling sound" msgstr "Dźwięk błędu pisowni" @@ -1831,7 +1855,7 @@ msgstr "Poza trybem przeglądu" msgid "Not in window Mode" msgstr "Nie znajdujesz się w trybie okna." -#: internal/preferences/settings.go:93 +#: internal/preferences/settings.go:96 msgid "OK" msgstr "OK" @@ -2076,11 +2100,11 @@ msgstr "Monitorowanie postępu włączone" msgid "PulseAudio" msgstr "PulseAudio" -#: internal/config/config.go:155 +#: internal/config/config.go:156 msgid "Punctuation level" msgstr "Poziom interpunkcji" -#: internal/config/config.go:154 +#: internal/config/config.go:155 msgid "Punctuation profile" msgstr "Profil interpunkcji" @@ -2112,7 +2136,7 @@ msgstr "wagon kolejowy" msgid "Rainbow" msgstr "Tęcza" -#: internal/config/config.go:156 +#: internal/config/config.go:157 msgid "Raise pitch for capital letters" msgstr "Podnieś wysokość głosu dla wielkich liter" @@ -2184,7 +2208,7 @@ msgstr "Czytaj obszar dynamiczny 8" msgid "Read live region 9" msgstr "Czytaj obszar dynamiczny 9" -#: internal/config/config.go:163 +#: internal/config/config.go:164 msgid "Read misspelled word" msgstr "Czytaj błędnie napisane słowo" @@ -2224,11 +2248,11 @@ msgstr "Usuwa znaczniki z zaznaczonego tekstu" msgid "Rename channel" msgstr "Zmień nazwę kanału" -#: internal/config/config.go:167 +#: internal/config/config.go:168 msgid "Repeated symbols" msgstr "Powtarzane symbole" -#: internal/config/config.go:168 +#: internal/config/config.go:169 msgid "Repeated symbols minimum length" msgstr "Minimalna liczba powtarzanych symboli" @@ -2344,11 +2368,11 @@ msgstr "Delektowanie się jedzeniem" msgid "Saxophone" msgstr "saksofon" -#: internal/config/config.go:158 +#: internal/config/config.go:159 msgid "Say “cap” before capital letters" msgstr "Mów „duża” przed wielkimi literami" -#: internal/config/config.go:164 +#: internal/config/config.go:165 msgid "Say “misspelled”" msgstr "Mów „błędna pisownia”" @@ -2376,6 +2400,10 @@ msgstr "Plik skryptu nie jest wykonywalny" msgid "Script file not found" msgstr "Nie znaleziono pliku skryptu" +#: internal/preferences/modifiers.go:63 +msgid "Script keys" +msgstr "Klawisze skryptów" + #: internal/app/scripts.go msgid "Script source is not a valid file" msgstr "Źródło skryptu nie jest prawidłowym plikiem" @@ -2697,6 +2725,10 @@ msgstr "Słonecznik" msgid "Sunglasses" msgstr "okulary słoneczne" +#: internal/preferences/modifiers.go:22 +msgid "Super" +msgstr "Super" + #: internal/emoji/catalog.go:98 msgid "Superhero" msgstr "superbohater" @@ -2769,11 +2801,11 @@ msgstr "telewizja" msgid "Tennis ball" msgstr "Piłka tenisowa" -#: internal/preferences/settings.go:83 +#: internal/preferences/settings.go:86 msgid "Test synthesizer" msgstr "Testuj syntezator" -#: internal/app/fenrir_menu.go:165 +#: internal/app/fenrir_menu.go:168 msgid "Testing voice" msgstr "Testowanie głosu" @@ -2821,7 +2853,7 @@ msgstr "Góra." msgid "Tornado" msgstr "tornado" -#: internal/config/config.go:153 +#: internal/config/config.go:154 msgid "Tracking mode" msgstr "Tryb śledzenia" @@ -3086,7 +3118,7 @@ msgstr "Wilk" msgid "Word" msgstr "Słowo" -#: internal/config/config.go:147 +#: internal/config/config.go:148 msgid "Word echo" msgstr "Echo słów" @@ -3176,7 +3208,7 @@ msgstr "wykrzyknik" msgid "bar" msgstr "kreska pionowa" -#: internal/config/definitions.go:206 +#: internal/config/definitions.go:212 msgid "beeps" msgstr "sygnały" @@ -3307,7 +3339,7 @@ msgstr "Aktualna linia" msgid "current word." msgstr "Aktualne słowo" -#: internal/config/definitions.go:225 internal/reader/reader.go:252 +#: internal/config/definitions.go:231 internal/reader/reader.go:252 msgid "cursor tracking" msgstr "Śledzenie kursora." @@ -3325,7 +3357,7 @@ msgid "dash" msgstr "łącznik" #: internal/app/quick_settings.go:185 internal/app/quick_settings.go:234 -#: internal/config/definitions.go:237 internal/preferences/discovery.go:45 +#: internal/config/definitions.go:243 internal/preferences/discovery.go:45 #: internal/preferences/discovery.go:55 internal/preferences/discovery.go:81 #: internal/presentation/attributes.go:18 #: internal/presentation/attributes.go:44 @@ -3507,7 +3539,7 @@ msgstr "ustawiono wiersz nagłówka" msgid "heart" msgstr "serce" -#: internal/config/definitions.go:225 internal/reader/reader.go:249 +#: internal/config/definitions.go:231 internal/reader/reader.go:249 msgid "highlight tracking" msgstr "Śledzenie podświetlenia." @@ -3695,7 +3727,7 @@ msgstr "brak danych tabeli" msgid "no text console" msgstr "brak konsoli tekstowej" -#: internal/config/definitions.go:206 internal/punctuation/profile.go +#: internal/config/definitions.go:212 internal/punctuation/profile.go msgid "none" msgstr "brak" @@ -3913,11 +3945,11 @@ msgstr "spacja" msgid "speaks the contents of the currently selected clipboard" msgstr "Wypowiada treść aktualnie wybranego showka" -#: internal/config/definitions.go:206 +#: internal/config/definitions.go:212 msgid "speech" msgstr "mowa" -#: internal/config/definitions.go:206 +#: internal/config/definitions.go:212 msgid "speech and beeps" msgstr "mowa i sygnały" @@ -3978,7 +4010,7 @@ msgstr "zaskoczenie" msgid "tab" msgstr "tabulator" -#: internal/config/definitions.go:225 internal/reader/reader.go:246 +#: internal/config/definitions.go:231 internal/reader/reader.go:246 msgid "table mode" msgstr "tryb tabeli" @@ -4092,7 +4124,7 @@ msgstr "{0} usunięte." msgid "{0} tab" msgstr "{0} zakładka" -#: internal/punctuation/text_processing.go:69 +#: internal/punctuation/text_processing.go:92 msgid "{0} {1}" msgstr "{0} {1}" diff --git a/internal/i18n/locale/pt/LC_MESSAGES/fenrir.po b/internal/i18n/locale/pt/LC_MESSAGES/fenrir.po index 3e2bd659..d68cd6e4 100644 --- a/internal/i18n/locale/pt/LC_MESSAGES/fenrir.po +++ b/internal/i18n/locale/pt/LC_MESSAGES/fenrir.po @@ -155,11 +155,11 @@ msgstr "Armadura" msgid "Astronaut" msgstr "Astronauta" -#: internal/config/config.go:161 +#: internal/config/config.go:162 msgid "Attribute format" msgstr "Formato dos atributos" -#: internal/config/config.go:160 +#: internal/config/config.go:161 msgid "Attributes" msgstr "Atributos" @@ -171,11 +171,11 @@ msgstr "Austrália" msgid "Auto Rickshaw" msgstr "Automóvel riquixá" -#: internal/config/config.go:151 +#: internal/config/config.go:152 msgid "Automatic reading" msgstr "Leitura automática" -#: internal/config/config.go:162 +#: internal/config/config.go:163 msgid "Automatic spell checking" msgstr "Verificação ortográfica automática" @@ -271,7 +271,7 @@ msgstr "Castor" msgid "Bee" msgstr "Abelha" -#: internal/config/config.go:157 +#: internal/config/config.go:158 msgid "Beep for capital letters" msgstr "Sinal sonoro para letras maiúsculas" @@ -412,7 +412,7 @@ msgid "Canada" msgstr "Canadá" #: internal/app/application_overrides.go:360 -#: internal/preferences/settings.go:93 +#: internal/preferences/settings.go:96 msgid "Cancel" msgstr "Cancelar" @@ -432,6 +432,10 @@ msgstr "Comida enlatada" msgid "Canoe" msgstr "Canoa" +#: internal/preferences/modifiers.go:21 +msgid "Caps Lock" +msgstr "Caps Lock" + #: internal/emoji/catalog.go:263 msgid "Carrot" msgstr "Cenoura" @@ -448,7 +452,7 @@ msgstr "Correntes" msgid "Channel name {0}" msgstr "Nome do canal {0}" -#: internal/config/config.go:146 +#: internal/config/config.go:147 msgid "Character echo" msgstr "Eco de caracteres" @@ -638,6 +642,10 @@ msgstr "Número da coluna do cursor" msgid "Comet" msgstr "Cometa" +#: internal/preferences/modifiers.go:102 internal/preferences/modifiers.go:67 +msgid "Compose" +msgstr "Composição" + #: internal/emoji/catalog.go:402 msgid "Computer mouse" msgstr "Mouse de computador" @@ -689,7 +697,7 @@ msgid "Could not open Emoji Menu: {0}" msgstr "Não foi possível abrir o menu de emojis: {0}" #: internal/app/fenrir_menu.go:40 internal/app/fenrir_menu.go:69 -#: internal/app/fenrir_menu.go:89 internal/app/runtime_settings.go:134 +#: internal/app/fenrir_menu.go:92 internal/app/runtime_settings.go:134 msgid "Could not open Fenrir Menu: {0}" msgstr "Não foi possível abrir o menu do Fenrir: {0}" @@ -706,7 +714,7 @@ msgstr "Não foi possível restaurar as configurações do aplicativo: {0}" msgid "Could not save exceptions: {0}" msgstr "Não foi possível salvar as exceções: {0}" -#: internal/app/fenrir_menu.go:200 internal/app/fenrir_menu.go:206 +#: internal/app/fenrir_menu.go:203 internal/app/fenrir_menu.go:209 #: internal/app/runtime_settings.go:177 internal/app/runtime_settings.go:205 msgid "Could not save settings: {0}" msgstr "Não foi possível salvar as configurações: {0}" @@ -767,7 +775,7 @@ msgstr "Pedra de curling" msgid "Curry Rice" msgstr "Arroz com curry" -#: internal/config/config.go:152 +#: internal/config/config.go:153 msgid "Cursor echo" msgstr "Eco do cursor" @@ -812,6 +820,10 @@ msgstr "Excluir canal" msgid "Denmark" msgstr "Dinamarca" +#: internal/config/definitions.go:168 +msgid "Desktop" +msgstr "Computador de mesa" + #: internal/emoji/catalog.go:399 msgid "Desktop computer" msgstr "Computador de mesa" @@ -908,7 +920,7 @@ msgstr "Águia" msgid "Ear" msgstr "Orelha" -#: internal/config/config.go:148 +#: internal/config/config.go:149 msgid "Echo on CapsLock" msgstr "Eco com CapsLock" @@ -940,7 +952,7 @@ msgstr "Emoji" msgid "Emoji Menu" msgstr "Menu de emojis" -#: internal/config/config.go:166 +#: internal/config/config.go:167 msgid "Emoticons" msgstr "Emoticons" @@ -1033,6 +1045,10 @@ msgstr "Folhas caídas" msgid "Fenrir Menu" msgstr "Menu do Fenrir" +#: internal/preferences/modifiers.go:62 +msgid "Fenrir keys" +msgstr "Teclas do Fenrir" + #: internal/app/commands.go:98 msgid "Fenrir screen reader version {0}-{1}" msgstr "Leitor de tela Fenrir versão {0}-{1}" @@ -1369,7 +1385,7 @@ msgstr "Diabinho" msgid "Import to Clipboard" msgstr "Importar para a área de transferência" -#: internal/config/config.go:159 +#: internal/config/config.go:160 msgid "Indentation" msgstr "Recuo" @@ -1385,6 +1401,10 @@ msgstr "Informação indisponível" msgid "Input {0}" msgstr "Entrada {0}" +#: internal/preferences/modifiers.go:20 +msgid "Insert" +msgstr "Insert" + #: internal/config/config.go:60 msgid "Internal UI" msgstr "Interface interna" @@ -1425,6 +1445,10 @@ msgstr "Canguru" msgid "Keyboard" msgstr "Teclado" +#: internal/preferences/modifiers.go:19 +msgid "Keypad Insert" +msgstr "Insert do teclado numérico" + #: internal/emoji/catalog.go:378 msgid "Kick Scooter" msgstr "Patinete" @@ -1453,7 +1477,7 @@ msgstr "Lacrosse" msgid "Lady beetle" msgstr "Joaninha" -#: internal/emoji/catalog.go:398 +#: internal/config/definitions.go:168 internal/emoji/catalog.go:398 msgid "Laptop" msgstr "Laptop" @@ -1473,7 +1497,7 @@ msgstr "Última mensagem" msgid "Laughing" msgstr "Rindo" -#: internal/config/config.go:145 +#: internal/config/config.go:146 msgid "Layout" msgstr "Layout do teclado" @@ -1630,7 +1654,7 @@ msgstr "Microfone" msgid "Minibus" msgstr "Van" -#: internal/config/config.go:165 +#: internal/config/config.go:166 msgid "Misspelling sound" msgstr "Som de erro ortográfico" @@ -1834,7 +1858,7 @@ msgstr "Fora do modo de revisão" msgid "Not in window Mode" msgstr "Não no Modo de Janela" -#: internal/preferences/settings.go:93 +#: internal/preferences/settings.go:96 msgid "OK" msgstr "OK" @@ -2080,11 +2104,11 @@ msgstr "Monitoramento de progresso ativado" msgid "PulseAudio" msgstr "PulseAudio" -#: internal/config/config.go:155 +#: internal/config/config.go:156 msgid "Punctuation level" msgstr "Nível de pontuação" -#: internal/config/config.go:154 +#: internal/config/config.go:155 msgid "Punctuation profile" msgstr "Perfil de pontuação" @@ -2116,7 +2140,7 @@ msgstr "Vagão de trem" msgid "Rainbow" msgstr "Arco-íris" -#: internal/config/config.go:156 +#: internal/config/config.go:157 msgid "Raise pitch for capital letters" msgstr "Elevar o tom para letras maiúsculas" @@ -2188,7 +2212,7 @@ msgstr "Ler região dinâmica 8" msgid "Read live region 9" msgstr "Ler região dinâmica 9" -#: internal/config/config.go:163 +#: internal/config/config.go:164 msgid "Read misspelled word" msgstr "Ler palavra com erro ortográfico" @@ -2228,11 +2252,11 @@ msgstr "Remove as marcas do texto selecionado" msgid "Rename channel" msgstr "Renomear canal" -#: internal/config/config.go:167 +#: internal/config/config.go:168 msgid "Repeated symbols" msgstr "Símbolos repetidos" -#: internal/config/config.go:168 +#: internal/config/config.go:169 msgid "Repeated symbols minimum length" msgstr "Quantidade mínima de símbolos repetidos" @@ -2348,11 +2372,11 @@ msgstr "Saboreando comida" msgid "Saxophone" msgstr "Saxofone" -#: internal/config/config.go:158 +#: internal/config/config.go:159 msgid "Say “cap” before capital letters" msgstr "Dizer “maiúscula” antes das letras maiúsculas" -#: internal/config/config.go:164 +#: internal/config/config.go:165 msgid "Say “misspelled”" msgstr "Dizer “erro ortográfico”" @@ -2380,6 +2404,10 @@ msgstr "O arquivo de script não é executável" msgid "Script file not found" msgstr "Arquivo de script não encontrado" +#: internal/preferences/modifiers.go:63 +msgid "Script keys" +msgstr "Teclas de scripts" + #: internal/app/scripts.go msgid "Script source is not a valid file" msgstr "A origem do script não é um arquivo válido" @@ -2701,6 +2729,10 @@ msgstr "Girassol" msgid "Sunglasses" msgstr "Óculos escuros" +#: internal/preferences/modifiers.go:22 +msgid "Super" +msgstr "Super" + #: internal/emoji/catalog.go:98 msgid "Superhero" msgstr "Super herói" @@ -2773,11 +2805,11 @@ msgstr "Televisão" msgid "Tennis ball" msgstr "Bola de tênis" -#: internal/preferences/settings.go:83 +#: internal/preferences/settings.go:86 msgid "Test synthesizer" msgstr "Testar sintetizador" -#: internal/app/fenrir_menu.go:165 +#: internal/app/fenrir_menu.go:168 msgid "Testing voice" msgstr "Testando a voz" @@ -2825,7 +2857,7 @@ msgstr "Topo" msgid "Tornado" msgstr "Tornado" -#: internal/config/config.go:153 +#: internal/config/config.go:154 msgid "Tracking mode" msgstr "Modo de acompanhamento" @@ -3091,7 +3123,7 @@ msgstr "Lobo" msgid "Word" msgstr "Palavra" -#: internal/config/config.go:147 +#: internal/config/config.go:148 msgid "Word echo" msgstr "Eco de palavras" @@ -3181,7 +3213,7 @@ msgstr "exclamação" msgid "bar" msgstr "barra vertical" -#: internal/config/definitions.go:206 +#: internal/config/definitions.go:212 msgid "beeps" msgstr "sinais sonoros" @@ -3313,7 +3345,7 @@ msgstr "linha atual" msgid "current word." msgstr "palavra atual." -#: internal/config/definitions.go:225 internal/reader/reader.go:252 +#: internal/config/definitions.go:231 internal/reader/reader.go:252 msgid "cursor tracking" msgstr "rastreamento do cursor" @@ -3331,7 +3363,7 @@ msgid "dash" msgstr "hífen" #: internal/app/quick_settings.go:185 internal/app/quick_settings.go:234 -#: internal/config/definitions.go:237 internal/preferences/discovery.go:45 +#: internal/config/definitions.go:243 internal/preferences/discovery.go:45 #: internal/preferences/discovery.go:55 internal/preferences/discovery.go:81 #: internal/presentation/attributes.go:18 #: internal/presentation/attributes.go:44 @@ -3514,7 +3546,7 @@ msgstr "linha de cabeçalho definida" msgid "heart" msgstr "coração" -#: internal/config/definitions.go:225 internal/reader/reader.go:249 +#: internal/config/definitions.go:231 internal/reader/reader.go:249 msgid "highlight tracking" msgstr "rastreamento em destaque" @@ -3706,7 +3738,7 @@ msgstr "sem dados de tabela" msgid "no text console" msgstr "nenhum console de texto" -#: internal/config/definitions.go:206 internal/punctuation/profile.go +#: internal/config/definitions.go:212 internal/punctuation/profile.go msgid "none" msgstr "nenhum" @@ -3926,11 +3958,11 @@ msgstr "espaço" msgid "speaks the contents of the currently selected clipboard" msgstr "fala o conteúdo da área de transferência atualmente selecionada" -#: internal/config/definitions.go:206 +#: internal/config/definitions.go:212 msgid "speech" msgstr "fala" -#: internal/config/definitions.go:206 +#: internal/config/definitions.go:212 msgid "speech and beeps" msgstr "fala e sinais sonoros" @@ -3991,7 +4023,7 @@ msgstr "surpreso" msgid "tab" msgstr "tabulação" -#: internal/config/definitions.go:225 internal/reader/reader.go:246 +#: internal/config/definitions.go:231 internal/reader/reader.go:246 msgid "table mode" msgstr "modo de tabela" @@ -4105,7 +4137,7 @@ msgstr "{0} removido" msgid "{0} tab" msgstr "{0} aba" -#: internal/punctuation/text_processing.go:69 +#: internal/punctuation/text_processing.go:92 msgid "{0} {1}" msgstr "{0} {1}" diff --git a/internal/i18n/locale/ru/LC_MESSAGES/fenrir.po b/internal/i18n/locale/ru/LC_MESSAGES/fenrir.po index f4372e0c..c2dc536c 100644 --- a/internal/i18n/locale/ru/LC_MESSAGES/fenrir.po +++ b/internal/i18n/locale/ru/LC_MESSAGES/fenrir.po @@ -154,11 +154,11 @@ msgstr "Броня" msgid "Astronaut" msgstr "Астронавт" -#: internal/config/config.go:161 +#: internal/config/config.go:162 msgid "Attribute format" msgstr "Формат атрибутов" -#: internal/config/config.go:160 +#: internal/config/config.go:161 msgid "Attributes" msgstr "Атрибуты" @@ -170,11 +170,11 @@ msgstr "Австралия" msgid "Auto Rickshaw" msgstr "Авторикша" -#: internal/config/config.go:151 +#: internal/config/config.go:152 msgid "Automatic reading" msgstr "Автоматическое чтение" -#: internal/config/config.go:162 +#: internal/config/config.go:163 msgid "Automatic spell checking" msgstr "Автоматическая проверка правописания" @@ -270,7 +270,7 @@ msgstr "Бобр" msgid "Bee" msgstr "Пчела" -#: internal/config/config.go:157 +#: internal/config/config.go:158 msgid "Beep for capital letters" msgstr "Звуковой сигнал для заглавных букв" @@ -411,7 +411,7 @@ msgid "Canada" msgstr "Канада" #: internal/app/application_overrides.go:360 -#: internal/preferences/settings.go:93 +#: internal/preferences/settings.go:96 msgid "Cancel" msgstr "Отмена" @@ -431,6 +431,10 @@ msgstr "Консервы" msgid "Canoe" msgstr "Каноэ" +#: internal/preferences/modifiers.go:21 +msgid "Caps Lock" +msgstr "Caps Lock" + #: internal/emoji/catalog.go:263 msgid "Carrot" msgstr "Морковь" @@ -447,7 +451,7 @@ msgstr "Цепи" msgid "Channel name {0}" msgstr "Название канала {0}" -#: internal/config/config.go:146 +#: internal/config/config.go:147 msgid "Character echo" msgstr "Эхо символов" @@ -637,6 +641,10 @@ msgstr "Номер столбца для курсора." msgid "Comet" msgstr "Комета" +#: internal/preferences/modifiers.go:102 internal/preferences/modifiers.go:67 +msgid "Compose" +msgstr "Compose" + #: internal/emoji/catalog.go:402 msgid "Computer mouse" msgstr "Компьютерная мышь" @@ -688,7 +696,7 @@ msgid "Could not open Emoji Menu: {0}" msgstr "Не удалось открыть меню эмодзи: {0}" #: internal/app/fenrir_menu.go:40 internal/app/fenrir_menu.go:69 -#: internal/app/fenrir_menu.go:89 internal/app/runtime_settings.go:134 +#: internal/app/fenrir_menu.go:92 internal/app/runtime_settings.go:134 msgid "Could not open Fenrir Menu: {0}" msgstr "Не удалось открыть меню Fenrir: {0}" @@ -705,7 +713,7 @@ msgstr "Не удалось восстановить настройки прил msgid "Could not save exceptions: {0}" msgstr "Не удалось сохранить исключения: {0}" -#: internal/app/fenrir_menu.go:200 internal/app/fenrir_menu.go:206 +#: internal/app/fenrir_menu.go:203 internal/app/fenrir_menu.go:209 #: internal/app/runtime_settings.go:177 internal/app/runtime_settings.go:205 msgid "Could not save settings: {0}" msgstr "Не удалось сохранить настройки: {0}" @@ -766,7 +774,7 @@ msgstr "Керлинг" msgid "Curry Rice" msgstr "Рис с карри" -#: internal/config/config.go:152 +#: internal/config/config.go:153 msgid "Cursor echo" msgstr "Озвучивание курсора" @@ -811,6 +819,10 @@ msgstr "Удалить канал" msgid "Denmark" msgstr "Дания" +#: internal/config/definitions.go:168 +msgid "Desktop" +msgstr "Настольный компьютер" + #: internal/emoji/catalog.go:399 msgid "Desktop computer" msgstr "Настольный компьютер" @@ -907,7 +919,7 @@ msgstr "Орел" msgid "Ear" msgstr "Ухо" -#: internal/config/config.go:148 +#: internal/config/config.go:149 msgid "Echo on CapsLock" msgstr "Эхо при CapsLock" @@ -939,7 +951,7 @@ msgstr "Эмодзи" msgid "Emoji Menu" msgstr "Меню эмодзи" -#: internal/config/config.go:166 +#: internal/config/config.go:167 msgid "Emoticons" msgstr "Текстовые смайлики" @@ -1029,6 +1041,10 @@ msgstr "Падающие листья" msgid "Fenrir Menu" msgstr "Меню Fenrir" +#: internal/preferences/modifiers.go:62 +msgid "Fenrir keys" +msgstr "Клавиши Fenrir" + #: internal/app/commands.go:98 msgid "Fenrir screen reader version {0}-{1}" msgstr "Программа экранного доступа Fenrir, версия {0}-{1}" @@ -1365,7 +1381,7 @@ msgstr "Бесёнок" msgid "Import to Clipboard" msgstr "Импортировать в буфер обмена" -#: internal/config/config.go:159 +#: internal/config/config.go:160 msgid "Indentation" msgstr "Отступы" @@ -1381,6 +1397,10 @@ msgstr "Информация недоступна" msgid "Input {0}" msgstr "Вход {0}" +#: internal/preferences/modifiers.go:20 +msgid "Insert" +msgstr "Insert" + #: internal/config/config.go:60 msgid "Internal UI" msgstr "Внутренний интерфейс" @@ -1421,6 +1441,10 @@ msgstr "Кенгуру" msgid "Keyboard" msgstr "Клавиатура" +#: internal/preferences/modifiers.go:19 +msgid "Keypad Insert" +msgstr "Insert на цифровой клавиатуре" + #: internal/emoji/catalog.go:378 msgid "Kick Scooter" msgstr "Самокат" @@ -1449,7 +1473,7 @@ msgstr "Лакросс" msgid "Lady beetle" msgstr "Божья коровка" -#: internal/emoji/catalog.go:398 +#: internal/config/definitions.go:168 internal/emoji/catalog.go:398 msgid "Laptop" msgstr "Ноутбук" @@ -1469,7 +1493,7 @@ msgstr "Последнее сообщение" msgid "Laughing" msgstr "Смех" -#: internal/config/config.go:145 +#: internal/config/config.go:146 msgid "Layout" msgstr "Раскладка клавиатуры" @@ -1626,7 +1650,7 @@ msgstr "Микрофон" msgid "Minibus" msgstr "Микроавтобус" -#: internal/config/config.go:165 +#: internal/config/config.go:166 msgid "Misspelling sound" msgstr "Звук ошибки правописания" @@ -1826,7 +1850,7 @@ msgstr "Нет в режиме просмотра." msgid "Not in window Mode" msgstr "Не в режиме окна." -#: internal/preferences/settings.go:93 +#: internal/preferences/settings.go:96 msgid "OK" msgstr "ОК" @@ -2072,11 +2096,11 @@ msgstr "Отслеживание хода выполнения включено" msgid "PulseAudio" msgstr "PulseAudio" -#: internal/config/config.go:155 +#: internal/config/config.go:156 msgid "Punctuation level" msgstr "Уровень пунктуации" -#: internal/config/config.go:154 +#: internal/config/config.go:155 msgid "Punctuation profile" msgstr "Профиль пунктуации" @@ -2108,7 +2132,7 @@ msgstr "Вагон" msgid "Rainbow" msgstr "Радуга" -#: internal/config/config.go:156 +#: internal/config/config.go:157 msgid "Raise pitch for capital letters" msgstr "Повышать тон для заглавных букв" @@ -2180,7 +2204,7 @@ msgstr "Прочитать динамическую область 8" msgid "Read live region 9" msgstr "Прочитать динамическую область 9" -#: internal/config/config.go:163 +#: internal/config/config.go:164 msgid "Read misspelled word" msgstr "Читать слово с ошибкой" @@ -2220,11 +2244,11 @@ msgstr "Удалить метки из выбранного текста." msgid "Rename channel" msgstr "Переименовать канал" -#: internal/config/config.go:167 +#: internal/config/config.go:168 msgid "Repeated symbols" msgstr "Повторяющиеся символы" -#: internal/config/config.go:168 +#: internal/config/config.go:169 msgid "Repeated symbols minimum length" msgstr "Минимальное число повторяющихся символов" @@ -2340,11 +2364,11 @@ msgstr "Наслаждение едой" msgid "Saxophone" msgstr "Саксофон" -#: internal/config/config.go:158 +#: internal/config/config.go:159 msgid "Say “cap” before capital letters" msgstr "Говорить «большая» перед заглавными буквами" -#: internal/config/config.go:164 +#: internal/config/config.go:165 msgid "Say “misspelled”" msgstr "Говорить «ошибка»" @@ -2372,6 +2396,10 @@ msgstr "Файл скрипта не исполняемый" msgid "Script file not found" msgstr "Скрипт не найден" +#: internal/preferences/modifiers.go:63 +msgid "Script keys" +msgstr "Клавиши скриптов" + #: internal/app/scripts.go msgid "Script source is not a valid file" msgstr "Исходник скрипта неправильный файл" @@ -2693,6 +2721,10 @@ msgstr "Подсолнух" msgid "Sunglasses" msgstr "Солнечные очки" +#: internal/preferences/modifiers.go:22 +msgid "Super" +msgstr "Super" + #: internal/emoji/catalog.go:98 msgid "Superhero" msgstr "Супергерой" @@ -2765,11 +2797,11 @@ msgstr "Телевизор" msgid "Tennis ball" msgstr "Теннисный мяч" -#: internal/preferences/settings.go:83 +#: internal/preferences/settings.go:86 msgid "Test synthesizer" msgstr "Проверить синтезатор" -#: internal/app/fenrir_menu.go:165 +#: internal/app/fenrir_menu.go:168 msgid "Testing voice" msgstr "Проверка голоса" @@ -2817,7 +2849,7 @@ msgstr "Начало" msgid "Tornado" msgstr "Торнадо" -#: internal/config/config.go:153 +#: internal/config/config.go:154 msgid "Tracking mode" msgstr "Режим отслеживания" @@ -3083,7 +3115,7 @@ msgstr "Волк" msgid "Word" msgstr "Слово" -#: internal/config/config.go:147 +#: internal/config/config.go:148 msgid "Word echo" msgstr "Эхо слов" @@ -3173,7 +3205,7 @@ msgstr "восклицательный знак" msgid "bar" msgstr "вертикальная черта" -#: internal/config/definitions.go:206 +#: internal/config/definitions.go:212 msgid "beeps" msgstr "звуковые сигналы" @@ -3304,7 +3336,7 @@ msgstr "Текущая строка." msgid "current word." msgstr "Текущее слово." -#: internal/config/definitions.go:225 internal/reader/reader.go:252 +#: internal/config/definitions.go:231 internal/reader/reader.go:252 msgid "cursor tracking" msgstr "Отслеживание курсора" @@ -3322,7 +3354,7 @@ msgid "dash" msgstr "дефис" #: internal/app/quick_settings.go:185 internal/app/quick_settings.go:234 -#: internal/config/definitions.go:237 internal/preferences/discovery.go:45 +#: internal/config/definitions.go:243 internal/preferences/discovery.go:45 #: internal/preferences/discovery.go:55 internal/preferences/discovery.go:81 #: internal/presentation/attributes.go:18 #: internal/presentation/attributes.go:44 @@ -3503,7 +3535,7 @@ msgstr "строка заголовков задана" msgid "heart" msgstr "сердце" -#: internal/config/definitions.go:225 internal/reader/reader.go:249 +#: internal/config/definitions.go:231 internal/reader/reader.go:249 msgid "highlight tracking" msgstr "Отслеживание выделения" @@ -3691,7 +3723,7 @@ msgstr "нет данных таблицы" msgid "no text console" msgstr "нет текстовой консоли" -#: internal/config/definitions.go:206 internal/punctuation/profile.go +#: internal/config/definitions.go:212 internal/punctuation/profile.go msgid "none" msgstr "нет" @@ -3910,11 +3942,11 @@ msgstr "пробел" msgid "speaks the contents of the currently selected clipboard" msgstr "Говорит содержимое выбранного буфера обмена." -#: internal/config/definitions.go:206 +#: internal/config/definitions.go:212 msgid "speech" msgstr "речь" -#: internal/config/definitions.go:206 +#: internal/config/definitions.go:212 msgid "speech and beeps" msgstr "речь и звуковые сигналы" @@ -3975,7 +4007,7 @@ msgstr "удивление" msgid "tab" msgstr "табуляция" -#: internal/config/definitions.go:225 internal/reader/reader.go:246 +#: internal/config/definitions.go:231 internal/reader/reader.go:246 msgid "table mode" msgstr "режим таблицы" @@ -4089,7 +4121,7 @@ msgstr "{0} удалено" msgid "{0} tab" msgstr "{0}, вкладка" -#: internal/punctuation/text_processing.go:69 +#: internal/punctuation/text_processing.go:92 msgid "{0} {1}" msgstr "{0} {1}" diff --git a/internal/input/bindings.go b/internal/input/bindings.go index 7b0ab331..31376396 100644 --- a/internal/input/bindings.go +++ b/internal/input/bindings.go @@ -26,6 +26,12 @@ import ( "github.com/pelletier/go-toml/v2" ) +type Layout struct { + Version int `toml:"version"` + FenrirKeys []string `toml:"fenrir_keys"` + Bindings []Binding `toml:"bindings"` +} + type Binding struct { Name string `toml:"name"` Keys []string `toml:"keys"` @@ -71,24 +77,48 @@ type chord struct { var modifierNames = map[string]modifiers{"FENRIR": fenrirModifier, "CTRL": ctrlModifier, "SHIFT": shiftModifier, "ALT": altModifier} // Decode_bindings rejects unsupported keys, commands and duplicate chords. -func Decode_bindings(data []byte) ([]Binding, error) { - var file struct { - Version int `toml:"version"` - Bindings []Binding `toml:"bindings"` - } +func Decode_bindings(data []byte) (Layout, error) { + var file Layout if err := toml.NewDecoder(bytes.NewReader(data)).DisallowUnknownFields().Decode(&file); err != nil { - return nil, err + return Layout{}, err } if file.Version != 1 { - return nil, fmt.Errorf("unsupported keyboard version %d", file.Version) + return Layout{}, fmt.Errorf("unsupported keyboard version %d", file.Version) } - _, err := New_router(file.Bindings) - return file.Bindings, err + _, err := New_router(file) + return file, err } // New_router compiles a private layout. Reset and capture preserve these bindings. -func New_router(bindings []Binding) (Router, error) { +func New_router(layout Layout) (Router, error) { + bindings := layout.Bindings router := Router{bindings: make(map[chord]command.ID), doubleBindings: make(map[chord]command.ID), tripleBindings: make(map[chord]command.ID), multiPressTimeout: DefaultMultiPressTimeout} + keys := layout.FenrirKeys + if len(keys) == 0 { + keys = []string{"INSERT", "SUPER", "KP0"} + } + router.readerKeys = make(map[uint16]bool) + for _, name := range keys { + var codes []uint16 + switch name { + case "INSERT": + codes = []uint16{KeyInsert} + case "SUPER": + codes = []uint16{KeyMeta, KeyRightMeta} + case "CAPSLOCK": + codes = []uint16{KeyCapsLock} + case "KP0": + codes = []uint16{KeyKP0} + default: + return Router{}, fmt.Errorf("unsupported Fenrir modifier %q", name) + } + for _, code := range codes { + if router.readerKeys[code] { + return Router{}, fmt.Errorf("duplicate Fenrir modifier %q", name) + } + router.readerKeys[code] = true + } + } if len(bindings) == 0 { return Router{}, fmt.Errorf("keyboard layout has no bindings") } diff --git a/internal/input/bindings_test.go b/internal/input/bindings_test.go index d82faa61..04c98457 100644 --- a/internal/input/bindings_test.go +++ b/internal/input/bindings_test.go @@ -105,7 +105,7 @@ func TestBothLayoutsProvideAllClipboardCommands(t *testing.T) { t.Fatal(err) } counts := make(map[command.ID]int) - for _, binding := range bindings { + for _, binding := range bindings.Bindings { counts[binding.Command]++ } for _, id := range want { @@ -242,7 +242,7 @@ func TestQuickSettingsChordsInBothLayouts(t *testing.T) { if err != nil { t.Fatal(err) } - for _, binding := range bindings { + for _, binding := range bindings.Bindings { if binding.Command == command.CurrentQuickValue { t.Fatal("bundled layout retains the redundant value-only shortcut", layout, binding) } diff --git a/internal/input/capslock_test.go b/internal/input/capslock_test.go new file mode 100644 index 00000000..f52bd8d3 --- /dev/null +++ b/internal/input/capslock_test.go @@ -0,0 +1,146 @@ +// Copyright (c) 2026 Storm Dragon +// SPDX-License-Identifier: GPL-3.0-or-later + +package input + +import ( + "git.stormux.org/storm/fenrir/internal/command" + "strings" + "testing" +) + +func laptop_router(t *testing.T) Router { + t.Helper() + layout, _, err := Load_bindings([]string{"../../config/keyboard/laptop.toml"}) + if err != nil { + t.Fatal(err) + } + router, err := New_router(layout) + if err != nil { + t.Fatal(err) + } + return router +} + +func TestLaptopCapsLockModifier(t *testing.T) { + for _, releaseCapsFirst := range []bool{false, true} { + r := laptop_router(t) + for range 2 { + if got := r.Key(0, KeyCapsLock, 1, false, false); got.Forward || got.ToggleCapsLock { + t.Fatal(got) + } + if got := r.Key(0, 23, 1, false, false); got.Command != command.CurrentLine || got.Forward || got.Typing { + t.Fatal(got) + } + keys := []uint16{23, KeyCapsLock} + if releaseCapsFirst { + keys = []uint16{KeyCapsLock, 23} + } + for _, key := range keys { + if got := r.Key(0, key, 0, false, false); got.Forward || got.ToggleCapsLock { + t.Fatal(got) + } + } + r.Reset() + } + if got := r.Key(0, KeyMeta, 1, false, false); !got.Forward { + t.Fatal("Super consumed", got) + } + if got := r.Key(0, 23, 1, false, false); got.Command != "" || !got.Forward { + t.Fatal("Super became Fenrir", got) + } + } + desktop := desktop_router(t) + if got := desktop.Key(0, KeyCapsLock, 1, false, false); !got.Forward { + t.Fatal("desktop Caps Lock consumed", got) + } + desktop.Key(0, KeyCapsLock, 0, false, false) + desktop.Key(0, KeyMeta, 1, false, false) + if got := desktop.Key(0, KeyF, 1, false, false); got.Command != command.FenrirMenu { + t.Fatal(got) + } +} + +func TestLaptopShiftCapsLockGesture(t *testing.T) { + for _, shift := range []uint16{KeyShift, KeyRightShift} { + for _, shiftFirst := range []bool{false, true} { + for _, releaseShiftFirst := range []bool{false, true} { + for _, extraKey := range []uint16{0, 22, KeyCtrl} { + r := laptop_router(t) + keys := []uint16{KeyCapsLock, shift} + if shiftFirst { + keys = []uint16{shift, KeyCapsLock} + } + for _, key := range keys { + r.Key(0, key, 1, false, false) + } + if extraKey != 0 { + r.Key(0, extraKey, 1, false, false) + r.Key(0, extraKey, 0, false, false) + } + // Repeats must not toggle or restart the gesture. + if got := r.Key(0, KeyCapsLock, 2, false, false); got.ToggleCapsLock { + t.Fatal(got) + } + if releaseShiftFirst { + r.Key(0, shift, 0, false, false) + } + got := r.Key(0, KeyCapsLock, 0, false, false) + if got.ToggleCapsLock != (extraKey == 0) || got.Forward { + t.Fatal(shift, shiftFirst, releaseShiftFirst, extraKey, got) + } + if !releaseShiftFirst { + r.Key(0, shift, 0, false, false) + } + if got := r.Key(0, KeyCapsLock, 0, false, false); got.ToggleCapsLock { + t.Fatal("orphan release toggled", got) + } + } + } + } + } +} + +func TestCapsLockCapturePassAndRepair(t *testing.T) { + for _, mode := range []string{"capture", "pass", "repair", "drop", "reset"} { + r := laptop_router(t) + if mode == "capture" { + r.Capture(true) + } + if mode == "pass" { + r.Pass_next() + } + r.Key(0, KeyShift, 1, false, false) + got := r.Key(0, KeyCapsLock, 1, false, false) + if mode == "pass" && !got.Forward { + t.Fatal("pass-next failed", got) + } + switch mode { + case "repair": + r.Key(1, KeyCtrl, 1, false, true) + case "drop": + r.Drop(1) + case "reset": + r.Reset() + } + if got := r.Key(0, KeyCapsLock, 0, false, false); got.ToggleCapsLock { + t.Fatal(mode, got) + } + } +} + +func TestLayoutModifierValidation(t *testing.T) { + for _, keys := range []string{`["UNKNOWN"]`, `["CAPSLOCK", "CAPSLOCK"]`} { + data := strings.Replace(string(desktop_data(t)), "version = 1", "version = 1\nfenrir_keys = "+keys, 1) + if _, err := Decode_bindings([]byte(data)); err == nil { + t.Fatal("invalid modifier keys accepted", keys) + } + } + r := laptop_router(t) + if got := r.script_name(KeyCapsLock, false); got != "KEY_FENRIR" { + t.Fatal(got) + } + if got := r.script_name(KeyMeta, false); got == "KEY_FENRIR" { + t.Fatal(got) + } +} diff --git a/internal/input/double_press_test.go b/internal/input/double_press_test.go index c4655485..1c276869 100644 --- a/internal/input/double_press_test.go +++ b/internal/input/double_press_test.go @@ -95,7 +95,7 @@ func TestTimeDateDoublePress(t *testing.T) { func TestDoubleCommandValidationAndRemapping(t *testing.T) { for _, doubleCommand := range []command.ID{command.Date, "unknown"} { - router, err := New_router([]Binding{{Name: "Clock", Keys: []string{"FENRIR", "G"}, Command: command.Time, DoubleCommand: doubleCommand}}) + router, err := New_router(Layout{Bindings: []Binding{{Name: "Clock", Keys: []string{"FENRIR", "G"}, Command: command.Time, DoubleCommand: doubleCommand}}}) if doubleCommand == "unknown" { if err == nil { t.Fatal("unknown double command accepted") diff --git a/internal/input/layout.go b/internal/input/layout.go index f5968016..40e750a1 100644 --- a/internal/input/layout.go +++ b/internal/input/layout.go @@ -59,25 +59,25 @@ func Layout_paths(layout, home string, uid int, bundledDirectory string) ([]stri // Load_bindings selects one whole file. A malformed or unreadable candidate // stops loading; only absent named candidates advance to the next directory. -func Load_bindings(paths []string) ([]Binding, string, error) { +func Load_bindings(paths []string) (Layout, string, error) { for _, path := range paths { file, err := os.Open(path) if errors.Is(err, os.ErrNotExist) { continue } if err != nil { - return nil, path, err + return Layout{}, path, err } data, readErr := io.ReadAll(io.LimitReader(file, (1<<20)+1)) closeErr := file.Close() if err := errors.Join(readErr, closeErr); err != nil { - return nil, path, err + return Layout{}, path, err } if len(data) > 1<<20 { - return nil, path, fmt.Errorf("keyboard layout exceeds 1 MiB") + return Layout{}, path, fmt.Errorf("keyboard layout exceeds 1 MiB") } bindings, err := Decode_bindings(data) return bindings, path, err } - return nil, "", fmt.Errorf("keyboard layout not found (searched %s)", strings.Join(paths, ", ")) + return Layout{}, "", fmt.Errorf("keyboard layout not found (searched %s)", strings.Join(paths, ", ")) } diff --git a/internal/input/learn.go b/internal/input/learn.go index 7a461dce..5ad3ec65 100644 --- a/internal/input/learn.go +++ b/internal/input/learn.go @@ -34,7 +34,7 @@ func (r *Router) learn_key(device int, key uint16, numLock bool) *LearnKey { if !scriptChord { id = r.binding(key, numLock) } - if !pure_modifier(key) && !reader_modifier(key, numLock) { + if !pure_modifier(key) && !r.reader_modifier(key, numLock) { id = r.multi_press(device, r.binding_chord(key, numLock), id, scriptChord) } pressed.Command = id diff --git a/internal/input/learn_test.go b/internal/input/learn_test.go index 1671bf68..4051efad 100644 --- a/internal/input/learn_test.go +++ b/internal/input/learn_test.go @@ -12,7 +12,7 @@ import ( func TestCapturedKeysDescribeActualBindingsWithoutDispatch(t *testing.T) { bindings := []Binding{{Name: "Custom", Keys: []string{"FENRIR", "Z"}, Command: command.Time, DoubleCommand: command.Date, TripleCommand: command.Quit}} - router, err := New_router(bindings) + router, err := New_router(Layout{Bindings: bindings, FenrirKeys: []string{"INSERT", "KP0"}}) if err != nil { t.Fatal(err) } diff --git a/internal/input/modifiers_linux.go b/internal/input/modifiers_linux.go new file mode 100644 index 00000000..72c31ea7 --- /dev/null +++ b/internal/input/modifiers_linux.go @@ -0,0 +1,154 @@ +// Copyright (c) 2026 Storm Dragon +// SPDX-License-Identifier: GPL-3.0-or-later + +//go:build linux && cgo + +package input + +import ( + "fmt" + "strings" +) + +// Modifier_codes expands generic modifiers to their physical left/right keys. +func Modifier_codes(names []string) (map[uint16]bool, error) { + keys := make(map[uint16]bool) + for _, name := range names { + code, ok := Key_code(name) + if !ok { + return nil, fmt.Errorf("invalid modifier key %q", name) + } + keys[code] = true + switch strings.ToUpper(name) { + case "KEY_META": + keys[KeyRightMeta] = true + case "KEY_CTRL": + keys[KeyRightCtrl] = true + case "KEY_SHIFT": + keys[KeyRightShift] = true + case "KEY_ALT": + keys[KeyRightAlt] = true + } + } + return keys, nil +} + +// Fenrir_codes validates the supported configurable reader modifiers. +func Fenrir_codes(names []string) (map[uint16]bool, error) { + if len(names) == 0 { + return nil, fmt.Errorf("select at least one Fenrir key") + } + keys, err := Modifier_codes(names) + if err != nil { + return nil, err + } + for code := range keys { + switch code { + case KeyInsert, KeyKP0, KeyCapsLock, KeyMeta, KeyRightMeta: + default: + return nil, fmt.Errorf("unsupported Fenrir key %q", Key_name(code)) + } + } + return keys, nil +} + +func Validate_modifier_keys(fenrirKeys, scriptKeys []string) error { + reader, err := Fenrir_codes(fenrirKeys) + if err != nil { + return err + } + scripts, err := Modifier_codes(scriptKeys) + if err != nil { + return err + } + for code := range scripts { + if reader[code] { + return fmt.Errorf("%s cannot be both a Fenrir key and a script key", Key_name(code)) + } + } + return nil +} + +func (r *Router) Set_fenrir_keys(names []string) error { + keys, err := Fenrir_codes(names) + if err != nil { + return err + } + for key := range keys { + if r.scriptKeys[key] { + return fmt.Errorf("%s cannot be both a Fenrir key and a script key", Key_name(key)) + } + } + r.Reset() + r.readerKeys = keys + return nil +} + +// Fenrir_keys returns the effective selection for the settings form. +func (r *Router) Fenrir_keys() []string { + var keys []string + for _, code := range []uint16{KeyKP0, KeyInsert, KeyCapsLock, KeyMeta, KeyRightMeta} { + if !r.reader_modifier(code, false) { + continue + } + if code == KeyMeta && r.reader_modifier(KeyRightMeta, false) { + keys = append(keys, "KEY_META") + continue + } + if code == KeyRightMeta && r.reader_modifier(KeyMeta, false) { + continue + } + keys = append(keys, Key_name(code)) + } + return keys +} + +func (r *Router) script_modifier(key uint16, numLock bool) bool { + return r.scriptKeys[key] && (key != KeyKP0 || !numLock) +} + +// Available_fenrir_keys keeps explicitly configured script keys out of reader +// defaults. Explicit reader selections are validated instead of being changed. +func Available_fenrir_keys(readerNames, scriptNames []string) ([]string, error) { + reader, err := Fenrir_codes(readerNames) + if err != nil { + return nil, err + } + scripts, err := Modifier_codes(scriptNames) + if err != nil { + return nil, err + } + var names []string + for _, key := range []uint16{KeyKP0, KeyInsert, KeyCapsLock, KeyMeta, KeyRightMeta} { + if reader[key] && !scripts[key] { + names = append(names, Key_name(key)) + } + } + if len(names) == 0 { + return nil, fmt.Errorf("select at least one Fenrir key that is not a script key") + } + return names, nil +} + +func (r *Router) Configure_modifiers(fenrirKeys, scriptKeys []string) error { + if fenrirKeys == nil { + var err error + fenrirKeys, err = Available_fenrir_keys(r.Fenrir_keys(), scriptKeys) + if err != nil { + return err + } + } + if err := Validate_modifier_keys(fenrirKeys, scriptKeys); err != nil { + return err + } + candidate := *r + candidate.scriptKeys = nil + if err := candidate.Set_fenrir_keys(fenrirKeys); err != nil { + return err + } + if err := candidate.Configure_scripts(scriptKeys, nil); err != nil { + return err + } + *r = candidate + return nil +} diff --git a/internal/input/modifiers_test.go b/internal/input/modifiers_test.go new file mode 100644 index 00000000..af289208 --- /dev/null +++ b/internal/input/modifiers_test.go @@ -0,0 +1,96 @@ +// 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 TestModifierConflictsUsePhysicalAliases(t *testing.T) { + for _, pair := range [][2]string{ + {"KEY_INSERT", "KEY_INSERT"}, {"KEY_KP0", "KEY_KP0"}, {"KEY_CAPSLOCK", "KEY_CAPSLOCK"}, + {"KEY_META", "KEY_LEFTMETA"}, {"KEY_META", "KEY_RIGHTMETA"}, {"KEY_RIGHTMETA", "KEY_META"}, + } { + if err := Validate_modifier_keys([]string{pair[0]}, []string{pair[1]}); err == nil { + t.Fatal("accepted overlap", pair) + } + } + r := desktop_router(t) + if err := r.Configure_modifiers(nil, []string{"KEY_META"}); err != nil { + t.Fatal(err) + } + if r.reader_modifier(KeyMeta, false) || r.reader_modifier(KeyRightMeta, false) || !r.scriptKeys[KeyMeta] || !r.scriptKeys[KeyRightMeta] { + t.Fatal("default reader stole explicit script key") + } + if err := r.Configure_modifiers([]string{"KEY_META"}, []string{"KEY_RIGHTMETA"}); err == nil { + t.Fatal("explicit conflict accepted") + } + if !r.scriptKeys[KeyMeta] || r.reader_modifier(KeyMeta, false) { + t.Fatal("failed reconfiguration changed keys") + } + if err := r.Configure_modifiers([]string{"KEY_META"}, []string{"KEY_COMPOSE"}); err != nil { + t.Fatal("could not move script key to reader", err) + } + if r.scriptKeys[KeyMeta] || !r.reader_modifier(KeyRightMeta, false) { + t.Fatal("old assignment survived") + } +} + +func TestConfigurableScriptModifiersAndNumLock(t *testing.T) { + for _, key := range []uint16{KeyInsert, KeyKP0, KeyCapsLock, KeyMeta, KeyRightMeta} { + r := desktop_router(t) + if err := r.Configure_modifiers([]string{"KEY_CAPSLOCK"}, []string{Key_name(key)}); err != nil { + if key != KeyCapsLock { + t.Fatal(err) + } + if err := r.Configure_modifiers([]string{"KEY_INSERT"}, []string{Key_name(key)}); err != nil { + t.Fatal(err) + } + } + if err := r.Configure_scripts([]string{Key_name(key)}, []ScriptBinding{{ID: "script:example", Keys: []string{"KEY_SCRIPT", "KEY_U"}}}); err != nil { + t.Fatal(err) + } + if got := r.Key(0, key, 1, false, false); got.Forward || got.Command != "" { + t.Fatal(key, got) + } + if got := r.Key(0, 22, 1, false, false); got.Command != "script:example" || got.Forward { + t.Fatal(key, got) + } + r.Key(0, key, 0, false, false) + if got := r.Key(0, 22, 0, false, false); got.Forward { + t.Fatal(key, got) + } + if key == KeyKP0 { + if got := r.Key(0, key, 1, true, false); !got.Forward || !got.Typing { + t.Fatal("numeric keypad zero consumed", got) + } + if got := r.Key(0, 22, 1, true, false); !got.Forward || got.Command != "" || !got.Typing { + t.Fatal("NumLock zero became modifier", got) + } + } + } +} + +func TestCapsLockScriptModifierKeepsShiftToggle(t *testing.T) { + r := desktop_router(t) + if err := r.Configure_modifiers([]string{"KEY_INSERT"}, []string{"KEY_CAPSLOCK"}); err != nil { + t.Fatal(err) + } + r.Key(0, KeyShift, 1, false, false) + if got := r.Key(0, KeyCapsLock, 1, false, false); got.Forward { + t.Fatal(got) + } + if got := r.Key(0, KeyCapsLock, 0, false, false); !got.ToggleCapsLock { + t.Fatal(got) + } + r.Key(0, KeyShift, 0, false, false) + r.Key(0, KeyCapsLock, 1, false, false) + if got := r.Key(0, 22, 1, false, false); got.Command != command.Unbound { + t.Fatal(got) + } + if got := r.Key(0, KeyCapsLock, 0, false, false); got.ToggleCapsLock { + t.Fatal("script chord toggled capitalization", got) + } +} diff --git a/internal/input/multi_press_test.go b/internal/input/multi_press_test.go index 7e136ca6..0312d3cf 100644 --- a/internal/input/multi_press_test.go +++ b/internal/input/multi_press_test.go @@ -24,11 +24,11 @@ func TestTriplePressSequences(t *testing.T) { primary = 37 target = command.CurrentWord } - for index := range bindings { - if bindings[index].Command == target { - bindings[index].Command = command.CurrentWord - bindings[index].DoubleCommand = command.CurrentChar - bindings[index].TripleCommand = command.CurrentLine + for index := range bindings.Bindings { + if bindings.Bindings[index].Command == target { + bindings.Bindings[index].Command = command.CurrentWord + bindings.Bindings[index].DoubleCommand = command.CurrentChar + bindings.Bindings[index].TripleCommand = command.CurrentLine } } router, err := New_router(bindings) @@ -77,7 +77,7 @@ func TestTriplePressSequences(t *testing.T) { func TestTriplePressBoundariesAndInvalidation(t *testing.T) { for _, scenario := range []string{"inside", "at boundary", "outside", "backward clock", "other chord", "typing", "numlock", "reset", "capture", "drop", "repair", "unchanged repair", "other device", "pass next", "reconfigure"} { t.Run(scenario, func(t *testing.T) { - router, err := New_router([]Binding{{Name: "Review", Keys: []string{"KP5"}, Command: command.CurrentWord, DoubleCommand: command.CurrentChar, TripleCommand: command.CurrentLine}, {Name: "Other", Keys: []string{"FENRIR", "KP5"}, Command: command.Time, DoubleCommand: command.Date}}) + router, err := New_router(Layout{Bindings: []Binding{{Name: "Review", Keys: []string{"KP5"}, Command: command.CurrentWord, DoubleCommand: command.CurrentChar, TripleCommand: command.CurrentLine}, {Name: "Other", Keys: []string{"FENRIR", "KP5"}, Command: command.Time, DoubleCommand: command.Date}}}) if err != nil { t.Fatal(err) } @@ -177,7 +177,7 @@ func TestRetainedFeatureKeyNames(t *testing.T) { if bare { keys = keys[1:] } - _, err := New_router([]Binding{{Name: "Retained key", Keys: keys, Command: command.Help}}) + _, err := New_router(Layout{Bindings: []Binding{{Name: "Retained key", Keys: keys, Command: command.Help}}}) wantValid := !bare || key == "KPMINUS" || key == "KPSLASH" if (err == nil) != wantValid { t.Fatal(key, bare, err) diff --git a/internal/input/regions_test.go b/internal/input/regions_test.go index 064f35d2..8f15d9de 100644 --- a/internal/input/regions_test.go +++ b/internal/input/regions_test.go @@ -17,7 +17,7 @@ func TestRegionChordsInBothLayoutsConsumeRepeatsAndEitherReleaseOrder(t *testing if err != nil { t.Fatal(err) } - for _, binding := range bindings { + for _, binding := range bindings.Bindings { if binding.Command != command.SetWindow && binding.Command != command.SetLiveRegion && binding.Command != command.ListReadingRegions && binding.Command != command.ClearLiveRegions && binding.Command != command.ClearReadingRegions && !strings.HasPrefix(string(binding.Command), "read_live_region_") && !strings.HasPrefix(string(binding.Command), "clear_live_region_") { continue diff --git a/internal/input/router.go b/internal/input/router.go index 26f39136..f4eb9fe0 100644 --- a/internal/input/router.go +++ b/internal/input/router.go @@ -37,6 +37,7 @@ const ( KeyRightShift = 54 KeyAlt = 56 KeySpace = 57 + KeyCapsLock = 58 KeyF4 = 62 KeyKP7 = 71 KeyKP8 = 72 @@ -66,34 +67,37 @@ 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 - Deletion bool // forwarded Backspace/Delete, including repeats - Typing bool // forwarded text key or repeat; the screen supplies its actual text - WordBoundary bool // a forwarded Space, rather than cursor navigation - Command command.ID - UI *interaction.Key - Learn *LearnKey // captured physical press and its inactive command + Code uint16 // physical key code, for forwarded application press/repeat hooks + ToggleCapsLock bool // completed Shift+Caps Lock gesture + Forward bool + Interrupt bool + Keypress bool // a fresh physical press, excluding repeats and state repair + Deletion bool // forwarded Backspace/Delete, including repeats + Typing bool // forwarded text key or repeat; the screen supplies its actual text + WordBoundary bool // a forwarded Space, rather than cursor navigation + Command command.ID + UI *interaction.Key + Learn *LearnKey // captured physical press and its inactive command } type Router struct { - keys map[int]map[uint16]heldKey - passArmed bool - passing bool - captured bool - bindings map[chord]command.ID - doubleBindings map[chord]command.ID - tripleBindings map[chord]command.ID - multiPressTimeout time.Duration - pressCount int - lastChord chord - lastDevice int - lastPress time.Time - now func() time.Time - scriptKeys map[uint16]bool - scriptBindings map[string]command.ID + readerKeys map[uint16]bool + capsShift, capsUsed bool + keys map[int]map[uint16]heldKey + passArmed bool + passing bool + captured bool + bindings map[chord]command.ID + doubleBindings map[chord]command.ID + tripleBindings map[chord]command.ID + multiPressTimeout time.Duration + pressCount int + lastChord chord + lastDevice int + lastPress time.Time + now func() time.Time + scriptKeys map[uint16]bool + scriptBindings map[string]command.ID } type heldKey struct { @@ -104,6 +108,7 @@ type heldKey struct { func (r *Router) Reset() { r.lastPress = time.Time{} + r.capsShift, r.capsUsed = false, false r.keys = nil r.passArmed = false r.passing = false @@ -111,6 +116,7 @@ func (r *Router) Reset() { } func (r *Router) Drop(device int) { + r.capsUsed = true r.lastPress = time.Time{} delete(r.keys, device) if r.empty() { @@ -161,6 +167,7 @@ func (r *Router) key(device int, key uint16, value int32, numLock, syncEvent, re _, held := r.keys[device][key] if value == 0 && held || value == 1 && !held { r.lastPress = time.Time{} + r.capsUsed = true } } if r.keys == nil { @@ -176,7 +183,11 @@ func (r *Router) key(device int, key uint16, value int32, numLock, syncEvent, re if r.empty() { r.passing = false } - return Decision{Forward: exists && state.forwarded} + toggle := key == KeyCapsLock && exists && !state.forwarded && r.Caps_modifier() && r.capsShift && !r.capsUsed && !syncEvent && !r.captured && !r.passing + if key == KeyCapsLock { + r.capsShift, r.capsUsed = false, false + } + return Decision{Forward: exists && state.forwarded, ToggleCapsLock: toggle} } if value != 1 && value != 2 { return Decision{} @@ -204,13 +215,34 @@ func (r *Router) key(device int, key uint16, value int32, numLock, syncEvent, re if value == 2 { return Decision{} } + if !syncEvent && r.Caps_modifier() { + switch key { + case KeyCapsLock: + r.capsShift, r.capsUsed = r.down(KeyShift) || r.down(KeyRightShift), false + for _, held := range r.keys { + for code := range held { + if code != KeyShift && code != KeyRightShift { + r.capsUsed = true + } + } + } + case KeyShift, KeyRightShift: + if r.down(KeyCapsLock) { + r.capsShift = true + } + default: + if r.down(KeyCapsLock) { + r.capsUsed = true + } + } + } if r.captured { var learn *LearnKey if !syncEvent { learn = r.learn_key(device, key, numLock) } held[key] = heldKey{ui: true} - if syncEvent || pure_modifier(key) { + if syncEvent || pure_modifier(key) || r.reader_modifier(key, numLock) { return Decision{Learn: learn, Keypress: !syncEvent, Interrupt: !syncEvent && (key == KeyCtrl || key == KeyRightCtrl)} } decision := r.ui_key(key) @@ -229,10 +261,10 @@ func (r *Router) key(device int, key uint16, value int32, numLock, syncEvent, re commandID = r.binding(key, numLock) } } - if !syncEvent && !pure_modifier(key) && !reader_modifier(key, numLock) { + if !syncEvent && !pure_modifier(key) && !r.reader_modifier(key, numLock) { commandID = r.multi_press(device, r.binding_chord(key, numLock), commandID, scriptChord) } - forwarded := commandID == "" && !scriptChord && !(reader_modifier(key, numLock) && !r.passing) + forwarded := commandID == "" && !scriptChord && !(r.reader_modifier(key, numLock) && !r.passing) held[key] = heldKey{forwarded: forwarded, commandID: commandID} if syncEvent { return Decision{Forward: forwarded} @@ -243,11 +275,11 @@ func (r *Router) key(device int, key uint16, value int32, numLock, syncEvent, re func (r *Router) typing_key(key uint16, numLock bool) bool { for scriptKey := range r.scriptKeys { - if r.down(scriptKey) { + if r.script_modifier(scriptKey, numLock) && r.down(scriptKey) { return false } } - if r.down(KeyCtrl) || r.down(KeyRightCtrl) || r.down(KeyAlt) || r.down(KeyInsert) || r.down(KeyMeta) || r.down(KeyRightMeta) { + if r.down(KeyCtrl) || r.down(KeyRightCtrl) || r.down(KeyAlt) || r.reader_down(numLock) || r.down(KeyMeta) || r.down(KeyRightMeta) { return false } // Physical text-key positions, independent of keyboard layout. AltGr is @@ -284,8 +316,34 @@ func (r *Router) empty() bool { return true } -func reader_modifier(key uint16, numLock bool) bool { - return key == KeyInsert || key == KeyMeta || key == KeyRightMeta || key == KeyKP0 && !numLock +func (r *Router) reader_modifier(key uint16, numLock bool) bool { + if r.readerKeys == nil { + return key == KeyInsert || key == KeyMeta || key == KeyRightMeta || key == KeyKP0 && !numLock + } + return r.readerKeys[key] && (key != KeyKP0 || !numLock) +} + +// Caps_modifier reports whether Caps Lock is reserved for reader or script chords. +func (r *Router) Caps_modifier() bool { return r.readerKeys[KeyCapsLock] || r.scriptKeys[KeyCapsLock] } + +// Reader_keys supplies the X11 adapter with the physical keys to intercept. +func (r *Router) Reader_keys() []uint16 { + var keys []uint16 + for _, key := range []uint16{KeyInsert, KeyMeta, KeyRightMeta, KeyCapsLock, KeyKP0} { + if r.reader_modifier(key, false) { + keys = append(keys, key) + } + } + return keys +} + +func (r *Router) reader_down(numLock bool) bool { + for _, key := range r.Reader_keys() { + if r.reader_modifier(key, numLock) && r.down(key) { + return true + } + } + return false } func pure_modifier(key uint16) bool { @@ -293,7 +351,7 @@ func pure_modifier(key uint16) bool { } func (r *Router) binding(key uint16, numLock bool) command.ID { - if reader_modifier(key, numLock) || pure_modifier(key) { + if r.reader_modifier(key, numLock) || pure_modifier(key) { return "" } combo := r.binding_chord(key, numLock) @@ -311,7 +369,7 @@ func (r *Router) binding(key uint16, numLock bool) command.ID { func (r *Router) binding_chord(key uint16, numLock bool) chord { var mods modifiers - if r.down(KeyInsert) || r.down(KeyMeta) || r.down(KeyRightMeta) || !numLock && r.down(KeyKP0) { + if r.reader_down(numLock) { mods |= fenrirModifier } if r.down(KeyCtrl) || r.down(KeyRightCtrl) { diff --git a/internal/input/script_keys_linux.go b/internal/input/script_keys_linux.go index 2173ab03..4cdaa904 100644 --- a/internal/input/script_keys_linux.go +++ b/internal/input/script_keys_linux.go @@ -79,25 +79,16 @@ func Script_chord(keys []string) string { } func (r *Router) Configure_scripts(keys []string, bindings []ScriptBinding) error { - r.scriptKeys = make(map[uint16]bool) - r.scriptBindings = make(map[string]command.ID) - for _, name := range keys { - code, ok := Key_code(name) - if !ok { - return fmt.Errorf("invalid script key %q", name) - } - r.scriptKeys[code] = true - switch strings.ToUpper(name) { - case "KEY_CTRL": - r.scriptKeys[KeyRightCtrl] = true - case "KEY_SHIFT": - r.scriptKeys[KeyRightShift] = true - case "KEY_ALT": - r.scriptKeys[KeyRightAlt] = true - case "KEY_META": - r.scriptKeys[KeyRightMeta] = true + scriptKeys, err := Modifier_codes(keys) + if err != nil { + return err + } + for code := range scriptKeys { + if r.reader_modifier(code, false) { + return fmt.Errorf("%s cannot be both a Fenrir key and a script key", Key_name(code)) } } + scriptBindings := make(map[string]command.ID) for _, binding := range bindings { if binding.ID == "" { return fmt.Errorf("script binding has no command") @@ -108,11 +99,12 @@ func (r *Router) Configure_scripts(keys []string, bindings []ScriptBinding) erro } } chord := Script_chord(binding.Keys) - if _, exists := r.scriptBindings[chord]; exists { + if _, exists := scriptBindings[chord]; exists { return fmt.Errorf("duplicate script shortcut %s", chord) } - r.scriptBindings[chord] = binding.ID + scriptBindings[chord] = binding.ID } + r.scriptKeys, r.scriptBindings = scriptKeys, scriptBindings return nil } @@ -138,10 +130,10 @@ func (r *Router) script_name(key uint16, numLock bool) string { case KeyRightMeta: key = KeyMeta } - if r.scriptKeys[physical] { + if r.script_modifier(physical, numLock) { return "KEY_SCRIPT" } - if reader_modifier(key, numLock) { + if r.reader_modifier(physical, numLock) { return "KEY_FENRIR" } switch key { @@ -160,10 +152,10 @@ func (r *Router) script_name(key uint16, numLock bool) string { } func (r *Router) script_binding(key uint16, numLock bool) (command.ID, bool) { - active := r.scriptKeys[key] + active := r.script_modifier(key, numLock) if !active { for scriptKey := range r.scriptKeys { - if r.down(scriptKey) { + if r.script_modifier(scriptKey, numLock) && r.down(scriptKey) { active = true break } diff --git a/internal/input/script_keys_test.go b/internal/input/script_keys_test.go index 9347da15..81a6c13b 100644 --- a/internal/input/script_keys_test.go +++ b/internal/input/script_keys_test.go @@ -54,7 +54,7 @@ func TestScriptChordsModifiersRepeatsAndReleaseOrders(t *testing.T) { func new_script_router(t *testing.T, keys []string, bindings []ScriptBinding) Router { t.Helper() - router, err := New_router([]Binding{{Name: "Quit", Keys: []string{"FENRIR", "Q"}, Command: command.Quit}}) + router, err := New_router(Layout{Bindings: []Binding{{Name: "Quit", Keys: []string{"FENRIR", "Q"}, Command: command.Quit}}}) if err != nil { t.Fatal(err) } diff --git a/internal/input/ui.go b/internal/input/ui.go index e5b08903..78451a85 100644 --- a/internal/input/ui.go +++ b/internal/input/ui.go @@ -28,6 +28,7 @@ import ( func (r *Router) Capture(enabled bool) { r.lastPress = time.Time{} r.captured = enabled + r.capsUsed = true r.passArmed, r.passing = false, false for _, held := range r.keys { for code, state := range held { diff --git a/internal/input/ui_test.go b/internal/input/ui_test.go index 35df9b8b..23fc1c4c 100644 --- a/internal/input/ui_test.go +++ b/internal/input/ui_test.go @@ -104,7 +104,7 @@ func TestCapturedModifiersRepeatAndSync(t *testing.T) { func TestDesktopTOMLValidated(t *testing.T) { bindings, err := Decode_bindings(desktop_data(t)) - if err != nil || len(bindings) == 0 { + if err != nil || len(bindings.Bindings) == 0 { t.Fatal(bindings, err) } for _, replacement := range []struct{ old, new string }{ diff --git a/internal/preferences/modifiers.go b/internal/preferences/modifiers.go new file mode 100644 index 00000000..aa5d5ef1 --- /dev/null +++ b/internal/preferences/modifiers.go @@ -0,0 +1,114 @@ +// Copyright (c) 2026 Storm Dragon +// SPDX-License-Identifier: GPL-3.0-or-later + +package preferences + +import ( + "fmt" + "path/filepath" + "slices" + + "git.stormux.org/storm/fenrir/internal/config" + "git.stormux.org/storm/fenrir/internal/input" + "git.stormux.org/storm/fenrir/internal/interaction" +) + +type modifierOption struct{ name, Label string } + +var modifierOptions = []modifierOption{ + {name: "KEY_KP0", Label: "Keypad Insert"}, + {name: "KEY_INSERT", Label: "Insert"}, + {name: "KEY_CAPSLOCK", Label: "Caps Lock"}, + {name: "KEY_META", Label: "Super"}, +} + +func reader_names(settings config.Settings) []string { + if settings.Keyboard.FenrirKeys != nil { + return settings.Keyboard.FenrirKeys + } + // Running applications supply the resolved layout's keys at startup. + // Standalone forms use the built-in layout defaults. + var names []string + switch filepath.Base(settings.Keyboard.Layout) { + case "laptop", "laptop.toml": + names = []string{"KEY_KP0", "KEY_INSERT", "KEY_CAPSLOCK"} + default: + names = []string{"KEY_KP0", "KEY_INSERT", "KEY_META"} + } + available, err := input.Available_fenrir_keys(names, settings.General.ScriptKeys) + if err != nil { + return []string{} + } + return available +} + +func modifier_checked(names []string, name string) bool { + keys, _ := input.Modifier_codes(names) + option, _ := input.Modifier_codes([]string{name}) + for key := range option { + if keys[key] { + return true + } + } + return false +} + +func modifier_controls(settings config.Settings) []interaction.Control { + var controls []interaction.Control + for _, role := range []struct { + id, Label string + names []string + }{ + {id: "fenrir_key.", Label: "Fenrir keys", names: reader_names(settings)}, + {id: "script_key.", Label: "Script keys", names: settings.General.ScriptKeys}, + } { + options := slices.Clone(modifierOptions) + if role.id == "script_key." { + options = append(options, modifierOption{name: "KEY_COMPOSE", Label: "Compose"}) + } + for _, option := range options { + controls = append(controls, interaction.Control{ID: role.id + option.name, Label: option.Label, Group: role.Label, Kind: interaction.CheckBox, Checked: modifier_checked(role.names, option.name)}) + } + } + return controls +} + +func selected_modifiers(values map[string]interaction.Control, prefix string, names []string, options []modifierOption) ([]string, bool, error) { + selected := slices.Clone(names) + changed := false + for _, option := range options { + control, ok := values["keyboard."+prefix+option.name] + if !ok { + return nil, false, fmt.Errorf("missing modifier checkbox %s", option.name) + } + if control.Checked == modifier_checked(names, option.name) { + continue + } + changed = true + if control.Checked { + selected = append(selected, option.name) + continue + } + selected = slices.DeleteFunc(selected, func(name string) bool { return modifier_checked([]string{name}, option.name) }) + } + return selected, changed, nil +} + +func modifier_settings(values map[string]interaction.Control, base *config.Settings) error { + reader, changed, err := selected_modifiers(values, "fenrir_key.", reader_names(*base), modifierOptions) + if err != nil { + return err + } + scripts, _, err := selected_modifiers(values, "script_key.", base.General.ScriptKeys, append(slices.Clone(modifierOptions), modifierOption{name: "KEY_COMPOSE", Label: "Compose"})) + if err != nil { + return err + } + if err := input.Validate_modifier_keys(reader, scripts); err != nil { + return err + } + if changed { + base.Keyboard.FenrirKeys = reader + } + base.General.ScriptKeys = scripts + return nil +} diff --git a/internal/preferences/modifiers_test.go b/internal/preferences/modifiers_test.go new file mode 100644 index 00000000..033726db --- /dev/null +++ b/internal/preferences/modifiers_test.go @@ -0,0 +1,110 @@ +// Copyright (c) 2026 Storm Dragon +// SPDX-License-Identifier: GPL-3.0-or-later + +package preferences + +import ( + "path/filepath" + "reflect" + "testing" + + "git.stormux.org/storm/fenrir/internal/config" + "git.stormux.org/storm/fenrir/internal/interaction" +) + +func focus_modifier(t *testing.T, s *interaction.Session, id string) { + t.Helper() + for range 40 { + if control, ok := s.Focused_control(); ok && control.ID == id { + return + } + s.Handle(interaction.Key{Name: "tab"}) + } + t.Fatal("could not focus", id) +} + +func TestModifierCheckboxesRejectConflictAndSaveSelections(t *testing.T) { + for _, key := range []string{"KEY_KP0", "KEY_INSERT", "KEY_CAPSLOCK", "KEY_META"} { + base := config.Defaults() + base.Keyboard.Devices = []string{} + base.Keyboard.FenrirKeys = []string{key} + base.General.ScriptKeys = []string{"KEY_COMPOSE", "KEY_F9"} + session, err := New_session(base, []string{"keyboard"}) + if err != nil { + t.Fatal(err) + } + for _, role := range []string{"fenrir_key.", "script_key."} { + control := session.Values()["keyboard."+role+key] + if control.Kind != interaction.CheckBox || control.Checked != (role == "fenrir_key.") { + t.Fatal(control) + } + } + focus_modifier(t, session, "script_key."+key) + session.Handle(interaction.Key{Name: "space"}) + if _, err := Settings(session, base); err == nil { + t.Fatal("overlapping checkbox roles saved", key) + } + focus_modifier(t, session, "fenrir_key."+key) + session.Handle(interaction.Key{Name: "space"}) + if _, err := Settings(session, base); err == nil { + t.Fatal("all reader keys unchecked accepted") + } + replacement := "KEY_INSERT" + if key == replacement { + replacement = "KEY_CAPSLOCK" + } + focus_modifier(t, session, "fenrir_key."+replacement) + session.Handle(interaction.Key{Name: "space"}) + draft, err := Settings(session, base) + if err != nil || !modifier_checked(draft.General.ScriptKeys, key) || !modifier_checked(draft.Keyboard.FenrirKeys, replacement) || modifier_checked(draft.Keyboard.FenrirKeys, key) { + t.Fatal(draft.Keyboard, draft.General, err) + } + if !reflect.DeepEqual(base.Keyboard.FenrirKeys, []string{key}) || len(base.General.ScriptKeys) != 2 { + t.Fatal("draft changed original") + } + path := filepath.Join(t.TempDir(), "settings.toml") + doc, err := config.Open_document(path, base) + if err != nil { + t.Fatal(err) + } + if err := doc.Save(draft); err != nil { + t.Fatal(err) + } + saved, _, err := config.Load(path, nil) + if err != nil || !reflect.DeepEqual(saved.Keyboard.FenrirKeys, draft.Keyboard.FenrirKeys) || !reflect.DeepEqual(saved.General.ScriptKeys, draft.General.ScriptKeys) { + t.Fatal(saved.Keyboard, saved.General, err) + } + if !modifier_checked(saved.General.ScriptKeys, "KEY_F9") { + t.Fatal("custom script key lost") + } + } +} + +func TestModifierCheckboxesPreserveSideSpecificSuperAndDefaults(t *testing.T) { + base := config.Defaults() + base.Keyboard.Devices = []string{} + base.Keyboard.FenrirKeys = []string{"KEY_RIGHTMETA"} + base.General.ScriptKeys = []string{"KEY_LEFTMETA"} + session, err := New_session(base, []string{"keyboard"}) + if err != nil { + t.Fatal(err) + } + unchanged, err := Settings(session, base) + if err != nil || !reflect.DeepEqual(unchanged, base) { + t.Fatal(unchanged, err) + } + base = config.Defaults() + base.Keyboard.Devices = []string{} + base.General.ScriptKeys = []string{"KEY_META", "KEY_COMPOSE"} + session, err = New_session(base, []string{"keyboard"}) + if err != nil { + t.Fatal(err) + } + if session.Values()["keyboard.fenrir_key.KEY_META"].Checked || !session.Values()["keyboard.script_key.KEY_META"].Checked { + t.Fatal("defaults claimed existing script key") + } + unchanged, err = Settings(session, base) + if err != nil || !reflect.DeepEqual(unchanged, base) { + t.Fatal(unchanged, err) + } +} diff --git a/internal/preferences/settings.go b/internal/preferences/settings.go index b34bc970..2e805700 100644 --- a/internal/preferences/settings.go +++ b/internal/preferences/settings.go @@ -78,6 +78,9 @@ func new_session(settings config.Settings, order []string, title string, applica return err } } + if section.ID == "keyboard" && !application { + tab.Controls = append(tab.Controls, modifier_controls(settings)...) + } if section.ID == "presentation.speech" { driver, _ := speech.Find_driver(settings.Presentation.Speech.Driver) tab.Controls = append(tab.Controls, interaction.Control{ID: "speech.test", Label: "Test synthesizer", Group: "Speech", Kind: interaction.Button, Action: "test-synthesizer", Hidden: !driver.Hardware}) @@ -219,6 +222,11 @@ func settings_values(session *interaction.Session, base config.Settings, applica return config.Settings{}, err } } + if !application { + if err := modifier_settings(values, &base); err != nil { + return config.Settings{}, err + } + } // Inactive Speech Dispatcher choices stay in the form's draft for switching // back, but cannot be sent to or saved as selections for a hardware driver. for _, definition := range config.Definitions() { diff --git a/internal/preferences/settings_test.go b/internal/preferences/settings_test.go index 6d107a0f..df458263 100644 --- a/internal/preferences/settings_test.go +++ b/internal/preferences/settings_test.go @@ -38,7 +38,7 @@ func TestSettingsFormCoversSchemaAndReturnsIsolatedDraft(t *testing.T) { t.Fatal(got) } values := session.Values() - if len(values) != 55 { + if len(values) != 64 { t.Fatal("missing settings or buttons", len(values)) } unchanged, err := Settings(session, base) @@ -147,24 +147,48 @@ func TestCapitalCheckboxesRoundTripIndependently(t *testing.T) { } } -func TestInvalidSettingsEntriesDoNotSubmit(t *testing.T) { - for _, field := range []string{"devices", "layout"} { - session, err := New_session(config.Defaults(), []string{"keyboard"}) +func TestInvalidDeviceEntryDoesNotSubmit(t *testing.T) { + session, err := New_session(config.Defaults(), []string{"keyboard"}) + if err != nil { + t.Fatal(err) + } + session.Handle(interaction.Key{Name: "tab"}) + session.Handle(interaction.Key{Ctrl: true, Text: "a"}) + session.Handle(interaction.Key{Text: "invalid value"}) + if _, err := Settings(session, config.Defaults()); err == nil { + t.Fatal("invalid devices accepted") + } +} + +func TestKeyboardLayoutComboBox(t *testing.T) { + for _, saved := range []string{"desktop", "laptop", "/tmp/custom.toml"} { + base := config.Defaults() + base.Keyboard.Layout = saved + session, err := New_session(base, []string{"keyboard"}) if err != nil { t.Fatal(err) } + control := session.Values()["keyboard.layout"] + if control.Kind != interaction.ComboBox || control.Choices[0].ID != "desktop" || control.Choices[1].ID != "laptop" || control.Choices[control.Selected].ID != saved { + t.Fatal(control) + } + session.Handle(interaction.Key{Name: "tab"}) session.Handle(interaction.Key{Name: "tab"}) - if field == "layout" { - session.Handle(interaction.Key{Name: "tab"}) - } session.Handle(interaction.Key{Ctrl: true, Text: "a"}) - value := "invalid value" - if field == "layout" { - value = "old.conf" + session.Handle(interaction.Key{Text: "old.conf"}) + draft, err := Settings(session, base) + if err != nil || draft.Keyboard.Layout != saved { + t.Fatal(draft.Keyboard.Layout, err) } - session.Handle(interaction.Key{Text: value}) - if _, err := Settings(session, config.Defaults()); err == nil { - t.Fatal("invalid field accepted", field) + if saved == "desktop" { + session.Handle(interaction.Key{Name: "down"}) + draft, err = Settings(session, base) + if err != nil || draft.Keyboard.Layout != "laptop" { + t.Fatal(draft.Keyboard.Layout, err) + } + if base.Keyboard.Layout != "desktop" { + t.Fatal("draft changed original settings") + } } } } diff --git a/internal/terminal/keyboard_linux.go b/internal/terminal/keyboard_linux.go index ce36ec32..7a0bb8aa 100644 --- a/internal/terminal/keyboard_linux.go +++ b/internal/terminal/keyboard_linux.go @@ -156,6 +156,30 @@ func (k *Keyboard) Poll(active, numLock bool, handle func(input.Decision)) error } } decision := s.router.Key(0, key, value, numLock, false) + if key == input.KeyCapsLock && s.router.Caps_modifier() { + if value == 1 { + s.capsConsumed = !decision.Forward + s.capsRestore = uint32(event.state)&2 != 0 + } + if s.capsConsumed && value != 2 { + // XKB locks on press and can unlock on release, even for grabbed keys. + enabled := C.int(0) + if s.capsRestore { + enabled = 1 + } + if err := native_error(C.terminal_x_caps_lock(s.x, enabled)); err != nil { + return err + } + } + if value == 0 { + s.capsConsumed = false + } + } + if decision.ToggleCapsLock { + if err := native_error(C.terminal_x_toggle_caps(s.x)); err != nil { + return err + } + } // Acquire before dispatch: a failed capture must not execute a command // or open a form from a partial, stale chord. if event.value == 1 && !decision.Forward && !s.grabbed { diff --git a/internal/terminal/native.c b/internal/terminal/native.c index 9260caaf..ee7b962a 100644 --- a/internal/terminal/native.c +++ b/internal/terminal/native.c @@ -218,7 +218,7 @@ static void install_grabs(TerminalX *x) { x->numlock |= 1u << mask; XFreeModifiermap(modifiers); } - KeySym keys[] = {XK_Insert, XK_Super_L, XK_Super_R, XK_Meta_L, XK_Meta_R, + KeySym keys[] = {XK_Insert, XK_KP_Insert, XK_KP_End, XK_KP_Down, XK_KP_Page_Down, XK_KP_Left, XK_KP_Begin, XK_KP_Right, XK_KP_Home, XK_KP_Up, XK_KP_Page_Up, XK_KP_Delete, XK_KP_Enter, XK_KP_Add, XK_KP_Subtract, XK_KP_Divide, @@ -243,6 +243,16 @@ static void install_grabs(TerminalX *x) { } } } +int terminal_x_caps_lock(TerminalX *x, int enabled) { + if (!XkbLockModifiers(x->display, XkbUseCoreKbd, LockMask, enabled ? LockMask : 0)) return -EIO; + XSync(x->display, False); + return 0; +} +int terminal_x_toggle_caps(TerminalX *x) { + XkbStateRec state; + if (XkbGetState(x->display, XkbUseCoreKbd, &state) != Success) return -EIO; + return terminal_x_caps_lock(x, !(state.locked_mods & LockMask)); +} int terminal_x_script_keys(TerminalX *x, const int *keys, int count) { if (count < 0 || count > KEY_MAX+1) return -EINVAL; XErrorHandler previous = begin_trap(x->display); @@ -443,6 +453,7 @@ int terminal_key_code(unsigned long symbol, int keycode) { case XK_Page_Up: return KEY_PAGEUP; case XK_Page_Down: return KEY_PAGEDOWN; case XK_Insert: return KEY_INSERT; + case XK_Caps_Lock: return KEY_CAPSLOCK; case XK_Multi_key: return KEY_COMPOSE; case XK_Menu: return KEY_MENU; case XK_Super_L: case XK_Meta_L: return KEY_LEFTMETA; diff --git a/internal/terminal/native.h b/internal/terminal/native.h index e95b763e..ac6273e8 100644 --- a/internal/terminal/native.h +++ b/internal/terminal/native.h @@ -26,6 +26,8 @@ typedef struct { uint32_t foreground, background, style; } TerminalAttribute; void terminal_cells(VTerm *vt, uint32_t *cells, TerminalAttribute *attributes, int count, int *x, int *y); int terminal_key(VTerm *vt, const TerminalEvent *event, char *bytes, int size); TerminalX *terminal_x_open(unsigned long window, int *error); +int terminal_x_caps_lock(TerminalX *x, int enabled); +int terminal_x_toggle_caps(TerminalX *x); int terminal_x_script_keys(TerminalX *x, const int *keys, int count); void terminal_x_close(TerminalX *x); int terminal_x_fd(TerminalX *x); diff --git a/internal/terminal/session_linux.go b/internal/terminal/session_linux.go index 2b25a3db..ce0facfd 100644 --- a/internal/terminal/session_linux.go +++ b/internal/terminal/session_linux.go @@ -76,7 +76,10 @@ type Session struct { exited chan struct{} closed bool eof bool - lastReplay struct { + + capsRestore, capsConsumed bool + + lastReplay struct { code int time uint64 } @@ -132,7 +135,7 @@ func open_session(inputFile, outputFile *os.File, window uint64, child *exec.Cmd return nil, fmt.Errorf("open X11 terminal or acquire its keys (error %d); check DISPLAY, WINDOWID and competing grabs", xError) } var scriptKeys []C.int - for _, key := range router.Script_keys() { + for _, key := range append(router.Reader_keys(), router.Script_keys()...) { scriptKeys = append(scriptKeys, C.int(key)) } if len(scriptKeys) > 0 { diff --git a/tests/native_terminal.py b/tests/native_terminal.py index e538d8f3..d9babdfb 100644 --- a/tests/native_terminal.py +++ b/tests/native_terminal.py @@ -327,6 +327,8 @@ def main(): 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", "SHIFT", "A"]\ncommand = "attribute_cursor"\n') + multiPressBindings) + if os.environ.get("FENRIR_CAPS_TEST_ONLY") == "1" or os.environ.get("FENRIR_MODIFIERS_TEST_ONLY") == "1": + (root / "mode").write_text("laptop") server = SpeechServer(root / "speech.sock") (root / "settings.toml").write_text( 'version=3\n[presentation.sound]\nenabled=false\n' @@ -338,6 +340,10 @@ def main(): ) (root / "scripts").mkdir() (root / "home").mkdir() + if os.environ.get("FENRIR_MODIFIERS_TEST_ONLY") == "1": + modifierScript = root / "scripts/modifiertest__-__KEY_Z.sh" + modifierScript.write_text("#!/bin/sh\necho script modifier works\n") + modifierScript.chmod(0o700) controller = root / "scripts/musicplayer__-__KEY_SHIFT__+__KEY_U.sh" controller.write_text('#!/bin/sh\necho "$1"\n') controller.chmod(0o700) @@ -408,6 +414,111 @@ def main(): xdo("windowfocus", "--sync", window) spoken("prompt") + if os.environ.get("FENRIR_MODIFIERS_TEST_ONLY") == "1": + originalSettings = (root / "settings.toml").read_bytes() + start = len(server.events) + key("Caps_Lock+f", "Right") + spoken("Keyboard tab", start) + for expected in ["Devices entry", "Layout combo box", "Character echo", "Word echo", "Echo on CapsLock", "Fenrir keys. Keypad Insert checked", "Insert checked", "Caps Lock checked"]: + start = len(server.events) + key("Tab") + spoken(expected, start) + key("space") # Caps Lock will become a script key on next launch. + start = len(server.events) + key("Tab", "space") # Fenrir Super on. + spoken("Super checked", start) + for expected in ["Script keys. Keypad Insert unchecked", "Insert unchecked", "Caps Lock unchecked"]: + start = len(server.events) + key("Tab") + spoken(expected, start) + key("space", "Tab", "space") # Script Caps Lock and Super on. + start = len(server.events) + key("Tab", "Tab", "Return") + spoken("cannot be both a Fenrir key and a script key", start) + assert (root / "settings.toml").read_bytes() == originalSettings + key("Shift_L+Tab", "Shift_L+Tab", "space") # Remove Script Super. + save_menu() + saved = tomllib.loads((root / "settings.toml").read_text()) + readerKeys = saved["keyboard"]["fenrir_keys"] + scriptKeys = saved["general"]["script_keys"] + assert "KEY_CAPSLOCK" not in readerKeys and "KEY_META" in readerKeys, saved + assert "KEY_CAPSLOCK" in scriptKeys and "KEY_META" not in scriptKeys, saved + start = len(server.events) + key("Caps_Lock+i") # Saving must not change the running modifiers. + spoken("prompt", start) + assert recorded() == b"", recorded() + key("Caps_Lock+q") + wait_for(lambda: (root / "finished").exists(), "modifier first session quit") + assert json.loads((root / "finished").read_text())["status"] == 0 + for name in ["started", "finished", "size", "input", "shell-pid"]: + (root / name).unlink(missing_ok=True) + start = len(server.events) + terminal = subprocess.Popen(["xterm", "-geometry", "80x25", "-T", "FenrirModifiersFixture", "-e", sys.executable, script, "--outer", str(root)], env=environment) + processes.append(terminal) + wait_for(lambda: (root / "started").exists(), "modifier second session launch") + state = json.loads((root / "started").read_text()) + xdo("windowfocus", "--sync", state["window"]) + spoken("prompt", start) + start = len(server.events) + key("Super_L+i") + spoken("prompt", start) + start = len(server.events) + key("Caps_Lock+z") + spoken("script modifier works", start) + key("a", "Shift_L+Caps_Lock", "b", "Shift_R+Caps_Lock", "c") + wait_for(lambda: recorded() == b"aBc", "script Caps Lock preserves typing and Shift toggle") + key("Super_L+q") + wait_for(lambda: (root / "finished").exists(), "modifier second session quit") + assert json.loads((root / "finished").read_text()) == {"status": 0, "restored": True, "flagsRestored": True} + print("PASS: modifier checkbox announcements, rejected overlap without saving, key role changes saved for next launch, restarted Super review, Caps Lock script dispatch, Shift Caps toggle and ordinary typing") + return + + if os.environ.get("FENRIR_CAPS_TEST_ONLY") == "1": + start = len(server.events) + key("Caps_Lock+i") + spoken("prompt", start) + assert recorded() == b"", recorded() + key("a") + wait_for(lambda: recorded() == b"a", "Caps modifier leaves lowercase typing") + key("Shift_L+Caps_Lock", "b") + wait_for(lambda: recorded() == b"aB", "Shift Caps turns capitalization on") + start = len(server.events) + key("Caps_Lock+i") + spoken("prompt", start) + key("c") + wait_for(lambda: recorded() == b"aBC", "reader chord preserves Caps on") + key("Caps_Lock+Shift_R", "d") + wait_for(lambda: recorded() == b"aBCd", "Caps then Shift turns capitalization off") + start = len(server.events) + key("Caps_Lock+Shift_L+u") + spoken("Top", start) + key("e") + wait_for(lambda: recorded() == b"aBCde", "shifted reader command preserves capitalization") + key("Super_L+z") + wait_for(lambda: recorded() == b"aBCdez", "Super passes through laptop mode") + start = len(server.events) + key("Caps_Lock+f") + spoken("Fenrir Menu", start) + key("Right") + spoken("Keyboard tab", start) + key("Tab", "Tab", "Home") + spoken("Layout combo box Desktop", start) + key("Down") + spoken("Laptop", start) + save_menu() + assert tomllib.loads((root / "settings.toml").read_text())["keyboard"]["layout"] == "laptop" + start = len(server.events) + key("Caps_Lock+f", "Right", "Tab", "Tab") + spoken("Layout combo box Laptop", start) + key("Home", "Escape", "f") + assert tomllib.loads((root / "settings.toml").read_text())["keyboard"]["layout"] == "laptop" + wait_for(lambda: recorded() == b"aBCdezf", "typing after modal capture") + key("Caps_Lock+q") + wait_for(lambda: (root / "finished").exists(), "caps fixture quit") + assert json.loads((root / "finished").read_text()) == {"status": 0, "restored": True, "flagsRestored": True} + print("PASS: laptop Caps Lock review, both Shift Caps orders, shifted commands, capitalization preservation, Super passthrough, layout combo save/cancel, modal capture and cleanup") + return + # A delayed reader-modifier press can predate a desktop client's # keyboard grab. Previously its old timestamp caused fatal EBUSY. start = len(server.events)