Route quarantined Remote capture and balance bypass modifiers

Add RemoteCaptureRouter for normalized controller capture. Ordinary keys
and Cthulhu/NVDA modifier chords are forwarded; only a required core
escape chord and local chords derived from resolved bindings stay local.
A local chord first releases forwarded modifiers remotely; a later
forwarded key presses still-held ones again. Releases of local or
withdrawn keys are consumed, and retirement reports physically held keys
whose releases the native owner must swallow. No production caller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chase CichorzandClaude Opus 5.5 committed 2026-11-06 16:06:38 -05:00
1 parent 6b3aa80138
commit c1333c2fdb
3 files changed
+594

No files matched your search

+255
View File
@@ -0,0 +1,255 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package input
import "errors"
var ErrRemoteCaptureChord = errors.New("remote capture local shortcut is invalid")
// RemoteCaptureKey is one normalized captured transition. Symbol is the
// group-zero, level-zero keysym the native owner resolved for a press, the
// same form the command owner matches against bindings; releases and repeats are
// matched by keycode, so a later layout change cannot alter ownership.
type RemoteCaptureKey struct {
Keycode uint8
Symbol uint32
Pressed bool
Repeat bool
}
// RemoteCaptureResult is the routing decision for one captured transition.
// Remote transitions must be delivered in order before Local is dispatched.
// Escape is set only for the core escape chord; Local names a matched
// explicit local command. At most one of them is set.
type RemoteCaptureResult struct {
Remote []RemoteRepeatTransition
Local Command
Escape bool
}
// RemoteCaptureRetirement transfers cleanup. Remote releases every key
// forwarded and still owned, in reverse down order. Swallow lists every key
// still physically held: the local application never received those presses,
// so the native owner must consume their releases instead of ungrabbing first.
type RemoteCaptureRetirement struct {
Remote []RemoteRepeatTransition
Swallow []uint8
}
type remoteCaptureState uint8
const (
remoteCaptureIdle remoteCaptureState = iota
remoteCaptureForwarded // Down sent to the remote.
remoteCaptureWithdrawn // Modifier released remotely after a local chord.
remoteCaptureLocal // Press consumed by a local command.
)
// RemoteCaptureRouter decides, for a controller capture lifetime, which keys
// go to the remote and which stay local. Ordinary keys and Cthulhu/NVDA
// modifier chords are forwarded. Only the core escape and the supplied local
// chords stay local; callers derive those from resolved bindings so remaps
// are honored rather than a hard-coded list.
//
// Modifiers cannot be classified when pressed, so they are forwarded at once.
// When a later key completes a local chord, forwarded modifiers are released
// remotely first, so no prefix stays held on the remote. While still held they
// are withdrawn; a later forwarded key presses them again before itself, so the
// remote sees the physical chord. Releases of local or withdrawn keys are
// consumed and never reach the remote.
//
// Input must come from RemoteRepeatNormalizer. One serial owner drives an
// instance; it performs no I/O and grants no consent or provenance.
type RemoteCaptureRouter struct {
escape CommandChord
local CommandBindings
cthulhu map[uint32]bool
state [256]remoteCaptureState
modifier [256]bool
mask [256]uint16
isCthulhu [256]bool
order []uint8
stopped bool
retired bool
}
// NewRemoteCaptureRouter requires a non-modifier escape chord. Local chords
// equal to the escape are ignored: the escape always wins.
func NewRemoteCaptureRouter(escape CommandChord, local CommandBindings, cthulhuModifiers []uint32) (*RemoteCaptureRouter, error) {
router := &RemoteCaptureRouter{
escape: escape,
local: make(CommandBindings, len(local)),
cthulhu: make(map[uint32]bool, len(cthulhuModifiers)),
order: make([]uint8, 0, RemoteHeldKeyLimit),
}
for _, symbol := range cthulhuModifiers {
router.cthulhu[symbol] = true
}
if escape.Symbol == 0 || router.isModifierSymbol(escape.Symbol) {
return nil, ErrRemoteCaptureChord
}
for chord, command := range local {
if chord == escape || command == "" {
continue
}
if chord.Symbol == 0 || router.isModifierSymbol(chord.Symbol) {
return nil, ErrRemoteCaptureChord
}
router.local[chord] = command
}
return router, nil
}
func remoteModifierMask(symbol uint32) (uint16, bool) {
switch symbol {
case leftShiftKeySymbol, rightShiftKeySymbol:
return 1, true
case leftControlKeySymbol, rightControlKeySymbol:
return controlModifier, true
case 0xffe7, 0xffe8, 0xffe9, 0xffea: // Meta, Alt.
return 8, true
case 0xffeb, 0xffec: // Super.
return 64, true
case 0xffed, 0xffee: // Hyper: a modifier with no chord bit.
return 0, true
}
return 0, false
}
func (router *RemoteCaptureRouter) isModifierSymbol(symbol uint32) bool {
_, modifier := remoteModifierMask(symbol)
return modifier || router.cthulhu[symbol]
}
func (router *RemoteCaptureRouter) fail(err error) (RemoteCaptureResult, error) {
router.stopped = true
return RemoteCaptureResult{}, err
}
// Route handles one transition. An error stops routing permanently while
// retaining ownership for Retire.
func (router *RemoteCaptureRouter) Route(key RemoteCaptureKey) (RemoteCaptureResult, error) {
if router == nil || router.stopped {
return RemoteCaptureResult{}, ErrRemoteKeyState
}
code := key.Keycode
if code < remoteRepeatMinKeycode {
return router.fail(ErrRemoteKeyUnavailable)
}
state := router.state[code]
if !key.Pressed {
if state == remoteCaptureIdle {
return RemoteCaptureResult{}, nil
}
router.forget(code)
if state == remoteCaptureForwarded {
return RemoteCaptureResult{Remote: []RemoteRepeatTransition{{Keycode: code}}}, nil
}
return RemoteCaptureResult{}, nil
}
if key.Repeat {
switch state {
case remoteCaptureIdle:
return router.fail(ErrRemoteKeyCollision)
case remoteCaptureForwarded:
return RemoteCaptureResult{Remote: []RemoteRepeatTransition{{Keycode: code, Pressed: true, Repeat: true}}}, nil
}
return RemoteCaptureResult{}, nil
}
if state != remoteCaptureIdle {
return router.fail(ErrRemoteKeyCollision)
}
if len(router.order) >= RemoteHeldKeyLimit {
return router.fail(ErrRemoteKeyLimit)
}
if mask, modifier := remoteModifierMask(key.Symbol); modifier || router.cthulhu[key.Symbol] {
router.hold(code, remoteCaptureForwarded)
router.modifier[code] = true
router.mask[code] = mask
router.isCthulhu[code] = router.cthulhu[key.Symbol]
return RemoteCaptureResult{Remote: []RemoteRepeatTransition{{Keycode: code, Pressed: true}}}, nil
}
chord := router.chord(key.Symbol)
escape := chord == router.escape
command := Command("")
if !escape {
command = router.local[chord]
}
if escape || command != "" {
var result RemoteCaptureResult
for index := len(router.order) - 1; index >= 0; index-- {
held := router.order[index]
if router.modifier[held] && router.state[held] == remoteCaptureForwarded {
router.state[held] = remoteCaptureWithdrawn
result.Remote = append(result.Remote, RemoteRepeatTransition{Keycode: held})
}
}
router.hold(code, remoteCaptureLocal)
result.Local, result.Escape = command, escape
return result, nil
}
var result RemoteCaptureResult
for _, held := range router.order {
if router.state[held] == remoteCaptureWithdrawn {
router.state[held] = remoteCaptureForwarded
result.Remote = append(result.Remote, RemoteRepeatTransition{Keycode: held, Pressed: true})
}
}
router.hold(code, remoteCaptureForwarded)
result.Remote = append(result.Remote, RemoteRepeatTransition{Keycode: code, Pressed: true})
return result, nil
}
// Retire stops routing and transfers cleanup once.
func (router *RemoteCaptureRouter) Retire() RemoteCaptureRetirement {
if router == nil || router.retired {
return RemoteCaptureRetirement{}
}
router.stopped = true
router.retired = true
var out RemoteCaptureRetirement
for index := len(router.order) - 1; index >= 0; index-- {
code := router.order[index]
if router.state[code] == remoteCaptureForwarded {
out.Remote = append(out.Remote, RemoteRepeatTransition{Keycode: code})
}
out.Swallow = append(out.Swallow, code)
}
router.state = [256]remoteCaptureState{}
router.modifier = [256]bool{}
router.order = router.order[:0]
return out
}
func (router *RemoteCaptureRouter) chord(symbol uint32) CommandChord {
chord := CommandChord{Symbol: symbol}
for _, code := range router.order {
if !router.modifier[code] {
continue
}
chord.Modifiers |= router.mask[code]
chord.CthulhuModifier = chord.CthulhuModifier || router.isCthulhu[code]
}
return chord
}
func (router *RemoteCaptureRouter) hold(code uint8, state remoteCaptureState) {
router.state[code] = state
router.order = append(router.order, code)
}
func (router *RemoteCaptureRouter) forget(code uint8) {
router.state[code] = remoteCaptureIdle
router.modifier[code] = false
router.mask[code] = 0
router.isCthulhu[code] = false
for index, held := range router.order {
if held == code {
copy(router.order[index:], router.order[index+1:])
router.order = router.order[:len(router.order)-1]
return
}
}
}
+307
View File
@@ -0,0 +1,307 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package input
import (
"errors"
"reflect"
"testing"
"time"
)
const (
testInsertSymbol uint32 = 0xff63
testF11Symbol uint32 = 0xffc8
codeShift uint8 = 50
codeControl uint8 = 37
codeAlt uint8 = 64
codeInsert uint8 = 118
codeA uint8 = 38
codeR uint8 = 27
codeF11 uint8 = 95
)
var testEscape = CommandChord{Symbol: testF11Symbol}
func newTestCaptureRouter(t *testing.T, local CommandBindings) *RemoteCaptureRouter {
t.Helper()
router, err := NewRemoteCaptureRouter(testEscape, local, []uint32{testInsertSymbol})
if err != nil {
t.Fatal(err)
}
return router
}
type captureStep struct {
key RemoteCaptureKey
want RemoteCaptureResult
}
func press(code uint8, symbol uint32) RemoteCaptureKey {
return RemoteCaptureKey{Keycode: code, Symbol: symbol, Pressed: true}
}
func hold(code uint8, symbol uint32) RemoteCaptureKey {
return RemoteCaptureKey{Keycode: code, Symbol: symbol, Pressed: true, Repeat: true}
}
func release(code uint8) RemoteCaptureKey { return RemoteCaptureKey{Keycode: code} }
func remote(transitions ...RemoteRepeatTransition) RemoteCaptureResult {
return RemoteCaptureResult{Remote: transitions}
}
func runCaptureSteps(t *testing.T, router *RemoteCaptureRouter, steps []captureStep) {
t.Helper()
for index, step := range steps {
got, err := router.Route(step.key)
if err != nil {
t.Fatalf("step %d: %v", index, err)
}
if len(got.Remote) == 0 {
got.Remote = nil
}
if len(step.want.Remote) == 0 {
step.want.Remote = nil
}
if !reflect.DeepEqual(got, step.want) {
t.Fatalf("step %d: result = %+v, want %+v", index, got, step.want)
}
}
}
func TestBehaviorRemoteCaptureForwardsOrdinaryAndReaderChords(t *testing.T) {
router := newTestCaptureRouter(t, nil)
runCaptureSteps(t, router, []captureStep{
{press(codeShift, leftShiftKeySymbol), remote(down(codeShift))},
{press(codeA, 'a'), remote(down(codeA))},
{hold(codeA, 'a'), remote(repeat(codeA))},
{release(codeA), remote(up(codeA))},
{release(codeShift), remote(up(codeShift))},
// A Cthulhu/NVDA modifier chord goes to the remote screen reader.
{press(codeInsert, testInsertSymbol), remote(down(codeInsert))},
{press(codeA, 'a'), remote(down(codeA))},
{release(codeA), remote(up(codeA))},
{release(codeInsert), remote(up(codeInsert))},
// Releases for keys held before capture are not forwarded.
{release(codeR), RemoteCaptureResult{}},
})
if got := router.Retire(); len(got.Remote) != 0 || len(got.Swallow) != 0 {
t.Fatalf("retire = %+v", got)
}
}
func TestBehaviorRemoteCaptureWithdrawsPrefixForLocalChord(t *testing.T) {
local := CommandBindings{{Symbol: 'r', Modifiers: controlModifier, CthulhuModifier: true}: "tentacle/remote/toggle-remote-focus"}
router := newTestCaptureRouter(t, local)
runCaptureSteps(t, router, []captureStep{
{press(codeInsert, testInsertSymbol), remote(down(codeInsert))},
{press(codeControl, leftControlKeySymbol), remote(down(codeControl))},
{hold(codeControl, leftControlKeySymbol), remote(repeat(codeControl))},
// The local chord releases the forwarded prefix remotely, newest first.
{press(codeR, 'r'), RemoteCaptureResult{Remote: []RemoteRepeatTransition{up(codeControl), up(codeInsert)}, Local: "tentacle/remote/toggle-remote-focus"}},
{hold(codeR, 'r'), RemoteCaptureResult{}},
{hold(codeControl, leftControlKeySymbol), RemoteCaptureResult{}},
{release(codeR), RemoteCaptureResult{}},
// A forwarded key while the prefix is still held presses it again.
{press(codeA, 'a'), remote(down(codeInsert), down(codeControl), down(codeA))},
{release(codeA), remote(up(codeA))},
{release(codeControl), remote(up(codeControl))},
{release(codeInsert), remote(up(codeInsert))},
})
}
func TestBehaviorRemoteCaptureWithdrawnReleaseIsConsumed(t *testing.T) {
local := CommandBindings{{Symbol: 'r', Modifiers: controlModifier}: "tentacle/remote/disconnect"}
router := newTestCaptureRouter(t, local)
runCaptureSteps(t, router, []captureStep{
{press(codeControl, rightControlKeySymbol), remote(down(codeControl))},
{press(codeR, 'r'), RemoteCaptureResult{Remote: []RemoteRepeatTransition{up(codeControl)}, Local: "tentacle/remote/disconnect"}},
{release(codeControl), RemoteCaptureResult{}}, // Already released remotely.
{release(codeR), RemoteCaptureResult{}},
{press(codeR, 'r'), remote(down(codeR))}, // Without Control it is ordinary.
})
}
func TestBehaviorRemoteCaptureEscapeWinsAndRetireSwallows(t *testing.T) {
// A local binding on the escape chord cannot replace the escape.
local := CommandBindings{testEscape: "tentacle/remote/other"}
router := newTestCaptureRouter(t, local)
runCaptureSteps(t, router, []captureStep{
{press(codeShift, leftShiftKeySymbol), remote(down(codeShift))},
{press(codeA, 'a'), remote(down(codeA))},
{press(codeAlt, 0xffe9), remote(down(codeAlt))},
// Shift+Alt+F11 is not the bare escape chord: it is forwarded.
{press(codeF11, testF11Symbol), remote(down(codeF11))},
{release(codeF11), remote(up(codeF11))},
{release(codeAlt), remote(up(codeAlt))},
{release(codeShift), remote(up(codeShift))},
{press(codeF11, testF11Symbol), RemoteCaptureResult{Escape: true}},
})
got := router.Retire()
want := RemoteCaptureRetirement{Remote: []RemoteRepeatTransition{up(codeA)}, Swallow: []uint8{codeF11, codeA}}
if !reflect.DeepEqual(got, want) {
t.Fatalf("retire = %+v, want %+v", got, want)
}
if again := router.Retire(); again.Remote != nil || again.Swallow != nil {
t.Fatalf("second retire = %+v", again)
}
if _, err := router.Route(release(codeA)); !errors.Is(err, ErrRemoteKeyState) {
t.Fatalf("route after retire = %v", err)
}
}
func TestBehaviorRemoteCaptureRetireReleasesForwardedPrefixes(t *testing.T) {
local := CommandBindings{{Symbol: 'r', Modifiers: controlModifier}: "tentacle/remote/menu"}
router := newTestCaptureRouter(t, local)
runCaptureSteps(t, router, []captureStep{
{press(codeShift, leftShiftKeySymbol), remote(down(codeShift))},
{press(codeControl, leftControlKeySymbol), remote(down(codeControl))},
{press(codeR, 'r'), remote(down(codeR))}, // Shift+Control+r is not bound.
})
got := router.Retire()
want := RemoteCaptureRetirement{
Remote: []RemoteRepeatTransition{up(codeR), up(codeControl), up(codeShift)},
Swallow: []uint8{codeR, codeControl, codeShift},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("retire = %+v, want %+v", got, want)
}
}
func TestBehaviorRemoteCaptureStopsOnInconsistentInput(t *testing.T) {
for name, keys := range map[string][]RemoteCaptureKey{
"orphan repeat": {hold(codeA, 'a')},
"double down": {press(codeA, 'a'), press(codeA, 'a')},
"invalid code": {press(codeA, 'a'), press(7, 'b')},
} {
t.Run(name, func(t *testing.T) {
router := newTestCaptureRouter(t, nil)
var err error
for _, key := range keys {
if _, err = router.Route(key); err != nil {
break
}
}
if err == nil {
t.Fatal("inconsistent input accepted")
}
if _, err := router.Route(release(codeA)); !errors.Is(err, ErrRemoteKeyState) {
t.Fatalf("stopped router = %v", err)
}
got := router.Retire()
if len(keys) > 1 && !reflect.DeepEqual(got.Remote, []RemoteRepeatTransition{up(codeA)}) {
t.Fatalf("retire = %+v", got)
}
})
}
router := newTestCaptureRouter(t, nil)
for index := 0; index < RemoteHeldKeyLimit; index++ {
code := uint8(100 + index)
if _, err := router.Route(press(code, 'a')); err != nil {
t.Fatal(err)
}
}
if _, err := router.Route(press(200, 'b')); !errors.Is(err, ErrRemoteKeyLimit) {
t.Fatalf("overflow = %v", err)
}
if got := router.Retire(); len(got.Remote) != RemoteHeldKeyLimit || len(got.Swallow) != RemoteHeldKeyLimit {
t.Fatalf("retire after overflow = %d/%d", len(got.Remote), len(got.Swallow))
}
}
func TestBehaviorRemoteCaptureRejectsInvalidChords(t *testing.T) {
for name, test := range map[string]struct {
escape CommandChord
local CommandBindings
}{
"empty escape": {CommandChord{}, nil},
"modifier escape": {CommandChord{Symbol: leftShiftKeySymbol}, nil},
"reader escape": {CommandChord{Symbol: testInsertSymbol}, nil},
"modifier local": {testEscape, CommandBindings{{Symbol: leftControlKeySymbol}: "tentacle/remote/x"}},
} {
if _, err := NewRemoteCaptureRouter(test.escape, test.local, []uint32{testInsertSymbol}); !errors.Is(err, ErrRemoteCaptureChord) {
t.Fatalf("%s: %v", name, err)
}
}
}
// Local chords come from resolved bindings filtered to Remote's commands, so a
// user-assigned shortcut is local while ordinary reader commands are forwarded.
func TestBehaviorRemoteCaptureHonorsResolvedRemaps(t *testing.T) {
suggestions := []BindingOverride{{Layout: DesktopKeyboardLayout, Scope: "global", Command: "tentacle/remote/menu", Keys: []string{"Cthulhu+Ctrl+r"}}}
remap := []BindingOverride{{Layout: DesktopKeyboardLayout, Scope: "global", Command: "tentacle/remote/menu", Keys: []string{"Cthulhu+Alt+r"}}}
for name, overrides := range map[string][]BindingOverride{
"suggested": MergeTentacleBindings(nil, suggestions),
"remapped": MergeTentacleBindings(remap, suggestions),
} {
t.Run(name, func(t *testing.T) {
resolved, err := ResolveCommandBindings(DesktopKeyboardLayout, false, overrides)
if err != nil {
t.Fatal(err)
}
local := CommandBindings{}
for chord, command := range resolved {
if command == "tentacle/remote/menu" {
local[chord] = command
}
}
router := newTestCaptureRouter(t, local)
modifierCode, modifierSymbol := codeControl, leftControlKeySymbol
if name == "remapped" {
modifierCode, modifierSymbol = codeAlt, uint32(0xffe9)
}
runCaptureSteps(t, router, []captureStep{
{press(codeInsert, testInsertSymbol), remote(down(codeInsert))},
{press(modifierCode, modifierSymbol), remote(down(modifierCode))},
{press(codeR, 'r'), RemoteCaptureResult{Remote: []RemoteRepeatTransition{up(modifierCode), up(codeInsert)}, Local: "tentacle/remote/menu"}},
})
// Local reader commands are not Remote commands: Cthulhu+t
// reaches the remote screen reader.
other := newTestCaptureRouter(t, local)
runCaptureSteps(t, other, []captureStep{
{press(codeInsert, testInsertSymbol), remote(down(codeInsert))},
{press(28, 't'), remote(down(28))},
})
})
}
}
// The normalizer and router compose: a classic repeat pair inside a local
// chord produces no remote traffic and no second local command.
func TestBehaviorRemoteCaptureComposesWithNormalizer(t *testing.T) {
normalizer := newTestNormalizer(t)
router := newTestCaptureRouter(t, CommandBindings{{Symbol: 'r', Modifiers: controlModifier}: "tentacle/remote/menu"})
symbols := map[uint8]uint32{codeControl: leftControlKeySymbol, codeR: 'r'}
var remoteOut []RemoteRepeatTransition
var commands []Command
now := time.Unix(1, 0)
for _, event := range []Event{
{Keycode: codeControl, Pressed: true, Timestamp: 10},
{Keycode: codeR, Pressed: true, Timestamp: 20},
{Keycode: codeR, Pressed: false, Timestamp: 700},
{Keycode: codeR, Pressed: true, Timestamp: 700},
{Keycode: codeR, Pressed: false, Timestamp: 800},
{Keycode: codeControl, Pressed: false, Timestamp: 0},
} {
transitions, err := normalizer.Observe(event, now)
if err != nil {
t.Fatal(err)
}
for _, transition := range transitions {
result, err := router.Route(RemoteCaptureKey{Keycode: transition.Keycode, Symbol: symbols[transition.Keycode], Pressed: transition.Pressed, Repeat: transition.Repeat})
if err != nil {
t.Fatal(err)
}
remoteOut = append(remoteOut, result.Remote...)
if result.Local != "" {
commands = append(commands, result.Local)
}
}
}
if want := []RemoteRepeatTransition{down(codeControl), up(codeControl)}; !reflect.DeepEqual(remoteOut, want) {
t.Fatalf("remote = %+v, want %+v", remoteOut, want)
}
if !reflect.DeepEqual(commands, []Command{"tentacle/remote/menu"}) {
t.Fatalf("commands = %v", commands)
}
}