Summary
Host Cmd (⌘) combinations are delivered to the guest as Super↓ Super↑ <key>↓, so SUPER bindings (⌘C copy, ⌘V paste, ⌘W close, etc.) never fire while the QEMU Cocoa display is in full-grab mode. Even holding ⌘ down for 2+ seconds and then pressing C produces the same premature Super release.
Environment
- Try Omarchy macOS app, Apple Silicon (arm64)
- QEMU 11.1.1 (
c3d48b7d1e89604920e5b81b91140c2ad39a1943)
- Display args:
-display cocoa,gl=es,show-cursor=on,zoom-to-fit=on,full-screen=on,full-grab=on,immersive=on,swap-opt-cmd=off
- Guest: ARCH Linux / Hyprland, virtio-keyboard + virtio-tablet
- Repro app: Chromium (select text then ⌘C), also Foot terminal
What happened
Pressing ⌘C with full-grab on produces, in the guest evdev stream (/dev/input/event0):
KEY code=125 val=1 (Super down)
KEY code=125 val=0 (Super up — premature!)
KEY code=46 val=1 (C down, now unmodified)
KEY code=46 val=0
Guest Hyprland hyprctl binds shows the expected modmask 64+SUPER+C entries, and non-modified keys arrive fine, but no SUPER chord is ever formed. Holding ⌘ for 2s first does not help: Super still goes up before C arrives. Clipboard bridging also does not help because the guest never sees a real copy event.
Root cause (in ui/cocoa.m)
The full-grab HID event tap swallows Cmd flagsChanged before AppKit records the modifier. A later C keyDown still carries no Command bit in modifierFlags, so the modifier-sync block at the top of handleEventLocked: (if modifierFlags lacks Command → qkbd_state_key_event(kbd, KEY_LEFTMETA, false)) emits a guest Super up immediately after the tap's Super down, before the chord key arrives.
Verified layers:
- QMP
display-update {"full-grab": false} doesn't change the broken sequence — the tap's destructive path isn't governed by that switch.
- QMP
input-send-event meta+C also shows the meta lost before C in guest wev (modifiers 00000000 at C).
- Host keyboard remappers (Karabiner/skhd/yabai/BTT) are not involved; killing them doesn't change the sequence.
Suggested fix (we have a working patch)
Track Command state inside cocoa.m itself instead of trusting AppKit modifierFlags while the keyboard is captured:
- Keep explicit
leftCommandGuestDown / rightCommandGuestDown ivars.
- In the
kVK_Command / kVK_RightCommand flagsChanged cases, send explicit idempotent Super down/up (no toggleKey flips).
- Make the "no Command modifier → release meta" sync a no-op while keyboard is captured (
isKeyboardCaptured).
- Reset the ivars in
raiseAllKeys.
With this patch, rebuilding QEMU and replacing the runtime binary changes the guest evdev sequence to:
KEY code=125 val=1 (Super down, held)
KEY code=46 val=1 (C down while Super held)
KEY code=46 val=0
KEY code=125 val=0 (Super up after Cmd release)
⌘C/⌘V and ⌘W-style SUPER bindings then work end-to-end; host/guest clipboard sync works.
We can open a PR with macos/patches/qemu-cocoa-command-chord-hold.patch + build-script wiring if maintainers want it.
Notes
- Original QEMU binary backed up; test was done by replacing the app runtime binary and re-signing ad-hoc with
com.apple.security.hypervisor + audio-input entitlements restored.
Summary
Host Cmd (⌘) combinations are delivered to the guest as
Super↓ Super↑ <key>↓, so SUPER bindings (⌘C copy, ⌘V paste, ⌘W close, etc.) never fire while the QEMU Cocoa display is in full-grab mode. Even holding ⌘ down for 2+ seconds and then pressing C produces the same premature Super release.Environment
c3d48b7d1e89604920e5b81b91140c2ad39a1943)-display cocoa,gl=es,show-cursor=on,zoom-to-fit=on,full-screen=on,full-grab=on,immersive=on,swap-opt-cmd=offWhat happened
Pressing ⌘C with full-grab on produces, in the guest evdev stream (
/dev/input/event0):Guest Hyprland
hyprctl bindsshows the expectedmodmask 64+SUPER+Centries, and non-modified keys arrive fine, but no SUPER chord is ever formed. Holding ⌘ for 2s first does not help: Super still goes up before C arrives. Clipboard bridging also does not help because the guest never sees a real copy event.Root cause (in
ui/cocoa.m)The full-grab HID event tap swallows Cmd
flagsChangedbefore AppKit records the modifier. A later CkeyDownstill carries no Command bit inmodifierFlags, so the modifier-sync block at the top ofhandleEventLocked:(if modifierFlags lacks Command → qkbd_state_key_event(kbd, KEY_LEFTMETA, false)) emits a guest Super up immediately after the tap's Super down, before the chord key arrives.Verified layers:
display-update {"full-grab": false}doesn't change the broken sequence — the tap's destructive path isn't governed by that switch.input-send-eventmeta+C also shows the meta lost before C in guestwev(modifiers00000000at C).Suggested fix (we have a working patch)
Track Command state inside
cocoa.mitself instead of trusting AppKitmodifierFlagswhile the keyboard is captured:leftCommandGuestDown/rightCommandGuestDownivars.kVK_Command/kVK_RightCommandflagsChangedcases, send explicit idempotent Super down/up (notoggleKeyflips).isKeyboardCaptured).raiseAllKeys.With this patch, rebuilding QEMU and replacing the runtime binary changes the guest evdev sequence to:
⌘C/⌘V and ⌘W-style SUPER bindings then work end-to-end; host/guest clipboard sync works.
We can open a PR with
macos/patches/qemu-cocoa-command-chord-hold.patch+ build-script wiring if maintainers want it.Notes
com.apple.security.hypervisor+audio-inputentitlements restored.