// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package input import "errors" const RemoteHeldKeyLimit = 32 var ( ErrRemoteKeyState = errors.New("remote input owner is retired") ErrRemoteKeyUnavailable = errors.New("remote input key is unavailable") ErrRemoteKeyCollision = errors.New("remote input key ownership is ambiguous") ErrRemoteKeyBackend = errors.New("remote input operation failed") ErrRemoteKeyLimit = errors.New("remote input held-key limit exceeded") ) // RemoteKeyBackend belongs to one serial native owner. Set can fail after an // effect reached the server; the ledger therefore reserves before down and // retains ownership until up succeeds. This interface supplies no consent, // watchdog, physical-source tracking or independent crash recovery. Resolve // returns an Xorg destination code in 8..255, never a VK or scan code. type RemoteKeyBackend interface { Resolve(name string) (uint16, error) Held(code uint16) (bool, error) Set(code uint16, pressed bool) error } // RemoteKeyLedger retains destination codes, not source keycodes or relay VKs. // One instance belongs to one native helper lifetime; its caller must serialize // all methods, validate the original lease at delivery and retire on revoke. type RemoteKeyLedger struct { backend RemoteKeyBackend held map[string]uint16 order []string stopped bool } func NewRemoteKeyLedger(backend RemoteKeyBackend) (*RemoteKeyLedger, error) { if backend == nil { return nil, ErrRemoteKeyUnavailable } return &RemoteKeyLedger{backend: backend, held: make(map[string]uint16)}, nil } func (ledger *RemoteKeyLedger) fail(err error) error { ledger.stopped = true return err } func (ledger *RemoteKeyLedger) Apply(name string, pressed bool) error { if ledger == nil || ledger.stopped { return ErrRemoteKeyState } if !ValidRemoteKeyName(name) { return ledger.fail(ErrRemoteKeyUnavailable) } code, owned := ledger.held[name] if owned { if pressed { // Idempotent duplicate downs. Native repeat synthesis/lock policy // remain unavailable; do not generate extra transitions here. return nil } if err := ledger.backend.Set(code, false); err != nil { return ledger.fail(ErrRemoteKeyBackend) } delete(ledger.held, name) for index, held := range ledger.order { if held == name { copy(ledger.order[index:], ledger.order[index+1:]) ledger.order[len(ledger.order)-1] = "" ledger.order = ledger.order[:len(ledger.order)-1] break } } return nil } 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) } if len(ledger.held) == RemoteHeldKeyLimit { return ledger.fail(ErrRemoteKeyLimit) } code, err := ledger.backend.Resolve(name) if err != nil || code < 8 || code > 255 { return ledger.fail(ErrRemoteKeyUnavailable) } for _, other := range ledger.held { if other == code { return ledger.fail(ErrRemoteKeyCollision) } } held, err := ledger.backend.Held(code) if err != nil { return ledger.fail(ErrRemoteKeyBackend) } if held { return ledger.fail(ErrRemoteKeyCollision) } ledger.held[name] = code ledger.order = append(ledger.order, name) if err := ledger.backend.Set(code, true); err != nil { return ledger.fail(ErrRemoteKeyBackend) } return nil } // Retire stops admission permanently and releases only reserved target codes, // in reverse down order. Failed ups retain their identity for explicit cleanup // retry; a successful repeat call has no effects. Cleanup must complete before // destroying the native backend. Never resume a retired ledger. func (ledger *RemoteKeyLedger) Retire() error { if ledger == nil { return ErrRemoteKeyState } ledger.stopped = true failed := false for index := len(ledger.order) - 1; index >= 0; index-- { name := ledger.order[index] if err := ledger.backend.Set(ledger.held[name], false); err != nil { failed = true } else { delete(ledger.held, name) } } remaining := ledger.order[:0] for _, name := range ledger.order { if _, owned := ledger.held[name]; owned { remaining = append(remaining, name) } } clear(ledger.order[len(remaining):]) ledger.order = remaining if failed { return ErrRemoteKeyBackend } return nil }