NativeDisplayBrightness
NativeDisplayBrightness copied to clipboard
Key bindings questions
Is it possible to make it also work on Apple keyboard where F1/F2 are brightness dim. I found no matching kVK key code for these two keys tho.
It is possible to accomplish this, but not by simply using a different key code. The special function keys are handled differently by the system. Some special function keys can be intercepted by passing NX_SYSDEFINED
as eventsOfInterest
to CGEventTapCreate
, but it doesn't work for the brightness keys (at least on my machine).
This could be solved by getting the HID events directly from IOKit, but that would create a whole lot of other problems.
This is what I recommend right now:
- If you're using clover bootloader, try this patch which remaps the brightness keys to F1/F2 permanently
- Alternatively, use FunctionFlip to remap the brightness keys to F1/F2
- If you don't want to install additional software, just use Function+F1/F2 to change the brightness (Function + special key is remapped to F1, F2 etc by default
Hey, thank you for the effort and explanation.
I will test out your suggestions for clover and 3rd party key mapping software.
Although the reason why I am looking into your code is a bit more complex.
By remapping system keys to Function keys won't be ideal since my goal is to use both types of keys on certain softwares.
My google search didn't really find any solution or working example of detecting Brightness keys, I will try to look into IOKit, thank you very much for the info.
Many thanks for the link to FunctionFlip. It's working nicely with NativeDisplayBrightness under Mojave on my Mac mini 2011 and Samsung S24F350FHU monitor.
There is also this reddit post: Use the real brightness keys on a real Apple keyboard for a third-party, external monitor It describes how to remap the keys with Karabiner-Elements.
But I would still prefer to have it natively coded into the application. Did anybody had success with that?
It seems to work for me with cgeventtap on nx_sys events. For reference, monitorControl also does the same thing so it should definitely work. I modified nativebrightness to support this, and also added volume support: it works well for me
//
// AppDelegate.m
// NativeDisplayBrightness
//
// Created by Benno Krauss on 19.10.16.
// Copyright © 2016 Benno Krauss. All rights reserved.
//
#import "AppDelegate.h"
#import "DDC.h"
#import "BezelServices.h"
#import "OSD.h"
#include <dlfcn.h>
@import Carbon;
#pragma mark - constants
static NSString *brightnessValuePreferenceKey = @"brightness";
static NSString *volumeValuePreferenceKey = @"volume";
static const float brightnessStep = 100 / 16.f;
static const float volumeStep = 100 / 16.f;
#pragma mark - variables
void *(*_BSDoGraphicWithMeterAndTimeout)(CGDirectDisplayID arg0, BSGraphic arg1,
int arg2, float v, int timeout) = NULL;
#pragma mark - functions
void set_control(CGDirectDisplayID cdisplay, uint control_id, uint new_value) {
struct DDCWriteCommand command;
command.control_id = control_id;
command.new_value = new_value;
printf("New value %d\n", new_value);
if (!DDCWrite(cdisplay, &command)) {
NSLog(@"E: Failed to send DDC command!");
}
}
static int mk_code(NSEvent *event) {
return (([event data1] & 0xFFFF0000) >> 16);
}
static int mk_flags(NSEvent *event) { return ([event data1] & 0x0000FFFF); }
static int mk_down(NSEvent *event) {
return (((mk_flags(event) & 0xFF00) >> 8)) == 0xA;
}
CGEventRef keyboardCGEventCallback(CGEventTapProxy proxy, CGEventType type,
CGEventRef event, void *refcon) {
NSEvent *nsevent = [NSEvent eventWithCGEvent:event];
if ([nsevent type] != NSSystemDefined)
return event;
if ([nsevent subtype] == 8) {
if (!mk_down(nsevent)) {
return event;
}
int keycode = mk_code(nsevent);
printf("Keycode: %d\n", keycode);
if (keycode == NX_KEYTYPE_SOUND_DOWN) {
printf("Volume down\n");
dispatch_async(dispatch_get_main_queue(), ^{
[(__bridge AppDelegate *)refcon decreaseVolume];
});
return NULL;
}
if (keycode == NX_KEYTYPE_SOUND_UP) {
printf("Volume up\n");
dispatch_async(dispatch_get_main_queue(), ^{
[(__bridge AppDelegate *)refcon increaseVolume];
});
return NULL;
}
if (keycode == NX_KEYTYPE_BRIGHTNESS_DOWN) {
printf("Brightness down\n");
dispatch_async(dispatch_get_main_queue(), ^{
[(__bridge AppDelegate *)refcon decreaseBrightness];
});
return NULL;
}
if (keycode == NX_KEYTYPE_BRIGHTNESS_UP) {
printf("Brightness up\n");
// filter eject key when no modifiers are pressed
dispatch_async(dispatch_get_main_queue(), ^{
[(__bridge AppDelegate *)refcon increaseBrightness];
});
return NULL;
}
}
return event;
}
#pragma mark - AppDelegate
@interface AppDelegate ()
@property(weak) IBOutlet NSWindow *window;
@property(nonatomic) float brightness;
@property(nonatomic) float volume;
@property(strong, nonatomic) dispatch_source_t signalHandlerSource;
@end
@implementation AppDelegate
- (BOOL)_loadBezelServices {
// Load BezelServices framework
void *handle = dlopen("/System/Library/PrivateFrameworks/"
"BezelServices.framework/Versions/A/BezelServices",
RTLD_GLOBAL);
if (!handle) {
NSLog(@"Error opening framework");
return NO;
} else {
_BSDoGraphicWithMeterAndTimeout =
dlsym(handle, "BSDoGraphicWithMeterAndTimeout");
return _BSDoGraphicWithMeterAndTimeout != NULL;
}
}
- (BOOL)_loadOSDFramework {
return [[NSBundle
bundleWithPath:@"/System/Library/PrivateFrameworks/OSD.framework"] load];
}
- (void)_configureLoginItem {
NSURL *bundleURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
LSSharedFileListRef loginItemsListRef =
LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
NSDictionary *properties = @{ @"com.apple.loginitem.HideOnLaunch" : @YES };
LSSharedFileListInsertItemURL(loginItemsListRef, kLSSharedFileListItemLast,
NULL, NULL, (__bridge CFURLRef)bundleURL,
(__bridge CFDictionaryRef)properties, NULL);
}
- (void)_checkTrusted {
BOOL isTrusted = AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef) @{
(__bridge NSString *)kAXTrustedCheckOptionPrompt : @true
});
NSLog(@"istrusted: %i", isTrusted);
}
- (void)_registerGlobalKeyboardEvents {
CFRunLoopRef runloop = (CFRunLoopRef)CFRunLoopGetCurrent();
CGEventMask interestedEvents = CGEventMaskBit(NX_SYSDEFINED);
CFMachPortRef eventTap = CGEventTapCreate(
kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault,
interestedEvents, keyboardCGEventCallback, (__bridge void *)(self));
// by passing self as last argument, you can later send events to this class
// instance
CFRunLoopSourceRef source =
CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);
CFRunLoopAddSource((CFRunLoopRef)runloop, source, kCFRunLoopCommonModes);
CGEventTapEnable(eventTap, true);
}
- (void)_saveBrightness {
[[NSUserDefaults standardUserDefaults] setFloat:self.brightness
forKey:brightnessValuePreferenceKey];
}
- (void)_loadBrightness {
[[NSUserDefaults standardUserDefaults] registerDefaults:@{
brightnessValuePreferenceKey : @(8 * brightnessStep)
}];
_brightness = [[NSUserDefaults standardUserDefaults]
floatForKey:brightnessValuePreferenceKey];
NSLog(@"Loaded value: %f", _brightness);
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
if (![self _loadBezelServices]) {
[self _loadOSDFramework];
}
//[self _configureLoginItem];
[self _checkTrusted];
[self _registerGlobalKeyboardEvents];
[self _loadBrightness];
[self _registerSignalHandling];
}
void shutdownSignalHandler(int signal) {
// Don't do anything
}
- (void)_registerSignalHandling {
// Register signal callback that will gracefully shut the application down
self.signalHandlerSource = dispatch_source_create(
DISPATCH_SOURCE_TYPE_SIGNAL, SIGTERM, 0, dispatch_get_main_queue());
dispatch_source_set_event_handler(self.signalHandlerSource, ^{
NSLog(@"Caught SIGTERM");
[[NSApplication sharedApplication] terminate:self];
});
dispatch_resume(self.signalHandlerSource);
// Register signal handler that will prevent the app from being killed
signal(SIGTERM, shutdownSignalHandler);
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {
[self _willTerminate];
}
- (void)_willTerminate {
NSLog(@"willTerminate");
[self _saveBrightness];
}
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:
(NSApplication *)sender {
return NO;
}
dispatch_source_t debounceTimer = nil;
dispatch_source_t CreateDebounceDispatchTimer(double debounceTime,
dispatch_queue_t queue,
dispatch_block_t block) {
dispatch_source_t timer =
dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
if (timer) {
dispatch_source_set_timer(
timer, dispatch_time(DISPATCH_TIME_NOW, debounceTime * NSEC_PER_SEC),
DISPATCH_TIME_FOREVER, (1ull * NSEC_PER_SEC) / 10);
dispatch_source_set_event_handler(timer, block);
dispatch_resume(timer);
}
return timer;
}
- (void)setBrightness:(float)value {
_brightness = value;
CGDirectDisplayID display = CGSMainDisplayID();
if (_BSDoGraphicWithMeterAndTimeout != NULL) {
// El Capitan and probably older systems
_BSDoGraphicWithMeterAndTimeout(display, BSGraphicBacklightMeter, 0x0,
value / 100.f, 1);
} else {
// Sierra+
[[NSClassFromString(@"OSDManager") sharedManager]
showImage:OSDGraphicBacklight
onDisplayID:CGSMainDisplayID()
priority:OSDPriorityDefault
msecUntilFade:1000
filledChiclets:value / brightnessStep
totalChiclets:100.f / brightnessStep
locked:NO];
}
if (debounceTimer != nil) {
dispatch_source_cancel(debounceTimer);
debounceTimer = nil;
}
dispatch_queue_t queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
double secondsToThrottle = 0.01f;
debounceTimer = CreateDebounceDispatchTimer(secondsToThrottle, queue, ^{
for (NSScreen *screen in NSScreen.screens) {
NSDictionary *description = [screen deviceDescription];
if ([description objectForKey:@"NSDeviceIsScreen"]) {
CGDirectDisplayID screenNumber =
[[description objectForKey:@"NSScreenNumber"] unsignedIntValue];
set_control(screenNumber, BRIGHTNESS, value);
}
}
});
}
- (void)setVolume:(float)value {
_volume = value;
CGDirectDisplayID display = CGSMainDisplayID();
if (_BSDoGraphicWithMeterAndTimeout != NULL) {
// El Capitan and probably older systems
_BSDoGraphicWithMeterAndTimeout(display, BSGraphicSpeakerMeter, 0x0,
value / 100.f, 1);
} else {
// Sierra+
[[NSClassFromString(@"OSDManager") sharedManager]
showImage:OSDGraphicBacklight
onDisplayID:CGSMainDisplayID()
priority:OSDPriorityDefault
msecUntilFade:1000
filledChiclets:value / brightnessStep
totalChiclets:100.f / brightnessStep
locked:NO];
}
if (debounceTimer != nil) {
dispatch_source_cancel(debounceTimer);
debounceTimer = nil;
}
dispatch_queue_t queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
double secondsToThrottle = 0.01f;
debounceTimer = CreateDebounceDispatchTimer(secondsToThrottle, queue, ^{
for (NSScreen *screen in NSScreen.screens) {
NSDictionary *description = [screen deviceDescription];
if ([description objectForKey:@"NSDeviceIsScreen"]) {
CGDirectDisplayID screenNumber =
[[description objectForKey:@"NSScreenNumber"] unsignedIntValue];
set_control(screenNumber, AUDIO_SPEAKER_VOLUME,
MIN(ceil(100.0 * pow(value / 100.0, 2)), 20));
}
}
});
}
- (void)increaseBrightness {
self.brightness = MIN(self.brightness + brightnessStep, 100);
}
- (void)decreaseBrightness {
self.brightness = MAX(self.brightness - brightnessStep, 0);
}
- (void)increaseVolume {
self.volume = MIN(self.volume + volumeStep, 100);
}
- (void)decreaseVolume {
self.volume = MAX(self.volume - volumeStep, 0);
}
@end