Skip to content

Instantly share code, notes, and snippets.

@fauxpark
Last active November 29, 2024 08:21
Show Gist options
  • Save fauxpark/010dcf5d6377c3a71ac98ce37414c6c4 to your computer and use it in GitHub Desktop.
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

Having just read the entire thread can I ask if @drashna 's patch in on track to be integrated into the upstream main/master?

If I am following then it seems like it can be used along with SHARED_EP to activate some basic functionality such as Globe+E for Emojis and/or it can be used as a layer activation key along with a layer which "emulates" the other functionality a "real" fn key provides.

This seems worth landing?

@HVR88
Copy link

HVR88 commented Nov 1, 2023

@fauxpark's PR was merged to the develop branch here: qmk/qmk_firmware#22256

If I am following then it seems like it can be used along with SHARED_EP to activate some basic functionality such as Globe+E for Emojis

Yes.

and/or it can be used as a layer activation key along with a layer which "emulates" the other functionality a "real" fn key provides.

Not by simply using LT alone, because it doesn't include some of the changes @drashna made. Namely, the globe key isn't defined as a basic keycode. You should be able to do it by including a custom condition in process_record_user() to check for the two key press types (tap and hold) on a placeholder, and then manually send the globe key on the tap.

Example placeholder:

#define KC_MYGLOBELAYERKEY LT(0, KC_ESC)

@lordpixel23
Copy link

Right. That's my question. I believe @fauxpark made reference to not defining a KC_ code yet and @drashna did take that additional step in their patch. So I am trying to understand if the plan is to make a second merge with additional changes or if there is some reason for not adding this as a basic keycode?

@fauxpark
Copy link
Author

fauxpark commented Nov 2, 2023

I didn't add a keycode because this usage only really does something useful on macOS, and even then you still need an Apple VID/PID pair for it to work properly, as has been mentioned several times already, and in my PR. So it's not like you can use the Configurator due to this restriction anyway; for now you can make a custom keycode like so.

@lordpixel23
Copy link

Thanks for the summary. I did read the whole history back to 2017 so I have a decent handle on it I think. I get that if you want a true Apple fn key which works like a native keyboard the VID/PID issue is still there and I suppose that means support in the GUI tools is not appropriate as the restrictions will confuse people who haven't followed the whole context. I do see value in being able to do the Globe+E shortcuts etc.

Anyway, thanks for the link to that comment link. That makes things clearer.

@HVR88
Copy link

HVR88 commented Nov 3, 2023

I didn't add a keycode because this usage only really does something useful on macOS, and even then you still need an Apple VID/PID pair for it to work properly,

There are many other codes already in QMK's basic list that only work on one platform, as a comparison.

IMO, the VID/PID issue isn't really relevant to the Globe/Fn key - it's relevant to Mac OS's support of Function keys (F1 to F12). Globe/Fn shortcuts work without Apple VID/PID (Emoji without combo, Globe-E, Globe-H, Globe-N etc.) It's the function keys that don't work without Apple's VID/PID - F1/F2 for brightness, etc. And with or without Apple VID/PID, you can't get the same behavior as a real Apple keyboard where Globe/Fn+F1 = real F1 so that's moot.

This is why Apple's accessories implementation document spells out the consumer code for others to use as Globe, but doesn't mention anything about its use with the Function keys.

I'm actually using Apple VID/PID on my keyboard right now and for only one reason. To make sure that screen brightness control on F1 and F2 work with both internal and external display. Otherwise everything else can be done with codes built into QMK and layer shifts.

@lordpixel23
Copy link

I wasn't going to say anything further but I think @HVR88 makes the case well. There is value in adding the key code to to make Globe available as an extra key code separately from the issue of VID/PID and function keys. It enables one to access most of the functionality.

@chrismanderson
Copy link

I was catching up on this thread and wanted to sum up a bit as it's exciting to see some progress being made, but understandably a bit tougher to follow the current state of this issue.

  1. Recently, Apple added a consumer code for the Globe Key (0x029D) to the keyboard consumer page for keyboards on Apple devices. This means that there is now an actual key code for the Globe key that can be used in QMK and that macOS can recognize. This means that the original patch in this gist is no longer required.

  2. To use the new Globe key key code,

    1. Apply the code provided by @drashna (drashna/qmk_firmware@fb94bc4)
    2. Set KEYBOARD_SHARED_EP = yes in your rules.mk file for your specific keyboard.
    3. Add KC_GLOBE to your keymap file
  3. In doing so, some of the Globe/Fn keyboard shortcuts will work properly, but not all. Namely, the F1-12 keys do not work as expected. There are other additional not well understood quirks, such as the emoji picker displaying no matter how long you hold the custom Globe key (where with a 'real' Globe key, the picker does not display if you hold it)

  4. In order to support the special functions of the F-keys, you still need to use the custom VID/PID linked at the top of the gist.

  5. Some of the functionality provided by @drashna has been merged into QMK's develop branch (qmk/qmk_firmware#22256). Notably, this does not include the actual key code you can use in your keymap.

  6. You cannot use KC_GLOBE as a layer modifier key because layer-tap/mod-tap only support basic key codes.

@HVR88
Copy link

HVR88 commented Nov 7, 2023

Mostly.

  1. To use the new Globekey code,
    i. Apply the code provided by @fauxpark or pull the master branch clone provided by @drashna
    iii. Add KC_GLOBE to your keymap file only for @drashna's branch

  2. Fn shortcuts work - it would be good to find/list any that don't (F1-12 not included, see #4). Emoji picker shows up on key-release after a long hold of the Fn key, which doesn't match behavior of real Apple keyboard (VID/PID has no effect on this)

  3. F1-12 keys don't work at all without Apple VID/PID and this has nothing to do with the Fn/Globe functionality

  4. @fauxpark's code has been merged into QMK's development branch

  5. You can't use KC_GLOBE as a layer modifier with @fauxpark's merged code because there is no KC_GLOBE defined as a basic keycode. You can use KC_GLOBE as a layer modifier with @drashna's branch because that keycode is defined.

  6. The overall behavior using 0x029D seems to be working as Apple intended - macOS bugs notwithstanding.

@volsk
Copy link

volsk commented Oct 18, 2024

Decided to take another look at the whole F-key thing using Apple VID/PID and ran through the 4 PIDs I was able to find for Apple external keyboards with FN/Globe keys - mainly because I wasn't previously able to get the F-keys to produce their special functions. SPOILER: PID 0x0267 won't work on my system.

  • Using @drashna's code base with the consumer code for the Globe key.
  • Testing on M1 Macbook Air 2020 (first release), running macOS 14 (Sonoma - latest Beta 14.1)
  • this Macbook Air has a microphone icon on the F4 key of its built-in keyboard which brings up Spotlight

I'm not specifying any version string along with VID/PID Maybe that makes a difference to some of this.

QMK on Keychron Q3 - disabled Keychron's key processing hackery

Keychron VID/PID - no special functions on F-Keys. They come up as regular F-Keys

Apple 0x0267 (Magic Keyboard ANSI) - same as above - doesn't work with any special functions

Apple 0x0220 (Aluminum Keyboard ANSI) - All F-keys produce their special functions

  • F1/F2 Brightness work on Internal and External display - (internal only when external not connected)
  • Globe + F-Key does not produce a regular F-Key - it just continues to do the special function
  • Globe + N shows notification center
  • Globe + H shows desktop
  • Globe + E shows emoji picker
  • F4 brings up LaunchPad (not spotlight like internal keyboard)

Apple 0x024f (Aluminum Keyboard ANSI) - Works as above Apple 0x021d (Aluminum MINI Keyboard ANSI) - Works as above

Other keyboards Microsoft Internet Keyboard c. 1999 - no special functions on F-Keys. They come up as regular F-Keys Random Logitec - same as above, normal F-keys

An interesting note: The QMK built-in keycodes for Brightness (KC_BRID and KC_BRIU, consumer codes 0x006F and 0x0070) don't work on my external monitor, only the built-in display of the MBA.

This all works, with capslock mapped to KC_GLOBE, but window tiling with Globe + Ctrl + Arrow does not work. Interestingly Globe + Ctrl + F does work for full window. Any suggestions?

@lordpixel23
Copy link

I just got a new keyboard yesterday with room for two function keys and by coincidence this thread comes back to life. I am still trying to wrap my head around why nothing has been upstreamed in the last year. Are you saying all o the above can work with @drashna's patch alone?

@lordpixel23
Copy link

So I've updated Drasha's patch to work with the latest code from master branch. I can post a gist later if anyone is interested.

I bound it to a fn key on my keyboard and I can see the OS recognizes it. Fn lights up in Keyboard Viewer and if I tap it quickly it starts dictation. But that's about all it does. I have not defined a VID/PID so I was not expecting F Key support. But nothing is working at all. e.g. Globe+N does nothing but it does on my laptop's built in keyboard. Which is an old intel MacBook Pro.

Did I miss anything obvious?

@HVR88
Copy link

HVR88 commented Oct 20, 2024

Did you miss this comment earlier in the thread about SHARED_EP?

I've just tested with @drashna's patch. That was it, SHARED_EP gets me the same behavior you've seen. Globe/Fn + OTHER_KEYS works as mostly as expected. With F1-12 not working and Globe/Fn + Backspace not performing a DEL. This is also how this was described in the ZMK threads.

@lordpixel23
Copy link

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

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