Return the keyboard on Remote shortcuts and announce focus changes
During remote focus a Remote shortcut now ends capture, like F11, instead of running its command, which would race with the focus change. The reader announces when the keyboard starts and stops controlling the remote computer, and the consent wording names the F11 escape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c234cb82a8
commit
477d860fb8
5 files changed
+43
-25
No files matched your search
@@ -105,8 +105,10 @@ func (runtime *tentacleRuntime) start_remote_capture(ctx context.Context, sessio
|
||||
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()
|
||||
@@ -118,6 +120,7 @@ func (runtime *tentacleRuntime) start_remote_capture(ctx context.Context, sessio
|
||||
}
|
||||
if 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}
|
||||
|
||||
@@ -118,7 +118,7 @@ func remote_consent_form(id, role string, grants []string) interaction.Form {
|
||||
labels := map[string]string{
|
||||
"output-observe": "Share local reader output with the remote controller",
|
||||
"remote-present": "Present output from the controlled computer",
|
||||
"keyboard-capture": "Forward local keyboard input to the controlled computer",
|
||||
"keyboard-capture": "Forward local keyboard input to the controlled computer while remote focus is on. Press F11 to return to this computer",
|
||||
"keyboard-inject": "Allow the remote controller to type on this computer",
|
||||
"clipboard-exchange": "Allow receiving clipboard text; send only on explicit local request",
|
||||
"braille-stream": "Exchange remote braille cells and supported display gestures",
|
||||
|
||||
@@ -60,8 +60,8 @@ func (listener *CommandListener) RemoteCaptureActive() bool {
|
||||
|
||||
// BeginRemoteCapture takes an exclusive keyboard grab and returns the session
|
||||
// and its event stream. Only the core escape (F11) and commands whose IDs start
|
||||
// with localPrefix stay local; everything else is forwarded, including Cthulhu
|
||||
// modifier chords. It waits until no key is held. It grants no consent: the
|
||||
// with localPrefix stay local: either returns the keyboard to this computer.
|
||||
// Everything else is forwarded, including Cthulhu modifier chords. It waits until no key is held. It grants no consent: the
|
||||
// caller must hold a live keyboard-capture lease and end capture on revocation.
|
||||
func (listener *CommandListener) BeginRemoteCapture(ctx context.Context, localPrefix string) (uint64, <-chan input.RemoteCaptureEvent, error) {
|
||||
if !strings.HasPrefix(localPrefix, "tentacle/") || !strings.HasSuffix(localPrefix, "/") {
|
||||
@@ -174,7 +174,7 @@ func (owner *commandOwner) end_remote_capture(reason string) {
|
||||
}
|
||||
capture.ending = true
|
||||
for _, transition := range capture.normalizer.Retire() {
|
||||
owner.route_remote(input.RemoteCaptureKey{Keycode: transition.Keycode}, 0)
|
||||
owner.route_remote(input.RemoteCaptureKey{Keycode: transition.Keycode})
|
||||
}
|
||||
for _, transition := range capture.router.Retire().Remote {
|
||||
owner.send_remote(input.RemoteCaptureEvent{Key: &input.RemoteCaptureTransition{Code: transition.Keycode}})
|
||||
@@ -208,7 +208,7 @@ func (owner *commandOwner) send_remote(event input.RemoteCaptureEvent) bool {
|
||||
|
||||
func (owner *commandOwner) flush_remote(now time.Time) {
|
||||
for _, transition := range owner.remote.normalizer.Flush(now) {
|
||||
owner.route_remote(input.RemoteCaptureKey{Keycode: transition.Keycode}, 0)
|
||||
owner.route_remote(input.RemoteCaptureKey{Keycode: transition.Keycode})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,14 +231,14 @@ func (owner *commandOwner) handle_remote_key(key C.CommandKey) {
|
||||
capture.names[transition.Keycode] = name
|
||||
routed.Unsupported = name == ""
|
||||
}
|
||||
owner.route_remote(routed, uint32(key.timestamp))
|
||||
owner.route_remote(routed)
|
||||
}
|
||||
if err != nil && owner.remote != nil {
|
||||
owner.end_remote_capture("local")
|
||||
}
|
||||
}
|
||||
|
||||
func (owner *commandOwner) route_remote(key input.RemoteCaptureKey, timestamp uint32) {
|
||||
func (owner *commandOwner) route_remote(key input.RemoteCaptureKey) {
|
||||
capture := owner.remote
|
||||
if capture == nil {
|
||||
return
|
||||
@@ -264,10 +264,9 @@ func (owner *commandOwner) route_remote(key input.RemoteCaptureKey, timestamp ui
|
||||
owner.end_remote_capture("escape")
|
||||
return
|
||||
}
|
||||
if result.Local != "" && owner.workerContext.Err() == nil {
|
||||
chord := input.CommandChord{Symbol: key.Symbol}
|
||||
if !owner.listener.enqueueSecureCommand(input.CommandEvent{Command: result.Local, Timestamp: timestamp, Chord: chord}) {
|
||||
owner.end_remote_capture("local")
|
||||
}
|
||||
// A Remote shortcut returns the keyboard here instead of running: running it
|
||||
// would race with the focus change. Press it again under local focus.
|
||||
if result.Local != "" {
|
||||
owner.end_remote_capture("requested")
|
||||
}
|
||||
}
|
||||
@@ -113,23 +113,37 @@ func TestNativeRemoteCaptureForwardsBypassesAndEscapes(t *testing.T) {
|
||||
expect("t", true, true)
|
||||
expect("", false, false)
|
||||
expect("", false, false)
|
||||
// Remote's own shortcut stays local and withdraws the forwarded prefix.
|
||||
// Unsupported keys are consumed locally, not forwarded.
|
||||
run("key", "XF86AudioMute")
|
||||
// Remote's own shortcut stays local: it withdraws the forwarded prefix and
|
||||
// returns the keyboard here without running the command.
|
||||
run("key", "Insert+ctrl+r")
|
||||
insert := expect("Insert", true, true)
|
||||
control := expect("Control_L", true, true)
|
||||
if expect("", false, false) != control || expect("", false, false) != insert {
|
||||
t.Fatal("prefix not withdrawn newest first")
|
||||
}
|
||||
if event := next(); event.Ended != "requested" {
|
||||
t.Fatalf("shortcut end event = %+v", event)
|
||||
}
|
||||
select {
|
||||
case event := <-listener.Commands():
|
||||
if event.Command != toggle {
|
||||
t.Fatalf("local command = %s", event.Command)
|
||||
t.Fatalf("shortcut ran %s during remote focus", event.Command)
|
||||
default:
|
||||
}
|
||||
if _, ok := <-events; ok {
|
||||
t.Fatal("capture stream stayed open after shortcut")
|
||||
}
|
||||
for listener.RemoteCaptureActive() {
|
||||
if ctx.Err() != nil {
|
||||
t.Fatal("shortcut did not end capture")
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatal("local Remote shortcut did not dispatch")
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
session, events, err = listener.BeginRemoteCapture(ctx, "tentacle/remote-1/")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Unsupported keys are consumed locally, not forwarded.
|
||||
run("key", "XF86AudioMute")
|
||||
run("key", "F11")
|
||||
if event := next(); event.Ended != "escape" {
|
||||
t.Fatalf("escape event = %+v", event)
|
||||
|
||||
Reference in new issue
Block a user