// 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 }