Attempt to make modifier key changes take affect when the dialogue closes. This means you don't have to restart Fenrir to apply them.
This commit is contained in:
1 parent
15e04aa8f8
commit
22f6df6af9
23 files changed
+454
-58
No files matched your search
@@ -45,6 +45,7 @@ type Keyboards interface {
|
||||
Scan() error
|
||||
Pass_next()
|
||||
Capture(bool) error
|
||||
Update_modifiers([]string, []string) (bool, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
|
||||
@@ -475,12 +475,13 @@ func (f *fakeSource) Read() (screen.Snapshot, error) {
|
||||
}
|
||||
|
||||
type fakeKeys struct {
|
||||
poll func(bool, func(input.Decision)) error
|
||||
closed, passed bool
|
||||
events chan error
|
||||
captures []bool
|
||||
captureErr error
|
||||
onCapture func(bool)
|
||||
updateModifiers func([]string, []string) (bool, error)
|
||||
poll func(bool, func(input.Decision)) error
|
||||
closed, passed bool
|
||||
events chan error
|
||||
captures []bool
|
||||
captureErr error
|
||||
onCapture func(bool)
|
||||
}
|
||||
|
||||
func (f *fakeKeys) Events() <-chan error { return f.events }
|
||||
@@ -496,6 +497,12 @@ func (f *fakeKeys) Capture(enabled bool) error {
|
||||
}
|
||||
return f.captureErr
|
||||
}
|
||||
func (f *fakeKeys) Update_modifiers(reader, script []string) (bool, error) {
|
||||
if f.updateModifiers != nil {
|
||||
return f.updateModifiers(reader, script)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
func (f *fakeKeys) Pass_next() { f.passed = true }
|
||||
func (f *fakeKeys) Close() error { f.closed = true; return nil }
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.stormux.org/storm/fenrir/internal/config"
|
||||
"git.stormux.org/storm/fenrir/internal/screen"
|
||||
)
|
||||
|
||||
func TestSavedModifiersWaitForMenuCloseAndIdleKeys(t *testing.T) {
|
||||
base := config.Defaults()
|
||||
base.Keyboard.FenrirKeys = []string{"KEY_INSERT"}
|
||||
source := &fakeSource{snapshot: screen.Snapshot{Terminal: 1, Active: true}}
|
||||
voice := &fakeVoice{}
|
||||
calls := 0
|
||||
idle := false
|
||||
keys := &fakeKeys{updateModifiers: func(reader, script []string) (bool, error) {
|
||||
calls++
|
||||
if !reflect.DeepEqual(reader, []string{"KEY_CAPSLOCK"}) || !reflect.DeepEqual(script, []string{"KEY_META"}) {
|
||||
t.Fatal(reader, script)
|
||||
}
|
||||
return idle, nil
|
||||
}}
|
||||
a, err := new_application(context.Background(), source, keys, voice, RunOptions{Settings: base})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
saved := base
|
||||
saved.Keyboard.FenrirKeys = []string{"KEY_CAPSLOCK"}
|
||||
saved.General.ScriptKeys = []string{"KEY_META"}
|
||||
a.ui.active = true
|
||||
a.apply_saved_settings(saved, false)
|
||||
a.apply_pending_modifiers()
|
||||
if calls != 0 || a.settings.pendingModifiers == nil {
|
||||
t.Fatal("changed during modal capture")
|
||||
}
|
||||
a.ui.active = false
|
||||
a.apply_pending_modifiers()
|
||||
if calls != 1 || !reflect.DeepEqual(a.settings.live.Keyboard.FenrirKeys, base.Keyboard.FenrirKeys) || !reflect.DeepEqual(a.settings.live.General.ScriptKeys, base.General.ScriptKeys) {
|
||||
t.Fatal("changed while held", a.settings.live)
|
||||
}
|
||||
idle = true
|
||||
a.apply_pending_modifiers()
|
||||
if calls != 2 || a.settings.pendingModifiers != nil || !reflect.DeepEqual(a.settings.live.Keyboard.FenrirKeys, saved.Keyboard.FenrirKeys) || !reflect.DeepEqual(a.settings.live.General.ScriptKeys, saved.General.ScriptKeys) {
|
||||
t.Fatal("new keys not applied", a.settings.live)
|
||||
}
|
||||
a.apply_pending_modifiers()
|
||||
if calls != 2 {
|
||||
t.Fatal("update repeated after success")
|
||||
}
|
||||
}
|
||||
|
||||
func TestModifierApplyFailureKeepsPreviousRuntimeAndAllowsRetry(t *testing.T) {
|
||||
base := config.Defaults()
|
||||
base.Keyboard.FenrirKeys = []string{"KEY_INSERT"}
|
||||
source := &fakeSource{snapshot: screen.Snapshot{Terminal: 1, Active: true}}
|
||||
voice := &fakeVoice{}
|
||||
keys := &fakeKeys{updateModifiers: func([]string, []string) (bool, error) { return false, errors.New("fixture grab conflict") }}
|
||||
a, err := new_application(context.Background(), source, keys, voice, RunOptions{Settings: base})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
saved := base
|
||||
saved.Keyboard.FenrirKeys = []string{"KEY_META"}
|
||||
a.apply_saved_settings(saved, false)
|
||||
a.apply_pending_modifiers()
|
||||
if a.settings.pendingModifiers != nil || !reflect.DeepEqual(a.settings.live.Keyboard.FenrirKeys, base.Keyboard.FenrirKeys) || !reflect.DeepEqual(a.settings.saveDraft.Keyboard.FenrirKeys, saved.Keyboard.FenrirKeys) {
|
||||
t.Fatal(a.settings)
|
||||
}
|
||||
if len(voice.messages) != 1 || !strings.Contains(voice.messages[0], "Previous keys remain active") {
|
||||
t.Fatal(voice.messages)
|
||||
}
|
||||
keys.updateModifiers = func([]string, []string) (bool, error) { return true, nil }
|
||||
a.apply_saved_settings(saved, false)
|
||||
a.apply_pending_modifiers()
|
||||
if !reflect.DeepEqual(a.settings.live.Keyboard.FenrirKeys, saved.Keyboard.FenrirKeys) {
|
||||
t.Fatal("retry failed")
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.stormux.org/storm/fenrir/internal/config"
|
||||
@@ -26,16 +27,19 @@ import (
|
||||
"git.stormux.org/storm/fenrir/internal/speech"
|
||||
)
|
||||
|
||||
type modifierSettings struct{ fenrirKeys, scriptKeys []string }
|
||||
|
||||
// applicationSettings separates the effective configuration from the save draft.
|
||||
// The draft retains next-launch menu choices; session mute, automatic reading,
|
||||
// and attribute overrides are not implicitly copied into it.
|
||||
type applicationSettings struct {
|
||||
live config.Settings
|
||||
saveDraft config.Settings
|
||||
resetBase config.Settings // Last loaded or successfully saved settings.
|
||||
remoteFields map[string]uint64
|
||||
remoteRevision uint64
|
||||
speechAvailable bool // The startup connection cannot be enabled by a remote setting.
|
||||
pendingModifiers *modifierSettings
|
||||
live config.Settings
|
||||
saveDraft config.Settings
|
||||
resetBase config.Settings // Last loaded or successfully saved settings.
|
||||
remoteFields map[string]uint64
|
||||
remoteRevision uint64
|
||||
speechAvailable bool // The startup connection cannot be enabled by a remote setting.
|
||||
}
|
||||
|
||||
// configure_reader applies startup settings, including next-launch-only fields.
|
||||
@@ -77,6 +81,11 @@ func (a *application) apply_saved_settings(saved config.Settings, speechApplied
|
||||
if speechApplied {
|
||||
copy_speech_selection(&a.settings.live.Presentation.Speech, saved.Presentation.Speech)
|
||||
}
|
||||
if !slices.Equal(saved.Keyboard.FenrirKeys, a.settings.live.Keyboard.FenrirKeys) || !slices.Equal(saved.General.ScriptKeys, a.settings.live.General.ScriptKeys) {
|
||||
a.settings.pendingModifiers = &modifierSettings{fenrirKeys: slices.Clone(saved.Keyboard.FenrirKeys), scriptKeys: slices.Clone(saved.General.ScriptKeys)}
|
||||
} else {
|
||||
a.settings.pendingModifiers = nil
|
||||
}
|
||||
a.settings.saveDraft = saved
|
||||
a.settings.resetBase = saved
|
||||
a.settings.live.InternalUI = saved.InternalUI
|
||||
@@ -120,6 +129,27 @@ func (a *application) apply_saved_settings(saved config.Settings, speechApplied
|
||||
a.clipboards.configure(saved.Clipboard)
|
||||
}
|
||||
|
||||
// apply_pending_modifiers runs on the event loop after menu closure. Native
|
||||
// adapters defer while keys are held; failure leaves the prior runtime intact.
|
||||
func (a *application) apply_pending_modifiers() {
|
||||
pending := a.settings.pendingModifiers
|
||||
if pending == nil || a.ui.active {
|
||||
return
|
||||
}
|
||||
applied, err := a.keyboards.Update_modifiers(pending.fenrirKeys, pending.scriptKeys)
|
||||
if err != nil {
|
||||
a.settings.pendingModifiers = nil
|
||||
a.voice.Say(i18n.Format("Settings saved, but key changes could not be applied: {0}. Previous keys remain active.", err.Error()))
|
||||
return
|
||||
}
|
||||
if !applied {
|
||||
return
|
||||
}
|
||||
a.settings.live.Keyboard.FenrirKeys = pending.fenrirKeys
|
||||
a.settings.live.General.ScriptKeys = pending.scriptKeys
|
||||
a.settings.pendingModifiers = nil
|
||||
}
|
||||
|
||||
// Quick commands have already changed live values and configured speech when
|
||||
// needed. Refresh the reader without copying session overrides into the draft.
|
||||
func (a *application) apply_quick_settings() {
|
||||
|
||||
@@ -128,6 +128,7 @@ func (a *application) poll_input(inputReady bool) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
a.apply_pending_modifiers()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
//go:build linux && cgo
|
||||
|
||||
package console
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.stormux.org/storm/fenrir/internal/command"
|
||||
"git.stormux.org/storm/fenrir/internal/input"
|
||||
)
|
||||
|
||||
func TestConsoleModifierUpdateWaitsForCaptureAndHeldChord(t *testing.T) {
|
||||
layout, _, err := input.Load_bindings([]string{"../../config/keyboard/laptop.toml"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
router, err := input.New_router(layout)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := router.Configure_modifiers([]string{"KEY_INSERT"}, []string{"KEY_COMPOSE"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := router.Configure_scripts([]string{"KEY_COMPOSE"}, []input.ScriptBinding{{ID: "script:example", Keys: []string{"KEY_SCRIPT", "KEY_U"}}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
keys := &Keyboards{router: router}
|
||||
update := func(want bool) {
|
||||
t.Helper()
|
||||
applied, err := keys.Update_modifiers([]string{"KEY_META"}, []string{"KEY_CAPSLOCK"})
|
||||
if err != nil || applied != want {
|
||||
t.Fatal(applied, err)
|
||||
}
|
||||
}
|
||||
keys.router.Capture(true)
|
||||
update(false)
|
||||
keys.router.Capture(false)
|
||||
keys.router.Key(1, input.KeyInsert, 1, false, false)
|
||||
if got := keys.router.Key(1, 23, 1, false, false); got.Command != command.CurrentLine {
|
||||
t.Fatal(got)
|
||||
}
|
||||
update(false)
|
||||
keys.router.Key(1, input.KeyInsert, 0, false, false)
|
||||
update(false)
|
||||
if got := keys.router.Key(1, 23, 0, false, false); got.Forward {
|
||||
t.Fatal("old chord release leaked", got)
|
||||
}
|
||||
update(true)
|
||||
keys.router.Key(1, input.KeyMeta, 1, false, false)
|
||||
if got := keys.router.Key(1, 23, 1, false, false); got.Command != command.CurrentLine {
|
||||
t.Fatal("new reader key failed", got)
|
||||
}
|
||||
keys.router.Key(1, input.KeyMeta, 0, false, false)
|
||||
keys.router.Key(1, 23, 0, false, false)
|
||||
keys.router.Key(1, input.KeyCapsLock, 1, false, false)
|
||||
if got := keys.router.Key(1, 22, 1, false, false); got.Command != "script:example" {
|
||||
t.Fatal("script binding lost", got)
|
||||
}
|
||||
}
|
||||
@@ -308,6 +308,25 @@ func (k *Keyboards) Close() error {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// Update_modifiers waits for every keyboard to release its old chord before
|
||||
// replacing the router; the existing evdev grabs remain owned throughout.
|
||||
func (k *Keyboards) Update_modifiers(fenrirKeys, scriptKeys []string) (bool, error) {
|
||||
if k.router.Captured() || !k.router.Idle() {
|
||||
return false, nil
|
||||
}
|
||||
for _, device := range k.devices {
|
||||
if device.syncing || device.held() {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
candidate := k.router
|
||||
if err := candidate.Configure_modifiers(fenrirKeys, scriptKeys); err != nil {
|
||||
return false, err
|
||||
}
|
||||
k.router = candidate
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (k *Keyboards) Poll(textMode, numLock bool, handle func(input.Decision)) error {
|
||||
if !textMode {
|
||||
for _, device := range k.devices {
|
||||
|
||||
@@ -697,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:92 internal/app/runtime_settings.go:134
|
||||
#: internal/app/fenrir_menu.go:92 internal/app/runtime_settings.go:164
|
||||
msgid "Could not open Fenrir Menu: {0}"
|
||||
msgstr "Fenrir-Menü konnte nicht geöffnet werden: {0}"
|
||||
|
||||
@@ -715,7 +715,7 @@ msgid "Could not save exceptions: {0}"
|
||||
msgstr "Ausnahmen konnten nicht gespeichert werden: {0}"
|
||||
|
||||
#: internal/app/fenrir_menu.go:203 internal/app/fenrir_menu.go:209
|
||||
#: internal/app/runtime_settings.go:177 internal/app/runtime_settings.go:205
|
||||
#: internal/app/runtime_settings.go:207 internal/app/runtime_settings.go:235
|
||||
msgid "Could not save settings: {0}"
|
||||
msgstr "Einstellungen konnten nicht gespeichert werden: {0}"
|
||||
|
||||
@@ -1027,7 +1027,7 @@ msgstr "Gesicht mit herausgestreckter zunge"
|
||||
msgid "Face with symbols"
|
||||
msgstr "Gesicht mit Symbolen"
|
||||
|
||||
#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:160
|
||||
#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:190
|
||||
msgid "Failed to save settings."
|
||||
msgstr "Einstellungen konnten nicht gespeichert werden."
|
||||
|
||||
@@ -2349,7 +2349,7 @@ msgstr "Salutierendes Gesicht"
|
||||
msgid "Santa"
|
||||
msgstr "Weihnachtsmann"
|
||||
|
||||
#: internal/app/runtime_settings.go:156 internal/app/runtime_settings.go:173
|
||||
#: internal/app/runtime_settings.go:186 internal/app/runtime_settings.go:203
|
||||
msgid "Save unavailable."
|
||||
msgstr "Speichern nicht verfügbar."
|
||||
|
||||
@@ -2454,6 +2454,12 @@ msgstr "Setze Fensteranfangs- und -endmarken"
|
||||
msgid "Settings are busy"
|
||||
msgstr "Einstellungen sind beschäftigt"
|
||||
|
||||
#: internal/app/runtime_settings.go:142
|
||||
msgid ""
|
||||
"Settings saved, but key changes could not be applied: {0}. Previous keys "
|
||||
"remain active."
|
||||
msgstr "Einstellungen gespeichert, aber die Tastenänderungen konnten nicht angewendet werden: {0}. Die bisherigen Tasten bleiben aktiv."
|
||||
|
||||
#: internal/app/quick_settings.go:298
|
||||
msgid "Settings saved."
|
||||
msgstr "Einstellungen gespeichert."
|
||||
|
||||
@@ -699,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:92 internal/app/runtime_settings.go:134
|
||||
#: internal/app/fenrir_menu.go:92 internal/app/runtime_settings.go:164
|
||||
msgid "Could not open Fenrir Menu: {0}"
|
||||
msgstr "No se pudo abrir el menú de Fenrir: {0}"
|
||||
|
||||
@@ -717,7 +717,7 @@ msgid "Could not save exceptions: {0}"
|
||||
msgstr "No se pudieron guardar las excepciones: {0}"
|
||||
|
||||
#: internal/app/fenrir_menu.go:203 internal/app/fenrir_menu.go:209
|
||||
#: internal/app/runtime_settings.go:177 internal/app/runtime_settings.go:205
|
||||
#: internal/app/runtime_settings.go:207 internal/app/runtime_settings.go:235
|
||||
msgid "Could not save settings: {0}"
|
||||
msgstr "No se pudo guardar la configuración: {0}"
|
||||
|
||||
@@ -1030,7 +1030,7 @@ msgstr "Cara sacando la lengua"
|
||||
msgid "Face with symbols"
|
||||
msgstr "Cara con símbolos"
|
||||
|
||||
#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:160
|
||||
#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:190
|
||||
msgid "Failed to save settings."
|
||||
msgstr "No se pudo guardar la configuración."
|
||||
|
||||
@@ -2358,7 +2358,7 @@ msgstr "Cara saludando militarmente"
|
||||
msgid "Santa"
|
||||
msgstr "Papá Noel"
|
||||
|
||||
#: internal/app/runtime_settings.go:156 internal/app/runtime_settings.go:173
|
||||
#: internal/app/runtime_settings.go:186 internal/app/runtime_settings.go:203
|
||||
msgid "Save unavailable."
|
||||
msgstr "Guardado no disponible."
|
||||
|
||||
@@ -2463,6 +2463,12 @@ msgstr "Establece una marca de principio y otra para el final de la ventana"
|
||||
msgid "Settings are busy"
|
||||
msgstr "La configuración está ocupada"
|
||||
|
||||
#: internal/app/runtime_settings.go:142
|
||||
msgid ""
|
||||
"Settings saved, but key changes could not be applied: {0}. Previous keys "
|
||||
"remain active."
|
||||
msgstr "Ajustes guardados, pero no se pudieron aplicar los cambios de teclas: {0}. Las teclas anteriores siguen activas."
|
||||
|
||||
#: internal/app/quick_settings.go:298
|
||||
msgid "Settings saved."
|
||||
msgstr "Configuración guardada."
|
||||
|
||||
@@ -681,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:92 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:164
|
||||
msgid "Could not open Fenrir Menu: {0}"
|
||||
msgstr ""
|
||||
|
||||
@@ -697,7 +697,7 @@ msgstr ""
|
||||
msgid "Could not save exceptions: {0}"
|
||||
msgstr ""
|
||||
|
||||
#: internal/app/fenrir_menu.go:203 internal/app/fenrir_menu.go:209 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:207 internal/app/runtime_settings.go:235
|
||||
msgid "Could not save settings: {0}"
|
||||
msgstr ""
|
||||
|
||||
@@ -997,7 +997,7 @@ msgstr ""
|
||||
msgid "Face with symbols"
|
||||
msgstr ""
|
||||
|
||||
#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:160
|
||||
#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:190
|
||||
msgid "Failed to save settings."
|
||||
msgstr ""
|
||||
|
||||
@@ -2309,7 +2309,7 @@ msgstr ""
|
||||
msgid "Santa"
|
||||
msgstr ""
|
||||
|
||||
#: internal/app/runtime_settings.go:156 internal/app/runtime_settings.go:173
|
||||
#: internal/app/runtime_settings.go:186 internal/app/runtime_settings.go:203
|
||||
msgid "Save unavailable."
|
||||
msgstr ""
|
||||
|
||||
@@ -2413,6 +2413,10 @@ msgstr ""
|
||||
msgid "Settings are busy"
|
||||
msgstr ""
|
||||
|
||||
#: internal/app/runtime_settings.go:142
|
||||
msgid "Settings saved, but key changes could not be applied: {0}. Previous keys remain active."
|
||||
msgstr ""
|
||||
|
||||
#: internal/app/quick_settings.go:298
|
||||
msgid "Settings saved."
|
||||
msgstr ""
|
||||
|
||||
@@ -699,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:92 internal/app/runtime_settings.go:134
|
||||
#: internal/app/fenrir_menu.go:92 internal/app/runtime_settings.go:164
|
||||
msgid "Could not open Fenrir Menu: {0}"
|
||||
msgstr "Nie można otworzyć menu Fenrira: {0}"
|
||||
|
||||
@@ -717,7 +717,7 @@ msgid "Could not save exceptions: {0}"
|
||||
msgstr "Nie można zapisać wyjątków: {0}"
|
||||
|
||||
#: internal/app/fenrir_menu.go:203 internal/app/fenrir_menu.go:209
|
||||
#: internal/app/runtime_settings.go:177 internal/app/runtime_settings.go:205
|
||||
#: internal/app/runtime_settings.go:207 internal/app/runtime_settings.go:235
|
||||
msgid "Could not save settings: {0}"
|
||||
msgstr "Nie można zapisać ustawień: {0}"
|
||||
|
||||
@@ -1029,7 +1029,7 @@ msgstr "twarz wystawiająca język"
|
||||
msgid "Face with symbols"
|
||||
msgstr "Twarz z symbolami"
|
||||
|
||||
#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:160
|
||||
#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:190
|
||||
msgid "Failed to save settings."
|
||||
msgstr "Nie udało się zapisać ustawień."
|
||||
|
||||
@@ -2352,7 +2352,7 @@ msgstr "Twarz salutująca"
|
||||
msgid "Santa"
|
||||
msgstr "Święty Mikołaj"
|
||||
|
||||
#: internal/app/runtime_settings.go:156 internal/app/runtime_settings.go:173
|
||||
#: internal/app/runtime_settings.go:186 internal/app/runtime_settings.go:203
|
||||
msgid "Save unavailable."
|
||||
msgstr "Zapisywanie niedostępne."
|
||||
|
||||
@@ -2457,6 +2457,12 @@ msgstr "Ustaw początek i koniec okna."
|
||||
msgid "Settings are busy"
|
||||
msgstr "Ustawienia są zajęte"
|
||||
|
||||
#: internal/app/runtime_settings.go:142
|
||||
msgid ""
|
||||
"Settings saved, but key changes could not be applied: {0}. Previous keys "
|
||||
"remain active."
|
||||
msgstr "Ustawienia zapisano, ale nie udało się zastosować zmian klawiszy: {0}. Poprzednie klawisze pozostają aktywne."
|
||||
|
||||
#: internal/app/quick_settings.go:298
|
||||
msgid "Settings saved."
|
||||
msgstr "Ustawienia zapisane."
|
||||
|
||||
@@ -697,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:92 internal/app/runtime_settings.go:134
|
||||
#: internal/app/fenrir_menu.go:92 internal/app/runtime_settings.go:164
|
||||
msgid "Could not open Fenrir Menu: {0}"
|
||||
msgstr "Não foi possível abrir o menu do Fenrir: {0}"
|
||||
|
||||
@@ -715,7 +715,7 @@ msgid "Could not save exceptions: {0}"
|
||||
msgstr "Não foi possível salvar as exceções: {0}"
|
||||
|
||||
#: internal/app/fenrir_menu.go:203 internal/app/fenrir_menu.go:209
|
||||
#: internal/app/runtime_settings.go:177 internal/app/runtime_settings.go:205
|
||||
#: internal/app/runtime_settings.go:207 internal/app/runtime_settings.go:235
|
||||
msgid "Could not save settings: {0}"
|
||||
msgstr "Não foi possível salvar as configurações: {0}"
|
||||
|
||||
@@ -1029,7 +1029,7 @@ msgstr "Rosto mostrando a língua"
|
||||
msgid "Face with symbols"
|
||||
msgstr "Rosto com símbolos"
|
||||
|
||||
#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:160
|
||||
#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:190
|
||||
msgid "Failed to save settings."
|
||||
msgstr "Falha ao salvar as configurações."
|
||||
|
||||
@@ -2356,7 +2356,7 @@ msgstr "Cara saudando militarmente"
|
||||
msgid "Santa"
|
||||
msgstr "Papai Noel"
|
||||
|
||||
#: internal/app/runtime_settings.go:156 internal/app/runtime_settings.go:173
|
||||
#: internal/app/runtime_settings.go:186 internal/app/runtime_settings.go:203
|
||||
msgid "Save unavailable."
|
||||
msgstr "Salvamento indisponível."
|
||||
|
||||
@@ -2461,6 +2461,12 @@ msgstr "Definir marcas de início e fim da janela"
|
||||
msgid "Settings are busy"
|
||||
msgstr "As configurações estão ocupadas"
|
||||
|
||||
#: internal/app/runtime_settings.go:142
|
||||
msgid ""
|
||||
"Settings saved, but key changes could not be applied: {0}. Previous keys "
|
||||
"remain active."
|
||||
msgstr "Configurações salvas, mas não foi possível aplicar as alterações de teclas: {0}. As teclas anteriores permanecem ativas."
|
||||
|
||||
#: internal/app/quick_settings.go:298
|
||||
msgid "Settings saved."
|
||||
msgstr "Configurações salvas."
|
||||
|
||||
@@ -696,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:92 internal/app/runtime_settings.go:134
|
||||
#: internal/app/fenrir_menu.go:92 internal/app/runtime_settings.go:164
|
||||
msgid "Could not open Fenrir Menu: {0}"
|
||||
msgstr "Не удалось открыть меню Fenrir: {0}"
|
||||
|
||||
@@ -714,7 +714,7 @@ msgid "Could not save exceptions: {0}"
|
||||
msgstr "Не удалось сохранить исключения: {0}"
|
||||
|
||||
#: internal/app/fenrir_menu.go:203 internal/app/fenrir_menu.go:209
|
||||
#: internal/app/runtime_settings.go:177 internal/app/runtime_settings.go:205
|
||||
#: internal/app/runtime_settings.go:207 internal/app/runtime_settings.go:235
|
||||
msgid "Could not save settings: {0}"
|
||||
msgstr "Не удалось сохранить настройки: {0}"
|
||||
|
||||
@@ -1025,7 +1025,7 @@ msgstr "Показывает язык"
|
||||
msgid "Face with symbols"
|
||||
msgstr "Лицо с символами"
|
||||
|
||||
#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:160
|
||||
#: internal/app/quick_settings.go:295 internal/app/runtime_settings.go:190
|
||||
msgid "Failed to save settings."
|
||||
msgstr "Не удалось сохранить настройки."
|
||||
|
||||
@@ -2348,7 +2348,7 @@ msgstr "Отдающее честь лицо"
|
||||
msgid "Santa"
|
||||
msgstr "Дед Мороз"
|
||||
|
||||
#: internal/app/runtime_settings.go:156 internal/app/runtime_settings.go:173
|
||||
#: internal/app/runtime_settings.go:186 internal/app/runtime_settings.go:203
|
||||
msgid "Save unavailable."
|
||||
msgstr "Сохранение недоступно."
|
||||
|
||||
@@ -2453,6 +2453,12 @@ msgstr "Установка начала и конца окна"
|
||||
msgid "Settings are busy"
|
||||
msgstr "Настройки заняты"
|
||||
|
||||
#: internal/app/runtime_settings.go:142
|
||||
msgid ""
|
||||
"Settings saved, but key changes could not be applied: {0}. Previous keys "
|
||||
"remain active."
|
||||
msgstr "Настройки сохранены, но изменения клавиш не удалось применить: {0}. Прежние клавиши остаются активными."
|
||||
|
||||
#: internal/app/quick_settings.go:298
|
||||
msgid "Settings saved."
|
||||
msgstr "Настройки сохранены"
|
||||
|
||||
@@ -149,6 +149,7 @@ func (r *Router) Configure_modifiers(fenrirKeys, scriptKeys []string) error {
|
||||
if err := candidate.Configure_scripts(scriptKeys, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
candidate.scriptBindings = r.scriptBindings
|
||||
*r = candidate
|
||||
return nil
|
||||
}
|
||||
@@ -94,3 +94,20 @@ func TestCapsLockScriptModifierKeepsShiftToggle(t *testing.T) {
|
||||
t.Fatal("script chord toggled capitalization", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModifierReconfigurationPreservesInstalledScriptBindings(t *testing.T) {
|
||||
r := desktop_router(t)
|
||||
if err := r.Configure_modifiers([]string{"KEY_INSERT"}, []string{"KEY_COMPOSE"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := r.Configure_scripts([]string{"KEY_COMPOSE"}, []ScriptBinding{{ID: "script:example", Keys: []string{"KEY_SCRIPT", "KEY_U"}}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := r.Configure_modifiers([]string{"KEY_META"}, []string{"KEY_CAPSLOCK"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.Key(0, KeyCapsLock, 1, false, false)
|
||||
if got := r.Key(0, 22, 1, false, false); got.Command != "script:example" {
|
||||
t.Fatal("script binding lost", got)
|
||||
}
|
||||
}
|
||||
@@ -134,6 +134,9 @@ func (r *Router) Forwarded(device int) []uint16 {
|
||||
return keys
|
||||
}
|
||||
|
||||
// Idle reports whether all devices have released their routed keys.
|
||||
func (r *Router) Idle() bool { return r.empty() }
|
||||
|
||||
// Held reports physical state for one device, including consumed keys whose
|
||||
// release has not arrived yet.
|
||||
func (r *Router) Held(device int) bool { return len(r.keys[device]) != 0 }
|
||||
|
||||
@@ -72,6 +72,36 @@ func (k *Keyboard) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update_modifiers applies only between complete chords and outside modal capture.
|
||||
// The native transaction retains the previous grabs if any new grab is refused.
|
||||
func (k *Keyboard) Update_modifiers(fenrirKeys, scriptKeys []string) (bool, error) {
|
||||
s := k.session
|
||||
if s.router.Captured() || s.grabbed || s.captureBlocked || len(s.held) != 0 || len(s.replayed) != 0 {
|
||||
return false, nil
|
||||
}
|
||||
candidate := s.router
|
||||
if err := candidate.Configure_modifiers(fenrirKeys, scriptKeys); err != nil {
|
||||
return false, err
|
||||
}
|
||||
var keys []C.int
|
||||
for _, key := range append(candidate.Reader_keys(), candidate.Script_keys()...) {
|
||||
keys = append(keys, C.int(key))
|
||||
}
|
||||
var keyPointer *C.int
|
||||
if len(keys) > 0 {
|
||||
keyPointer = &keys[0]
|
||||
}
|
||||
status := C.terminal_x_update_keys(s.x, keyPointer, C.int(len(keys)))
|
||||
if err := native_error(status); err != nil {
|
||||
return false, fmt.Errorf("update X11 modifier keys: %w", err)
|
||||
}
|
||||
if status != 0 {
|
||||
return false, nil
|
||||
}
|
||||
s.router = candidate
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// capture_keyboard treats desktop/focus contention as a refused chord, not a
|
||||
// terminal-session failure. Keep modal intent while discarding stale key state.
|
||||
func (s *Session) capture_keyboard() (int, error) {
|
||||
|
||||
+62
-17
@@ -175,7 +175,7 @@ int terminal_key(VTerm *vt, const TerminalEvent *event, char *bytes, int size) {
|
||||
return length;
|
||||
}
|
||||
|
||||
struct TerminalX { Display *display; Window window; unsigned int numlock; int grabbed; XIM im; XIC ic; int scriptKeys[KEY_MAX+1]; int scriptKeyCount; };
|
||||
struct TerminalX { Display *display; Window window; unsigned int numlock; int grabbed; XIM im; XIC ic; int scriptKeys[KEY_MAX+1]; int scriptKeyCount; unsigned char grabbedKeys[256]; };
|
||||
static int x_error;
|
||||
static int trap_error(Display *display, XErrorEvent *event) {
|
||||
(void)display;
|
||||
@@ -206,8 +206,7 @@ static void observe_windows(TerminalX *x, Window window, int depth, int *remaini
|
||||
}
|
||||
if (children) XFree(children);
|
||||
}
|
||||
static void install_grabs(TerminalX *x) {
|
||||
Window window = x->window;
|
||||
static void collect_grabs(TerminalX *x, const int *keys, int count, unsigned char wanted[256]) {
|
||||
x->numlock = 0;
|
||||
XModifierKeymap *modifiers = XGetModifierMapping(x->display);
|
||||
KeyCode numlock = XKeysymToKeycode(x->display, XK_Num_Lock);
|
||||
@@ -218,14 +217,14 @@ static void install_grabs(TerminalX *x) {
|
||||
x->numlock |= 1u << mask;
|
||||
XFreeModifiermap(modifiers);
|
||||
}
|
||||
KeySym keys[] = {XK_Insert,
|
||||
KeySym fixedKeys[] = {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,
|
||||
XK_Control_L, XK_Control_R};
|
||||
for (unsigned int i=0; i<sizeof(keys)/sizeof(keys[0]); i++) {
|
||||
KeyCode code = XKeysymToKeycode(x->display, keys[i]);
|
||||
if (code) XGrabKey(x->display, code, AnyModifier, window, False, GrabModeAsync, GrabModeSync);
|
||||
for (unsigned int i=0; i<sizeof(fixedKeys)/sizeof(fixedKeys[0]); i++) {
|
||||
KeyCode code = XKeysymToKeycode(x->display, fixedKeys[i]);
|
||||
if (code) wanted[code] = 1;
|
||||
}
|
||||
// Script modifiers may be any Linux key, not just Compose. Resolve through
|
||||
// the same symbol translation as event routing instead of assuming that
|
||||
@@ -235,14 +234,63 @@ static void install_grabs(TerminalX *x) {
|
||||
for (int code=first; code<=last; code++) {
|
||||
KeySym symbol = XkbKeycodeToKeysym(x->display, code, 0, 0);
|
||||
int key = terminal_key_code(symbol, code);
|
||||
for (int i=0; i<x->scriptKeyCount; i++) {
|
||||
if (key == x->scriptKeys[i]) {
|
||||
XGrabKey(x->display, code, AnyModifier, window, False, GrabModeAsync, GrabModeSync);
|
||||
for (int i=0; i<count; i++) {
|
||||
if (key == keys[i]) {
|
||||
wanted[code] = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
static int replace_grabs(TerminalX *x, const int *keys, int count, int idleOnly) {
|
||||
if (count < 0 || count > KEY_MAX+1) return -EINVAL;
|
||||
// Freeze other clients only for the bounded grab transaction. This closes
|
||||
// the gap between the idle check, adding grabs and switching key sets.
|
||||
XGrabServer(x->display);
|
||||
XSync(x->display, False);
|
||||
if (idleOnly) {
|
||||
char down[32];
|
||||
XQueryKeymap(x->display, down);
|
||||
int busy = XPending(x->display) != 0;
|
||||
for (int i=0; i<32; i++) busy |= down[i] != 0;
|
||||
if (busy) { XUngrabServer(x->display); XFlush(x->display); return 1; }
|
||||
}
|
||||
unsigned char wanted[256] = {0};
|
||||
XErrorHandler previous = begin_trap(x->display);
|
||||
collect_grabs(x, keys, count, wanted);
|
||||
for (int code=0; code<256; code++) {
|
||||
if (wanted[code] && !x->grabbedKeys[code])
|
||||
XGrabKey(x->display, code, AnyModifier, x->window, False, GrabModeAsync, GrabModeSync);
|
||||
}
|
||||
int error = end_trap(x->display, previous);
|
||||
if (error) {
|
||||
previous = begin_trap(x->display);
|
||||
for (int code=0; code<256; code++) {
|
||||
if (wanted[code] && !x->grabbedKeys[code])
|
||||
XUngrabKey(x->display, code, AnyModifier, x->window);
|
||||
}
|
||||
end_trap(x->display, previous);
|
||||
} else {
|
||||
for (int code=0; code<256; code++) {
|
||||
if (x->grabbedKeys[code] && !wanted[code])
|
||||
XUngrabKey(x->display, code, AnyModifier, x->window);
|
||||
}
|
||||
memcpy(x->grabbedKeys, wanted, sizeof(wanted));
|
||||
x->scriptKeyCount = count;
|
||||
for (int i=0; i<count; i++) x->scriptKeys[i] = keys[i];
|
||||
}
|
||||
XUngrabServer(x->display);
|
||||
XSync(x->display, False);
|
||||
return error ? -error : 0;
|
||||
}
|
||||
static void install_grabs(TerminalX *x) {
|
||||
unsigned char wanted[256] = {0};
|
||||
collect_grabs(x, x->scriptKeys, x->scriptKeyCount, wanted);
|
||||
for (int code=0; code<256; code++) {
|
||||
if (wanted[code]) XGrabKey(x->display, code, AnyModifier, x->window, False, GrabModeAsync, GrabModeSync);
|
||||
}
|
||||
memcpy(x->grabbedKeys, wanted, sizeof(wanted));
|
||||
}
|
||||
int terminal_x_caps_lock(TerminalX *x, int enabled) {
|
||||
if (!XkbLockModifiers(x->display, XkbUseCoreKbd, LockMask, enabled ? LockMask : 0)) return -EIO;
|
||||
XSync(x->display, False);
|
||||
@@ -254,13 +302,10 @@ int terminal_x_toggle_caps(TerminalX *x) {
|
||||
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);
|
||||
x->scriptKeyCount = count;
|
||||
for (int i=0; i<count; i++) x->scriptKeys[i] = keys[i];
|
||||
install_grabs(x);
|
||||
int error = end_trap(x->display, previous);
|
||||
return error ? -error : 0;
|
||||
return replace_grabs(x, keys, count, 0);
|
||||
}
|
||||
int terminal_x_update_keys(TerminalX *x, const int *keys, int count) {
|
||||
return replace_grabs(x, keys, count, 1);
|
||||
}
|
||||
TerminalX *terminal_x_open(unsigned long window, int *error) {
|
||||
TerminalX *x = calloc(1, sizeof(*x));
|
||||
|
||||
@@ -29,6 +29,7 @@ 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);
|
||||
int terminal_x_update_keys(TerminalX *x, const int *keys, int count);
|
||||
void terminal_x_close(TerminalX *x);
|
||||
int terminal_x_fd(TerminalX *x);
|
||||
int terminal_x_down(TerminalX *x, int code);
|
||||
|
||||
Reference in new issue
Block a user