Inject Remote lock keys instead of retiring keyboard input
Refusing Caps, Num and Scroll Lock retired all remote input, so a controller using Caps Lock as its screen-reader modifier lost keyboard control on the first press. Lock keys are now owned like other keys and released once at retirement without another toggle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8e0ac3b258
commit
1c8ec3ce10
4 files changed
+52
-14
No files matched your search
@@ -62,7 +62,7 @@ fixture changes maps and lock state explicitly to exercise these cases.
|
||||
See the [XKB library specification](https://xorg.freedesktop.org/archive/current/doc/libX11/XKB/xkblib.html)
|
||||
for `XkbGetMap`, `XkbGetState` and `XkbTranslateKeyCode`.
|
||||
|
||||
Lock-key downs are unavailable until lock synchronization and reader modifier
|
||||
(Superseded: see Lock keys below.) Lock-key downs were unavailable until lock synchronization and reader modifier
|
||||
handling have a defined policy. This includes Caps Lock even when a reader
|
||||
configuration would otherwise use it as a modifier. AltGr, IME/dead keys,
|
||||
general group/level handling and exact remote autorepeat remain unfinished.
|
||||
@@ -114,3 +114,15 @@ external Remote race suite, affected-package vet, a separate reader build,
|
||||
the existing speech regression harness and Tentacles `make check` passed.
|
||||
No full reader suite, installed-binary behavior,
|
||||
audible speech, physical input/Braille or live NVDA/Orca acceptance was performed.
|
||||
|
||||
## Lock keys (2026-11-06)
|
||||
|
||||
Caps Lock, Num Lock and Scroll Lock are now owned like other keys instead of
|
||||
being refused. Refusal retired all remote input, so a controller using Caps
|
||||
Lock as its NVDA or Cthulhu modifier lost keyboard control on the first press.
|
||||
A remote press and release toggles the lock here, as the same physical key
|
||||
would. Retirement releases a held lock key once and never re-toggles a lock;
|
||||
when Caps Lock is the controlled reader's Cthulhu modifier, the command owner
|
||||
keeps its local lock state. A private Xvfb test checks that each lock key is
|
||||
owned and released at retirement. Lock-state synchronization between computers
|
||||
is not attempted.
|
||||
@@ -80,11 +80,10 @@ func (ledger *RemoteKeyLedger) Apply(name string, pressed bool) error {
|
||||
if !pressed {
|
||||
return nil // An unowned up must not release a physical/local key.
|
||||
}
|
||||
if name == "Caps_Lock" || name == "Num_Lock" || name == "Scroll_Lock" {
|
||||
// Lock synchronization and reader-modifier handling need their own
|
||||
// native policy. The quarantined backend must not toggle local locks.
|
||||
return ledger.fail(ErrRemoteKeyUnavailable)
|
||||
}
|
||||
// Lock keys are owned like any other key: a remote press and release
|
||||
// toggles the lock here, as the same physical key would. The ledger still
|
||||
// releases a lock key held at retirement; it never re-toggles a lock. When
|
||||
// Caps Lock is the Cthulhu modifier, the command owner keeps its lock state.
|
||||
if len(ledger.held) == RemoteHeldKeyLimit {
|
||||
return ledger.fail(ErrRemoteKeyLimit)
|
||||
}
|
||||
|
||||
@@ -170,11 +170,16 @@ func TestRemoteKeyLedgerBoundsAndUnavailableKeys(t *testing.T) {
|
||||
if !errors.Is(absent.Apply("a", true), ErrRemoteKeyState) || !errors.Is(absent.Retire(), ErrRemoteKeyState) {
|
||||
t.Fatal("nil ledger accepted work")
|
||||
}
|
||||
for _, name := range []string{"unknown private key", "Caps_Lock", "Num_Lock", "Scroll_Lock"} {
|
||||
ledger, backend := ledgerFixture(t)
|
||||
if err := ledger.Apply(name, true); !errors.Is(err, ErrRemoteKeyUnavailable) || len(backend.effects) != 0 || len(backend.resolves) != 0 {
|
||||
t.Fatal("unsupported key or lock reached the native backend")
|
||||
}
|
||||
ledger, backend := ledgerFixture(t)
|
||||
if err := ledger.Apply("unknown private key", true); !errors.Is(err, ErrRemoteKeyUnavailable) || len(backend.effects) != 0 || len(backend.resolves) != 0 {
|
||||
t.Fatal("unsupported key reached the native backend")
|
||||
}
|
||||
// Lock keys are owned like other keys and released once at retirement.
|
||||
ledger, backend = ledgerFixture(t)
|
||||
backend.codes["Caps_Lock"] = 66
|
||||
applyLedgerKey(t, ledger, "Caps_Lock", true)
|
||||
if err := ledger.Retire(); err != nil || !reflect.DeepEqual(backend.effects, []ledgerTransition{{66, true}, {66, false}}) {
|
||||
t.Fatal("lock key was not owned and released once", err, backend.effects)
|
||||
}
|
||||
for _, code := range []uint16{0, 7, 256, 65535} {
|
||||
ledger, backend := ledgerFixture(t)
|
||||
@@ -183,7 +188,7 @@ func TestRemoteKeyLedgerBoundsAndUnavailableKeys(t *testing.T) {
|
||||
t.Fatal("invalid native code produced an effect")
|
||||
}
|
||||
}
|
||||
ledger, backend := ledgerFixture(t)
|
||||
ledger, backend = ledgerFixture(t)
|
||||
for index := range RemoteHeldKeyLimit {
|
||||
name := string(rune('a' + index))
|
||||
if index >= 26 {
|
||||
|
||||
@@ -200,8 +200,30 @@ func TestNativeRemoteKeyTargetLayoutChangeKeepsRelease(t *testing.T) {
|
||||
remoteNativeHeld(t, probe, after, false)
|
||||
}
|
||||
|
||||
func TestNativeRemoteKeyTargetRejectsUnavailableAndLocks(t *testing.T) {
|
||||
for _, name := range []string{"unknown private name", "Caps_Lock", "Num_Lock", "Scroll_Lock", "semicolon"} {
|
||||
func TestNativeRemoteKeyTargetLockKeysAreOwnedAndReleased(t *testing.T) {
|
||||
for _, name := range []string{"Caps_Lock", "Num_Lock", "Scroll_Lock"} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
remoteNativeThread(t)
|
||||
target, probe := remoteNativeTarget(t), remoteNativeTarget(t)
|
||||
code, err := target.backend.Resolve(name)
|
||||
if err != nil {
|
||||
t.Skip("lock key absent from this keymap")
|
||||
}
|
||||
if err := target.apply(name, true); err != nil {
|
||||
t.Fatal("lock key refused", err)
|
||||
}
|
||||
remoteNativeHeld(t, probe, code, true)
|
||||
// Retirement releases the held lock key without another toggle.
|
||||
if err := target.retire(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
remoteNativeHeld(t, probe, code, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNativeRemoteKeyTargetRejectsUnavailable(t *testing.T) {
|
||||
for _, name := range []string{"unknown private name", "semicolon"} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
remoteNativeThread(t)
|
||||
target, probe := remoteNativeTarget(t), remoteNativeTarget(t)
|
||||
|
||||
Reference in new issue
Block a user