Forward numeric keypad identities with Num Lock enabled and preserve held-key pairing across lock changes. Suppress local lock feedback during remote capture so it cannot interrupt incoming speech. Add debug speech and capture-end diagnostics, expand private keypad checks, and repair the suspension probe source list. Validated affected Go packages, focused remote speech race checks, the private X11 Remote input harness, and a reader build. Live physical keyboard, audible PowerShell speech, Braille, and disconnect-cause verification remain pending; the active reader session was left running.
138 lines
4.7 KiB
Go
138 lines
4.7 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/input"
|
|
"git.stormux.org/storm/cthulhu/internal/tentacle"
|
|
"git.stormux.org/storm/cthulhu/internal/tentaclewire"
|
|
)
|
|
|
|
// remoteCaptureEndTimeout bounds waiting for the native owner, which ends a
|
|
// capture only between chords.
|
|
const remoteCaptureEndTimeout = 2 * time.Second
|
|
|
|
// remoteCaptureInput is the native exclusive-capture owner (Xorg only).
|
|
type remoteCaptureInput interface {
|
|
BeginRemoteCapture(context.Context, string) (uint64, <-chan input.RemoteCaptureEvent, error)
|
|
EndRemoteCapture(context.Context, uint64) error
|
|
}
|
|
|
|
// start_remote_capture attaches the controller capture owner for a session
|
|
// installed with keyboard-capture. Without a native capture owner remote.focus
|
|
// stays owner-unavailable. Capture is entered only on a request for the current
|
|
// controller lease while the reader is unlocked, not handed off and showing no
|
|
// reader UI. Revocation, policy or the core escape end it; ending releases all
|
|
// forwarded keys through the stream before the local focus event.
|
|
func (runtime *tentacleRuntime) start_remote_capture(ctx context.Context, session *tentacle.Session) <-chan struct{} {
|
|
done := make(chan struct{})
|
|
capture, ok := runtime.input.(remoteCaptureInput)
|
|
owner := runtime.output
|
|
if !ok || owner == nil || owner.gate == nil || runtime.remotePolicy == nil ||
|
|
!slices.Contains(session.RemoteGrants(), "keyboard-capture") {
|
|
close(done)
|
|
return done
|
|
}
|
|
requests := session.EnableRemoteCapture()
|
|
allowed := func() bool {
|
|
owner.gate.mutex.Lock()
|
|
defer owner.gate.mutex.Unlock()
|
|
return !owner.gate.Locked() && !owner.gate.Suspended() && !owner.gate.InternalUIActive()
|
|
}
|
|
prefix := "tentacle/" + session.Identity() + "/"
|
|
go func() {
|
|
defer close(done)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
stop := context.AfterFunc(session.OutputContext(), cancel)
|
|
defer stop()
|
|
var active uint64
|
|
var lease tentaclewire.RemoteLease
|
|
var events <-chan input.RemoteCaptureEvent
|
|
var leaseDone <-chan struct{}
|
|
end := func() {
|
|
if active == 0 {
|
|
return
|
|
}
|
|
// Use a fresh bounded context: ending must not depend on the lease
|
|
// or reader context that may already be cancelled.
|
|
endCtx, endCancel := context.WithTimeout(context.Background(), remoteCaptureEndTimeout)
|
|
_ = capture.EndRemoteCapture(endCtx, active)
|
|
endCancel()
|
|
}
|
|
defer end()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-leaseDone:
|
|
leaseDone = nil
|
|
end()
|
|
case request := <-requests:
|
|
current := request.Context != nil && request.Context.Err() == nil
|
|
if current {
|
|
_, err := session.RemoteContext(request.Lease, "keyboard-capture")
|
|
current = err == nil
|
|
}
|
|
if !request.Remote {
|
|
if active != 0 && lease == request.Lease {
|
|
end() // The stream reports "requested" when it closes.
|
|
} else if current {
|
|
_ = session.PublishRemoteFocus(request.Lease, tentaclewire.RemoteFocusLocal, "requested")
|
|
}
|
|
continue
|
|
}
|
|
if !current {
|
|
continue
|
|
}
|
|
if active != 0 {
|
|
if lease == request.Lease {
|
|
_ = session.PublishRemoteFocus(lease, tentaclewire.RemoteFocusRemote, "requested")
|
|
continue
|
|
}
|
|
end()
|
|
}
|
|
if !allowed() {
|
|
_ = session.PublishRemoteFocus(request.Lease, tentaclewire.RemoteFocusLocal, "local")
|
|
continue
|
|
}
|
|
id, stream, err := capture.BeginRemoteCapture(request.Context, prefix)
|
|
if err != nil {
|
|
_ = session.PublishRemoteFocus(request.Lease, tentaclewire.RemoteFocusLocal, "unavailable")
|
|
runtime.report("Remote keyboard control is unavailable.")
|
|
continue
|
|
}
|
|
runtime.report("Keyboard controls the remote computer. Press F11 to return.")
|
|
active, lease, events, leaseDone = id, request.Lease, stream, request.Context.Done()
|
|
if session.PublishRemoteFocus(lease, tentaclewire.RemoteFocusRemote, "requested") != nil {
|
|
end()
|
|
}
|
|
case event, open := <-events:
|
|
if !open {
|
|
active, events, leaseDone = 0, nil, nil
|
|
continue
|
|
}
|
|
if event.Ended != "" {
|
|
runtime.logger.Debug("remote keyboard capture ended", "reason", event.Ended)
|
|
_ = session.PublishRemoteFocus(lease, tentaclewire.RemoteFocusLocal, event.Ended)
|
|
runtime.report("Keyboard returned to this computer.")
|
|
continue
|
|
}
|
|
key := tentaclewire.RemoteCapturedKey{Code: int(event.Key.Code), Name: event.Key.Name, Pressed: event.Key.Pressed, Repeat: event.Key.Repeat}
|
|
if session.PublishRemoteInput(lease, key) != nil && key.Pressed {
|
|
// A retired lease cannot carry keys. Releases continue to
|
|
// drain the stream so the native owner never blocks.
|
|
end()
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
return done
|
|
}
|