swiftmacosswiftuiappkitmacos-carbon

Simulating Fn + Control + arrow key events ignores Fn


I want to simulate the pressing of Fn (Globe)+Control+Right Arrow, but I’m encountering an issue where the Fn modifier seems to be ignored, and the system behaves as if only Control+Right Arrow is being pressed.

In macOS, the combination of Fn+Right Arrow (key code 124) is treated as End (key code 119), therefore simulating just Control+End combo is logical to try, and even using that didn’t have the expected behavior. Instead of moving the window to the right edge of the screen on Sequoia, it switches to the next space, which is the default behavior for Control+Right Arrow.

Demo:

import SwiftUI

@main
struct LittleKeypressDemo: App {
  var body: some Scene {
    Window("Keypress Demo", id: "keypress-demo") {
      ContentView()
    }
    .windowStyle(.hiddenTitleBar)
    .windowResizability(.contentSize)
    .windowBackgroundDragBehavior(.enabled)
  }
}

struct ContentView: View {
  var body: some View {
    VStack {
      KeyPressButton(
        icon: "arrowtriangle.right.fill",
        keyCode: 124,
        eventFlags: [.maskSecondaryFn, .maskControl]
      )
      KeyPressButton( // this works
        label: "C",
        keyCode: 8,
        eventFlags: [.maskSecondaryFn, .maskControl]
      )
      KeyPressButton(
        icon: "arrow.down.to.line",
        keyCode: 119,
        eventFlags: [ /* .maskSecondaryFn, */ .maskControl]
      )
    }
    .padding()
  }
}

struct KeyPressButton: View {
  let icon: String?
  let label: String?
  let keyCode: CGKeyCode
  let eventFlags: CGEventFlags

  init(
    icon: String? = nil,
    label: String? = nil,
    keyCode: CGKeyCode,
    eventFlags: CGEventFlags
  ) {
    self.icon = icon
    self.label = label
    self.keyCode = keyCode
    self.eventFlags = eventFlags
  }

  var body: some View {
    Button(action: {
      simulateKeyPress(keyCode: keyCode, eventFlags: eventFlags)
    }) {
      HStack {
        if eventFlags.contains(.maskSecondaryFn) {
          Image(systemName: "globe")
        }
        if eventFlags.contains(.maskControl) {
          Image(systemName: "control")
        }
        if let icon = icon {
          Image(systemName: icon)
        } else if let label = label {
          Text(label)
        }
      }
    }
    .controlSize(.large)
  }
}

func simulateKeyPress(keyCode: CGKeyCode, eventFlags: CGEventFlags) {
  guard let eventDown = CGEvent(
    keyboardEventSource: nil,
    virtualKey: keyCode,
    keyDown: true
  ),
    let eventUp = CGEvent(
      keyboardEventSource: nil,
      virtualKey: keyCode,
      keyDown: false
    ) else {
    print("Error creating CGEvent for keyCode \(keyCode)")
    return
  }
  eventDown.flags = eventFlags
  eventUp.flags = eventFlags
  eventDown.post(tap: .cghidEventTap)
  eventUp.post(tap: .cghidEventTap)
}

(I included Fn+Control+C to show that centering the window works for example.)

Expected behavior: Simulating the key combo Fn+Control+Right Arrow on macOS Sequoia should move the current window to the right half of the screen, and activate the menu item WindowMove & ResizeRight, just like Fn+Control+C does in my code example for WindowCenter.

Questions: What’s the obvious thing I’m missing? Are there alternative approaches or workarounds to correctly simulate this behavior?

Info in Apple’s old documentation article on handling key events didn’t help, nor did this answer.

(Btw., window management is completely irrelevant here. I’m specifically asking about simulating these key presses.)

Any insights or suggestions would be greatly appreciated.


UPDATE: I think I’ve found where the problem is. It looks like .maskSecondaryFn in CGEventFlags corresponds to 0xFF0100000030, but the usage ID from IOHID for real hardware Fn (Globe) key is 0xFF00000003. This mismatch is likely causing the problem during key simulation. The question now is: how can this be used for the purpose of simulating key presses?

Solution

  • The issue is entirely due to a bug in macOS’s event handling system. You can observe the same behavior in macOS’s built-in onscreen Keyboard Viewer – when simulating the Fn + Control + any arrow key combination, it results in incorrect response. Unfortunately, there’s no workaround available until Apple addresses this.