remote.focus lets a controller keyboard-capture lease ask core to enter or leave remote focus; the reply acknowledges admission only. Core reports the actual state with remote.focus events and forwards routed transitions as remote.input events carrying the source keycode and, on fresh presses, a canonical key name. A full focus mailbox revokes the lease; revocation drains pending focus work. No native capture owner is attached yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
101 lines
3.2 KiB
Go
101 lines
3.2 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package tentaclewire
|
|
|
|
import (
|
|
"encoding/json"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/input"
|
|
)
|
|
|
|
// RemoteFocusQueueSize bounds pending controller focus requests per session.
|
|
const RemoteFocusQueueSize = 4
|
|
|
|
// Focus states and the reasons a controller focus event can report.
|
|
const (
|
|
RemoteFocusLocal = "local"
|
|
RemoteFocusRemote = "remote"
|
|
)
|
|
|
|
var remoteFocusReasons = map[string]bool{
|
|
"requested": true, // The child asked for this state.
|
|
"escape": true, // The core escape returned to local focus.
|
|
"local": true, // Reader policy (lock, handoff, reader UI) ended capture.
|
|
"unavailable": true, // Capture could not be acquired.
|
|
}
|
|
|
|
// RemoteCapturedKey is one controller transition from the reader's exclusive
|
|
// capture, after repeat normalization and local-chord routing. Code is the
|
|
// source Xorg keycode, used only to pair repeats and releases. Name is the
|
|
// canonical unshifted key name and is present only on a fresh press; releases
|
|
// and repeats are matched by Code so a layout change cannot strand a key.
|
|
type RemoteCapturedKey struct {
|
|
Code int `json:"code"`
|
|
Name string `json:"name,omitempty"`
|
|
Pressed bool `json:"pressed"`
|
|
Repeat bool `json:"repeat,omitempty"`
|
|
}
|
|
|
|
func ValidRemoteCapturedKey(key RemoteCapturedKey) bool {
|
|
if key.Code < 8 || key.Code > 255 {
|
|
return false
|
|
}
|
|
if key.Pressed && !key.Repeat {
|
|
return input.ValidRemoteKeyName(key.Name)
|
|
}
|
|
return key.Name == "" && (key.Pressed || !key.Repeat)
|
|
}
|
|
|
|
func (key *RemoteCapturedKey) UnmarshalJSON(data []byte) error {
|
|
var fields map[string]json.RawMessage
|
|
if DecodeStrict(data, &fields) != nil || fields["code"] == nil || fields["pressed"] == nil {
|
|
return ErrProtocol
|
|
}
|
|
type plain RemoteCapturedKey
|
|
var value plain
|
|
if DecodeStrict(data, &value) != nil || !ValidRemoteCapturedKey(RemoteCapturedKey(value)) {
|
|
return ErrProtocol
|
|
}
|
|
*key = RemoteCapturedKey(value)
|
|
return nil
|
|
}
|
|
|
|
// RemoteFocusRequest asks core to enter or leave remote focus for the current
|
|
// controller keyboard-capture lease. The reply acknowledges admission only;
|
|
// the actual state is reported by a remote.focus event.
|
|
type RemoteFocusRequest struct {
|
|
RemoteOperation
|
|
Remote bool `json:"remote"`
|
|
}
|
|
|
|
func ValidRemoteFocusRequest(request RemoteFocusRequest) bool {
|
|
return len(request.Lease) == 32 && request.Epoch != 0 && request.Sequence != 0
|
|
}
|
|
|
|
func (request *RemoteFocusRequest) UnmarshalJSON(data []byte) error {
|
|
var fields map[string]json.RawMessage
|
|
if DecodeStrict(data, &fields) != nil || len(fields) != 4 || fields["lease"] == nil || fields["epoch"] == nil ||
|
|
fields["sequence"] == nil || fields["remote"] == nil {
|
|
return ErrProtocol
|
|
}
|
|
var value struct {
|
|
RemoteOperation
|
|
Remote *bool `json:"remote"`
|
|
}
|
|
if DecodeStrict(data, &value) != nil || value.Remote == nil {
|
|
return ErrProtocol
|
|
}
|
|
result := RemoteFocusRequest{RemoteOperation: value.RemoteOperation, Remote: *value.Remote}
|
|
if !ValidRemoteFocusRequest(result) {
|
|
return ErrProtocol
|
|
}
|
|
*request = result
|
|
return nil
|
|
}
|
|
|
|
func ValidRemoteFocusEvent(focus, reason string) bool {
|
|
return (focus == RemoteFocusLocal || focus == RemoteFocusRemote) && remoteFocusReasons[reason]
|
|
}
|