// 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, unshifted keysym the native owner resolved for a press, with // Num Lock selecting numeric keypad identities; 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 // Unsupported marks a fresh press the Remote key vocabulary cannot name. // It is consumed locally with its repeats and release, never forwarded. Unsupported bool } // RemoteCaptureEvent is one item from a native capture owner to its reader // consumer. Exactly one of Key or Ended is set. Name is present only on a // fresh press. Ended reports why capture stopped: "escape", "requested" or // "local" (reader policy, overflow or an inconsistent native stream); no // further events follow it. type RemoteCaptureEvent struct { Key *RemoteCaptureTransition Ended string } type RemoteCaptureTransition struct { Code uint8 Name string Pressed bool Repeat bool } // RemoteCaptureEscape is the core escape chord: F11 with no modifiers. It is // handled by the native owner without waiting for the network or a Tentacle. var RemoteCaptureEscape = CommandChord{Symbol: 0xffc8} // RemoteCaptureKeyName returns the canonical Remote key name for an unshifted // keysym name (with Num Lock selecting keypad identities), or "" when it has none. func RemoteCaptureKeyName(keysym string) string { switch keysym { case "Prior": keysym = "Page_Up" case "Next": keysym = "Page_Down" case "KP_Prior": keysym = "KP_Page_Up" case "KP_Next": keysym = "KP_Page_Down" } if !ValidRemoteKeyName(keysym) { return "" } return keysym } // 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 key.Unsupported { router.hold(code, remoteCaptureLocal) return RemoteCaptureResult{}, nil } 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 } } }