146 lines
4.5 KiB
Go
146 lines
4.5 KiB
Go
// 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
|
|
}
|