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
@@ -61,10 +61,11 @@ Each native transition passes through `RemoteRepeatNormalizer` and
|
||||
local chord. Keys outside the Remote vocabulary are consumed locally.
|
||||
|
||||
Only two things stay local. The core escape, F11 with no modifiers, ends capture
|
||||
on the X thread without waiting for a Tentacle or the network. Commands whose
|
||||
IDs start with the requesting Tentacle's `tentacle/<session>/` prefix, as
|
||||
resolved from current bindings including remaps, are dispatched through the
|
||||
normal command queue. Lock, secure input, XTerm handoff, learn mode, OCR review,
|
||||
on the X thread without waiting for a Tentacle or the network. Shortcuts for
|
||||
commands whose IDs start with the requesting Tentacle's `tentacle/<session>/`
|
||||
prefix, as resolved from current bindings including remaps, also return the
|
||||
keyboard here (reason `requested`) without running the command; running it
|
||||
would race with the focus change. Pressed again under local focus they run. Lock, secure input, XTerm handoff, learn mode, OCR review,
|
||||
a device hierarchy change, an inconsistent stream or a full 512-event queue end
|
||||
capture with reason `local`.
|
||||
|
||||
@@ -81,8 +82,9 @@ handed off and showing no reader UI. Revocation of the lease ends capture.
|
||||
|
||||
A private Xvfb test drives XTEST keys into the real owner with a focused `xev`
|
||||
window as the local application: plain keys, Shift+Tab and a reader-modifier
|
||||
chord are forwarded with correct names and pairing; the Remote shortcut stays
|
||||
local and withdraws its prefix; unsupported keys and F11 are consumed; and the
|
||||
chord are forwarded with correct names and pairing; the Remote shortcut
|
||||
withdraws its prefix and ends capture without running; unsupported keys and F11
|
||||
are consumed; and the
|
||||
window receives none of the captured keys but does receive input afterwards.
|
||||
Lock ends capture with a held modifier released remotely. Five repeated runs
|
||||
passed. App tests with a synthetic owner and a real Go SDK child cover focus
|
||||
|
||||
@@ -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