11

It’s quite easy to detect if Mac has an illuminated keyboard with ioreg at the command line:

ioreg -c IOResources -d 3 | grep '"KeyboardBacklight" =' | sed 's/^.*= //g'

But how can I programmatically get this IOKit boolean property using the latest Swift? I’m looking for some sample code.

Tzar
  • 5,132
  • 4
  • 23
  • 57

2 Answers2

9

I figured out the following with some trial and error:

  • Get the "IOResources" node from the IO registry.
  • Get the "KeyboardBacklight" property from that node.
  • (Conditionally) convert the property value to a boolean.

I have tested this on an MacBook Air (with keyboard backlight) and on an iMac (without keyboard backlight), and it produced the correct result in both cases.

import Foundation
import IOKit

func keyboardHasBacklight() -> Bool {
    let port: mach_port_t
    if #available(macOS 12.0, *) {
        port = kIOMainPortDefault // New name as of macOS 12
    } else {
        port = kIOMasterPortDefault // Old name up to macOS 11
    }
    let service = IOServiceGetMatchingService(port, IOServiceMatching(kIOResourcesClass))
    guard service != IO_OBJECT_NULL else {
        // Could not read IO registry node. You have to decide whether
        // to treat this as a fatal error or not.
        return false
    }
    guard let cfProp = IORegistryEntryCreateCFProperty(service, "KeyboardBacklight" as CFString,
                                                       kCFAllocatorDefault, 0)?.takeRetainedValue(),
          let hasBacklight = cfProp as? Bool
    else {
        // "KeyboardBacklight" property not present, or not a boolean.
        // This happens on Macs without keyboard backlight.
        return false
    }
    // Successfully read boolean "KeyboardBacklight" property:
    return hasBacklight
}
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • I did a little bit of research after you posted and I was wondering why didn’t you use `IOServiceMatching` and `IOServiceGetMatchingServices`? Since I require macOS 12 or later, I will be using the newest `kIOMainPortDefault` instead of older `kIOMasterPortDefault`. Thanks! – Tzar Dec 15 '21 at 13:39
  • 1
    @Tzar: You are right, `IOServiceGetMatchingService(..., IOServiceMatching("IOResources"))` would also work. `IORegistryEntryFromPath(...)` was the first version I managed to get running. I doubt that there is a functional difference, so pick your choice :) – I am still on macOS 11.6, therefore I did not notice the renamed property. You can of course use kIOMainPortDefault instead. – Martin R Dec 15 '21 at 13:50
  • 1
    @Tzar: Rewritten to use kIOMainPortDefault on macOS 12, and IOServiceMatching with a predefined constant instead of a literal string. – Martin R Dec 15 '21 at 14:12
1

As an addendum to Martin’s excellent answer, here are the notes I got from Apple’s Developer Technical Support:

Calling I/O Kit from Swift is somewhat challenging. You have two strategies here:

  • You can wrap the I/O Kit API in a Swift-friendly wrapper and then use that to accomplish your task.
  • You can just go straight to your task, resulting in lots of ugly low-level Swift.

Pulling random properties out of the I/O Registry is not the best path to long-term binary compatibility. Our general policy here is that we only support properties that have symbolic constants defined in the headers (most notably IOKitKeys.h, but there are a bunch of others). KeyboardBacklight has no symbolic constant and thus isn’t supported.


Make sure you code defensively. This property might go away, change meaning, change its type, and so on. Your code must behave reasonable in all such scenarios.


Please make sure you file an enhancement request for a proper API to get this info, making sure to include a high-level description of your overall goal.

Tzar
  • 5,132
  • 4
  • 23
  • 57
  • 1
    The updated code should meet that requirements (use predefined constants if available, code defensively) :) – Martin R Dec 16 '21 at 07:38