Skip to content

Instantly share code, notes, and snippets.

@fauxpark
Last active September 30, 2026 13:21
Show Gist options
  • Select an option

  • Save fauxpark/010dcf5d6377c3a71ac98ce37414c6c4 to your computer and use it in GitHub Desktop.

Select an option

Save fauxpark/010dcf5d6377c3a71ac98ce37414c6c4 to your computer and use it in GitHub Desktop.
QMK Apple Fn

QMK Apple Fn Key

This patch adds support for the Apple Fn key, which unlike most keyboards with Fn keys, is actually sent over the wire. It works by repurposing the reserved byte in the keyboard report to represent the KeyboardFn usage of the AppleVendor Top Case usage page. When the Fn key is pressed, the value of this byte becomes 1.

To apply this patch, download the below file, cd to your qmk_firmware repository in your preferred terminal, and run git apply /path/to/applefn.patch. Then, add the QK_APPLE_FN keycode (or AP_FN for short) to your keymap.

There are a couple of caveats to this implementation that are important to be aware of. Firstly, it is not compatible with NKRO, as QMK's NKRO report format has no reserved byte - it is part of the 6KRO report for compatibility with the HID boot protocol. Thus you must set NKRO_ENABLE = no in your keymap's rules.mk. You will also need to redefine the USB Vendor and Product IDs in your keymap's config.h to that of a genuine Apple keyboard* in order for macOS to recognise the Fn key:

#undef VENDOR_ID
#define VENDOR_ID 0x05AC
#undef PRODUCT_ID
#define PRODUCT_ID <pid>

This is the primary reason this patch has not been integrated into upstream QMK - Apple would probably not be too happy about others using their vendor ID, and a feature that relies on the VID/PID pair being set to a specific value is not particularly ideal anyway.

See qmk/qmk_firmware#2179 for a little more info and discussion.

* It appears that the functionality of certain F keys can differ depending on the PID, likely because they have evolved over time on real Apple keyboards.

diff --git a/builddefs/common_features.mk b/builddefs/common_features.mk
index 18f8b0bbfc..4ef3e230e4 100644
--- a/builddefs/common_features.mk
+++ b/builddefs/common_features.mk
@@ -878,6 +878,10 @@ ifeq ($(strip $(JOYSTICK_ENABLE)), yes)
endif
endif
+ifeq ($(strip $(APPLE_FN_ENABLE)), yes)
+ OPT_DEFS += -DAPPLE_FN_ENABLE
+endif
+
USBPD_ENABLE ?= no
VALID_USBPD_DRIVER_TYPES = custom vendor
USBPD_DRIVER ?= vendor
diff --git a/data/constants/keycodes/keycodes_0.0.2_applefn.hjson b/data/constants/keycodes/keycodes_0.0.2_applefn.hjson
new file mode 100644
index 0000000000..1378413a9e
--- /dev/null
+++ b/data/constants/keycodes/keycodes_0.0.2_applefn.hjson
@@ -0,0 +1,11 @@
+{
+ "keycodes": {
+ "0x5300": {
+ "group": "apple_fn",
+ "key": "QK_APPLE_FN",
+ "aliases": [
+ "AP_FN"
+ ]
+ }
+ }
+}
diff --git a/quantum/action.c b/quantum/action.c
index 6368f7398c..d9bd34f681 100644
--- a/quantum/action.c
+++ b/quantum/action.c
@@ -555,6 +555,18 @@ void process_action(keyrecord_t *record, action_t action) {
}
break;
#endif // EXTRAKEY_ENABLE
+#ifdef APPLE_FN_ENABLE
+ /* Apple Fn */
+ case ACT_APPLE_FN:
+ if (event.pressed) {
+ add_apple_fn(keyboard_report);
+ send_keyboard_report();
+ } else {
+ del_apple_fn(keyboard_report);
+ send_keyboard_report();
+ }
+ break;
+#endif
/* Mouse key */
case ACT_MOUSEKEY:
register_mouse(action.key.code, event.pressed);
@@ -1196,6 +1208,9 @@ void debug_action(action_t action) {
case ACT_USAGE:
ac_dprintf("ACT_USAGE");
break;
+ case ACT_APPLE_FN:
+ dprint("ACT_APPLE_FN");
+ break;
case ACT_MOUSEKEY:
ac_dprintf("ACT_MOUSEKEY");
break;
diff --git a/quantum/action_code.h b/quantum/action_code.h
index d9a575b518..f347010c8d 100644
--- a/quantum/action_code.h
+++ b/quantum/action_code.h
@@ -53,7 +53,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
* ACT_SWAP_HANDS(0110):
* 0110|xxxx| keycode Swap hands (keycode on tap, or options)
*
- * 0111|xxxx xxxx xxxx (reserved)
+ * ACT_APPLE_FN(0111):
+ * 0111|0000|0000|0000 Apple Fn
*
* Layer Actions(10xx)
* -------------------
@@ -95,6 +96,8 @@ enum action_kind_id {
ACT_MOUSEKEY = 0b0101,
/* One-hand Support */
ACT_SWAP_HANDS = 0b0110,
+ /* Apple Fn */
+ ACT_APPLE_FN = 0b0111,
/* Layer Actions */
ACT_LAYER = 0b1000,
ACT_LAYER_MODS = 0b1001,
@@ -182,6 +185,7 @@ enum usage_pages {
#define ACTION_USAGE_SYSTEM(id) ACTION(ACT_USAGE, PAGE_SYSTEM << 10 | (id))
#define ACTION_USAGE_CONSUMER(id) ACTION(ACT_USAGE, PAGE_CONSUMER << 10 | (id))
+#define ACTION_APPLE_FN() ACTION(ACT_APPLE_FN, 0)
#define ACTION_MOUSEKEY(key) ACTION(ACT_MOUSEKEY, key)
/** \brief Layer Actions
diff --git a/quantum/keycodes.h b/quantum/keycodes.h
index bbf10da36d..b8e894a399 100644
--- a/quantum/keycodes.h
+++ b/quantum/keycodes.h
@@ -310,6 +310,7 @@ enum qk_keycode_defines {
KC_RIGHT_SHIFT = 0x00E5,
KC_RIGHT_ALT = 0x00E6,
KC_RIGHT_GUI = 0x00E7,
+ QK_APPLE_FN = 0x5300,
QK_SWAP_HANDS_TOGGLE = 0x56F0,
QK_SWAP_HANDS_TAP_TOGGLE = 0x56F1,
QK_SWAP_HANDS_MOMENTARY_ON = 0x56F2,
@@ -938,6 +939,7 @@ enum qk_keycode_defines {
KC_RGUI = KC_RIGHT_GUI,
KC_RCMD = KC_RIGHT_GUI,
KC_RWIN = KC_RIGHT_GUI,
+ AP_FN = QK_APPLE_FN,
SH_TOGG = QK_SWAP_HANDS_TOGGLE,
SH_TT = QK_SWAP_HANDS_TAP_TOGGLE,
SH_MON = QK_SWAP_HANDS_MOMENTARY_ON,
@@ -1406,6 +1408,7 @@ enum qk_keycode_defines {
#define IS_CONSUMER_KEYCODE(code) ((code) >= KC_AUDIO_MUTE && (code) <= KC_LAUNCHPAD)
#define IS_MOUSE_KEYCODE(code) ((code) >= KC_MS_UP && (code) <= KC_MS_ACCEL2)
#define IS_MODIFIER_KEYCODE(code) ((code) >= KC_LEFT_CTRL && (code) <= KC_RIGHT_GUI)
+#define IS_APPLE_FN_KEYCODE(code) ((code) >= QK_APPLE_FN && (code) <= QK_APPLE_FN)
#define IS_SWAP_HANDS_KEYCODE(code) ((code) >= QK_SWAP_HANDS_TOGGLE && (code) <= QK_SWAP_HANDS_ONE_SHOT)
#define IS_MAGIC_KEYCODE(code) ((code) >= QK_MAGIC_SWAP_CONTROL_CAPS_LOCK && (code) <= QK_MAGIC_TOGGLE_ESCAPE_CAPS_LOCK)
#define IS_MIDI_KEYCODE(code) ((code) >= QK_MIDI_ON && (code) <= QK_MIDI_PITCH_BEND_UP)
diff --git a/quantum/keymap_common.c b/quantum/keymap_common.c
index 9a67fad278..36a0b309be 100644
--- a/quantum/keymap_common.c
+++ b/quantum/keymap_common.c
@@ -70,6 +70,11 @@ action_t action_for_keycode(uint16_t keycode) {
case KC_AUDIO_MUTE ... KC_LAUNCHPAD:
action.code = ACTION_USAGE_CONSUMER(KEYCODE2CONSUMER(keycode));
break;
+#endif
+#ifdef APPLE_FN_ENABLE
+ case QK_APPLE_FN:
+ action.code = ACTION_APPLE_FN();
+ break;
#endif
case KC_MS_UP ... KC_MS_ACCEL2:
action.code = ACTION_MOUSEKEY(keycode);
diff --git a/tmk_core/protocol/report.c b/tmk_core/protocol/report.c
index 1ba3be4604..b53d3f687b 100644
--- a/tmk_core/protocol/report.c
+++ b/tmk_core/protocol/report.c
@@ -299,3 +299,13 @@ __attribute__((weak)) bool has_mouse_report_changed(report_mouse_t* new_report,
return changed;
}
#endif
+
+#ifdef APPLE_FN_ENABLE
+void add_apple_fn(report_keyboard_t* keyboard_report) {
+ keyboard_report->reserved = 1;
+}
+
+void del_apple_fn(report_keyboard_t* keyboard_report) {
+ keyboard_report->reserved = 0;
+}
+#endif
diff --git a/tmk_core/protocol/report.h b/tmk_core/protocol/report.h
index 9d415a3bfd..8c65b40386 100644
--- a/tmk_core/protocol/report.h
+++ b/tmk_core/protocol/report.h
@@ -348,6 +348,11 @@ void clear_keys_from_report(report_keyboard_t* keyboard_report);
bool has_mouse_report_changed(report_mouse_t* new_report, report_mouse_t* old_report);
#endif
+#ifdef APPLE_FN_ENABLE
+void add_apple_fn(report_keyboard_t* keyboard_report);
+void del_apple_fn(report_keyboard_t* keyboard_report);
+#endif
+
#ifdef __cplusplus
}
#endif
diff --git a/tmk_core/protocol/usb_descriptor.c b/tmk_core/protocol/usb_descriptor.c
index e215c90900..e38c0d37f7 100644
--- a/tmk_core/protocol/usb_descriptor.c
+++ b/tmk_core/protocol/usb_descriptor.c
@@ -75,10 +75,22 @@ const USB_Descriptor_HIDReport_Datatype_t PROGMEM KeyboardReport[] = {
HID_RI_REPORT_COUNT(8, 0x08),
HID_RI_REPORT_SIZE(8, 0x01),
HID_RI_INPUT(8, HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE),
+
+#ifdef APPLE_FN_ENABLE
+ HID_RI_USAGE_PAGE(8, 0xFF), // AppleVendor Top Case
+ HID_RI_USAGE(8, 0x03), // KeyboardFn
+ HID_RI_LOGICAL_MINIMUM(8, 0x00),
+ HID_RI_LOGICAL_MAXIMUM(8, 0x01),
+ HID_RI_REPORT_COUNT(8, 0x01),
+ HID_RI_REPORT_SIZE(8, 0x08),
+ HID_RI_INPUT(8, HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE),
+#else
// Reserved (1 byte)
HID_RI_REPORT_COUNT(8, 0x01),
HID_RI_REPORT_SIZE(8, 0x08),
HID_RI_INPUT(8, HID_IOF_CONSTANT),
+#endif
+
// Keycodes (6 bytes)
HID_RI_USAGE_PAGE(8, 0x07), // Keyboard/Keypad
HID_RI_USAGE_MINIMUM(8, 0x00),
diff --git a/tmk_core/protocol/vusb/vusb.c b/tmk_core/protocol/vusb/vusb.c
index d74f375f66..2ade1350ad 100644
--- a/tmk_core/protocol/vusb/vusb.c
+++ b/tmk_core/protocol/vusb/vusb.c
@@ -417,10 +417,22 @@ const PROGMEM uchar keyboard_hid_report[] = {
0x95, 0x08, // Report Count (8)
0x75, 0x01, // Report Size (1)
0x81, 0x02, // Input (Data, Variable, Absolute)
+
+#ifdef APPLE_FN_ENABLE
+ 0x05, 0xFF, // Usage Page (AppleVendor Top Case)
+ 0x09, 0x03, // Usage (KeyboardFn)
+ 0x15, 0x00, // Logical Minimum (0)
+ 0x25, 0x01, // Logical Maximum (1)
+ 0x95, 0x01, // Report Count (1)
+ 0x75, 0x08, // Report Size (8)
+ 0x81, 0x02, // Input (Data, Variable, Absolute)
+#else
// Reserved (1 byte)
0x95, 0x01, // Report Count (1)
0x75, 0x08, // Report Size (8)
0x81, 0x03, // Input (Constant)
+#endif
+
// Keycodes (6 bytes)
0x05, 0x07, // Usage Page (Keyboard/Keypad)
0x19, 0x00, // Usage Minimum (0)
@lordpixel23

Copy link
Copy Markdown

Yes, thank you. SHARED_EP was the missing step. Now working as expected. I will make an up to date gist later.

Now, if only I could get the keyboard setting to let me use this thing as a generic modifier key.

@lordpixel23

Copy link
Copy Markdown

@andruhon

Copy link
Copy Markdown

It seems that on original Apple keyboard globe and arrows have special relationship:
Globe+UP - effectively produces PgUP,
Globe+DOWN - PgDown
Globe+Left - Home
Globe+Right - End

Logically Globe+Ctrl+Up and Globe+Ctrl+Down should be producing Ctrl+PgUp and Ctrl+PgDown and indeed hitting Ctrl+PgUp and Ctrl+PgDown on my QMK keyboard in some circumstances produces the same effect, for example with original Mac keyboard in gogle chrome globe+ctrl+up (or down) does not move the window up, but instead changes the tabs order and hitting ctrl+PgUp (or down) on QMK keyboard makes the same effect, however it never produces effect of composing the window.

Same with text editors and IDE, such as IntelliJ, which have higher control over the keyboard: hitting globe+ctrl+arrow on native Mac keyboard does whatever Ctrl+Home (End, PgDown, PgUp) supposed to do in the IDE.

The conclusion
Today in OS Tahoe Globe+Arrow is somewhat convoluted even with native Mac keyboard.

Other globe functions such as Globe+Ctrl+F in most cases work as expected with your module (but, again, IntelliJ does not care about Globe+Ctrl+F even with native keyboard.)

Composing Window is somewhat new feature on MacOS does not have the same consistency as on MS Windows or, for example in KDE. I remember one Apple Fan was telling me back in 2008 that you don't really need full screen or window composing, Mac OS is perfect without that stuff 🤷‍♂️ .

@madmalkav

Copy link
Copy Markdown

I know this is a forlorn hope, but has anyone tested to see if there is any change when using MacOS 27?

@marceliwac

marceliwac commented Jun 28, 2026 •

Copy link
Copy Markdown

I know this is a forlorn hope, but has anyone tested to see if there is any change when using MacOS 27?

@madmalkav If you're referring just to the patch itself, it should - at least to the extent that it works on previous versions. I've been using it for the past 2 or 3 major versions of MacOS and never had a problem on my OLKB Planck rev. 6 (Drop).

I might have a chance to try it with MacOS 27 in the coming week, but I'll be shocked if anything stops working (I haven't had to re-flash my keyboard in years). Should that be the case, I'll let you know.

Have you tried using the patch with MacOS 26 on your setup? What hardware are you using? And also, what functionality are you after (there seems to be specific use-cases for Fn-key related shortcuts)?

@madmalkav

Copy link
Copy Markdown

Sorry, I didn’t explained myself correctly, I meant if someone tried if MacOS27 changed its behavior so i.e. Command + Globe + Arrows actually work now on 3rd keyboards using this patch

@HVR88

HVR88 commented Sep 30, 2026 •

Copy link
Copy Markdown

Sorry, I didn’t explained myself correctly, I meant if someone tried if MacOS27 changed its behavior so i.e. Command + Globe + Arrows actually work now on 3rd keyboards using this patch

Don’t laugh… You want to know what the solution is? See fauxpark’s first post.

Yeah, really.

The key detail we lost sight of is that Globe and Apple Fn are not the same HID usage. Globe is useful and macOS recognizes it, but it doesn't replace Apple’s original AppleVendorTopCase / KeyboardFn usage (FF/03) for the native Fn processing we’re after. Which we've covered extensively in the past - but there were always issues. Fn didn't work with all the expected shortcuts and using Globe required second layer keys. None of it was transparent to macOS. But...

I went back to fauxpark’s original patch and tested this again on macOS 27. The FF/03 approach works. I don't know when it STARTED working, but it definitely does TODAY. For every shortcut*. The other critical piece is making macOS treat the keyboard as an Apple keyboard obviously - that's always a must.

  • the only two codes that are not automatically handled by the system are DICTATION (F5) and DO_NO_DISTURB (F6) - they just aren't enabled by default on any known Apple VID/PID combination (I think I tested all of them). To get them, use drashna's extra_extra_key module: https://github.com/drashna/qmk_modules/tree/main/extra_extra_key

With those two things together, macOS handles the Fn behavior natively. You don’t need to implement Fn+Arrow, Fn+F1, etc. in the QMK keymap.

I also made one QMK-side change to fauxpark’s original implementation: instead of keeping Apple Fn as the extended QK_APPLE_FN (0x5300), I implemented KC_APPLE_FN as a basic keycode (0xC4). That means it can be used with the QMK constructs that require an 8-bit basic keycode, which was one of the problems we ran into earlier in this thread.

I now have both as separate basic keycodes:

KC_GLOBE    = 0x00C3
KC_APPLE_FN = 0x00C4

KC_GLOBE sends Globe. KC_APPLE_FN drives the reserved byte in the keyboard report that the HID descriptor exposes as FF/03, essentially the mechanism in fauxpark’s original patch.

So, ya: after all of this, the answer was always at the top of the thread - and today, it finally works.

I have modified files and changes to a Keychron Q3 here: https://github.com/HVR88/qmk_bits

@lordpixel23

Copy link
Copy Markdown

I'm going to commit the sin of speaking for others when I don't really know for sure what motivates them, but as I recall a lot of this discussion has been about trying to find a solution which works without Apple VID/PID usage.

The other thing which seems to have stalled out is any hope of getting anything into the base QMK distribution so we do not need to patch source :-/

@HVR88

HVR88 commented Sep 30, 2026 •

Copy link
Copy Markdown

a lot of this discussion has been about trying to find a solution which works without Apple VID/PID usage.

Some. But that's simply not possible. No amount of code on/in any keyboard firmware can change how Apple has implements this.

Fn support, and other vendor-defined HID usages (i.e. non-standard), are strictly enabled by macOS only for specific Apple VID/PID combinations recognized during USB enumeration - they're hard-coded into macOS.

You can get the low-down from the link I posted: https://github.com/HVR88/qmk_bits

On a generic VID/PID, macOS does not provide the Apple FnKeyboardUsageMap / FnFunctionUsageMap.
With Apple VID/PID, macOS applies an appropriate Apple keyboard personality and supplies those
mappings and FF/03 functions as the native Fn modifier.

The other thing which seems to have stalled out is any hope of getting anything into the base QMK distribution so we do not need to patch source :-/

Modules. For some basic stuff anyway. Some changes/additions get pretty niche and/or hacky, so it's unlikely anyone's going to add them into the master branch. And as the working Fn requires Apple VID/PID, there's not much sense in adding that. Globe is there already as KC_AC_NEXT_KEYBOARD_LAYOUT_SELECT which is the only thing Apple officially documents for third party vendors when building keyboards.

Apple's docs actually forbid the FN trickery that QMK does, including trying to work around their proprietary use of Fn for their own keyboards.

@HVR88

HVR88 commented Sep 30, 2026 •

Copy link
Copy Markdown

OK, full disclosure there's ONE possibility of getting around this using any VID/PID, but it requires making and installing a codeless IOKit KEXT, to add the specific VID/PID combinations to the keyboard driver for a real supported keyboard.

Something sort of like this...

<key>IOKitPersonalities</key>
<dict>

    <key>QMK Keyboard 01</key>
    <dict>
        <key>IOProviderClass</key>
        <string>IOHIDInterface</string>

        <key>VendorID</key>
        <integer>13364</integer>
        <key>ProductID</key>
        <integer>1</integer>

        <!-- Apple Fn properties/maps go here -->
    </dict>

    <key>QMK Keyboard 02</key>
    <dict>
        <key>IOProviderClass</key>
        <string>IOHIDInterface</string>

        <key>VendorID</key>
        <integer>13364</integer>
        <key>ProductID</key>
        <integer>2</integer>

        <!-- identical Apple Fn properties/maps -->
    </dict>

    <key>QMK Keyboard 03</key>
    <dict>
        ...
    </dict>

    <!-- etc. through 10 -->

</dict>

If you know the pain in the ass it is to install kexts on modern macOS, you also realize it's much easier and stress-free to just change VID/PID in QMK.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment