Files
cthulhu/internal/input/remote_key_ledger.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
}