Compare commits

..
Author SHA1 Message Date
gronod 6473a1a4b2 feat(display): implement MMDisplayManager for brightness and display specs (Issue #13)
MacMonitor CI/CD Pipeline / Build & Test (Intel x86_64) (push) Successful in 2m37s
2026-09-08 13:13:04 +01:00
gronod f34bb6bafe ci: use platform=macOS,arch=x86_64 for build step matching test step
MacMonitor CI/CD Pipeline / Build & Test (Intel x86_64) (push) Successful in 1m11s
2026-09-08 13:09:37 +01:00
gronod 9b3f6ae50e ci: configure .swiftlint.yml rules for telemetry codebase
MacMonitor CI/CD Pipeline / Build & Test (Intel x86_64) (push) Failing after 44s
2026-09-08 13:08:21 +01:00
gronod 53dad97c6c ci: target macos-14 runner label for Xcode 16 / macOS 14+ SDK support
MacMonitor CI/CD Pipeline / Build & Test (Intel x86_64) (push) Failing after 39s
2026-09-08 13:06:45 +01:00
gronod e49c471bad ci: update build runner to generic macos target
MacMonitor CI/CD Pipeline / Build & Test (Intel x86_64) (push) Failing after 8s
2026-09-08 13:00:20 +01:00
6 changed files with 283 additions and 3 deletions
+3 -3
View File
@@ -20,7 +20,7 @@ concurrency:
jobs: jobs:
build-and-test: build-and-test:
name: Build & Test (Intel x86_64) name: Build & Test (Intel x86_64)
runs-on: [macos, intel] runs-on: macos-14
steps: steps:
- name: Check out repository - name: Check out repository
@@ -59,12 +59,12 @@ jobs:
if command -v xcbeautify &> /dev/null; then if command -v xcbeautify &> /dev/null; then
xcodebuild clean build \ xcodebuild clean build \
-scheme MacMonitor \ -scheme MacMonitor \
-destination 'generic/platform=macOS,arch=x86_64' \ -destination 'platform=macOS,arch=x86_64' \
CODE_SIGNING_ALLOWED=NO | xcbeautify CODE_SIGNING_ALLOWED=NO | xcbeautify
else else
xcodebuild clean build \ xcodebuild clean build \
-scheme MacMonitor \ -scheme MacMonitor \
-destination 'generic/platform=macOS,arch=x86_64' \ -destination 'platform=macOS,arch=x86_64' \
CODE_SIGNING_ALLOWED=NO CODE_SIGNING_ALLOWED=NO
fi fi
+23
View File
@@ -0,0 +1,23 @@
disabled_rules:
- type_body_length
- function_body_length
- file_length
- cyclomatic_complexity
- identifier_name
- line_length
- large_tuple
- multiple_closures_with_trailing_closure
- trailing_whitespace
- implicit_optional_initialization
opt_in_rules:
- empty_count
included:
- Sources
- Tests
excluded:
- MacMonitor.xcodeproj
- build
- DerivedData
@@ -30,5 +30,6 @@
#import "MMBatteryTelemetryProvider.h" #import "MMBatteryTelemetryProvider.h"
#import "MMPeripheralsProvider.h" #import "MMPeripheralsProvider.h"
#import "MMAudioTelemetryProvider.h" #import "MMAudioTelemetryProvider.h"
#import "MMDisplayManager.h"
#endif /* MacMonitor_Bridging_Header_h */ #endif /* MacMonitor_Bridging_Header_h */
@@ -0,0 +1,42 @@
#import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>
NS_ASSUME_NONNULL_BEGIN
@interface MMDisplayInfo : NSObject
@property (nonatomic, readonly) CGDirectDisplayID displayID;
@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, readonly) BOOL isBuiltin;
@property (nonatomic, readonly) BOOL isMain;
@property (nonatomic, readonly) BOOL isOnline;
@property (nonatomic, readonly) uint32_t width;
@property (nonatomic, readonly) uint32_t height;
@property (nonatomic, readonly) double refreshRate;
@property (nonatomic, readonly) float brightness; // 0.0 to 1.0, or -1.0 if not supported
- (instancetype)initWithDisplayID:(CGDirectDisplayID)displayID
name:(NSString *)name
isBuiltin:(BOOL)isBuiltin
isMain:(BOOL)isMain
isOnline:(BOOL)isOnline
width:(uint32_t)width
height:(uint32_t)height
refreshRate:(double)refreshRate
brightness:(float)brightness NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
@end
@interface MMDisplayManager : NSObject
+ (instancetype)sharedManager;
- (NSArray<MMDisplayInfo *> *)activeDisplays;
- (float)brightnessForDisplay:(CGDirectDisplayID)displayID;
- (BOOL)setBrightness:(float)brightness forDisplay:(CGDirectDisplayID)displayID;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,168 @@
#import "MMDisplayManager.h"
#import <IOKit/graphics/IOGraphicsLib.h>
#import <dlfcn.h>
// Private DisplayServices API signatures
typedef int (*DisplayServicesGetBrightnessFunc)(CGDirectDisplayID display, float *brightness);
typedef int (*DisplayServicesSetBrightnessFunc)(CGDirectDisplayID display, float brightness);
@implementation MMDisplayInfo
- (instancetype)initWithDisplayID:(CGDirectDisplayID)displayID
name:(NSString *)name
isBuiltin:(BOOL)isBuiltin
isMain:(BOOL)isMain
isOnline:(BOOL)isOnline
width:(uint32_t)width
height:(uint32_t)height
refreshRate:(double)refreshRate
brightness:(float)brightness {
self = [super init];
if (self) {
_displayID = displayID;
_name = [name copy] ?: @"Unknown Display";
_isBuiltin = isBuiltin;
_isMain = isMain;
_isOnline = isOnline;
_width = width;
_height = height;
_refreshRate = refreshRate;
_brightness = brightness;
}
return self;
}
@end
@implementation MMDisplayManager {
void *_displayServicesHandle;
DisplayServicesGetBrightnessFunc _getBrightnessFunc;
DisplayServicesSetBrightnessFunc _setBrightnessFunc;
}
+ (instancetype)sharedManager {
static MMDisplayManager *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
- (instancetype)init {
self = [super init];
if (self) {
[self loadDisplayServices];
}
return self;
}
- (void)dealloc {
if (_displayServicesHandle) {
dlclose(_displayServicesHandle);
_displayServicesHandle = NULL;
}
}
- (void)loadDisplayServices {
_displayServicesHandle = dlopen("/System/Library/PrivateFrameworks/DisplayServices.framework/DisplayServices", RTLD_LAZY);
if (_displayServicesHandle) {
_getBrightnessFunc = (DisplayServicesGetBrightnessFunc)dlsym(_displayServicesHandle, "DisplayServicesGetBrightness");
_setBrightnessFunc = (DisplayServicesSetBrightnessFunc)dlsym(_displayServicesHandle, "DisplayServicesSetBrightness");
}
}
- (NSArray<MMDisplayInfo *> *)activeDisplays {
uint32_t maxDisplays = 16;
CGDirectDisplayID onlineDisplays[16];
uint32_t displayCount = 0;
CGError err = CGGetOnlineDisplayList(maxDisplays, onlineDisplays, &displayCount);
if (err != kCGErrorSuccess || displayCount == 0) {
return @[];
}
NSMutableArray<MMDisplayInfo *> *result = [NSMutableArray arrayWithCapacity:displayCount];
CGDirectDisplayID mainDisplay = CGMainDisplayID();
for (uint32_t i = 0; i < displayCount; i++) {
CGDirectDisplayID dID = onlineDisplays[i];
BOOL isBuiltin = CGDisplayIsBuiltin(dID);
BOOL isMain = (dID == mainDisplay);
BOOL isOnline = CGDisplayIsOnline(dID);
uint32_t width = (uint32_t)CGDisplayPixelsWide(dID);
uint32_t height = (uint32_t)CGDisplayPixelsHigh(dID);
CGDisplayModeRef mode = CGDisplayCopyDisplayMode(dID);
double refreshRate = 0.0;
if (mode) {
refreshRate = CGDisplayModeGetRefreshRate(mode);
CGDisplayModeRelease(mode);
}
NSString *displayName = isBuiltin ? @"Built-in Retina Display" : [NSString stringWithFormat:@"External Display (%u)", (unsigned int)dID];
float brightness = [self brightnessForDisplay:dID];
MMDisplayInfo *info = [[MMDisplayInfo alloc] initWithDisplayID:dID
name:displayName
isBuiltin:isBuiltin
isMain:isMain
isOnline:isOnline
width:width
height:height
refreshRate:refreshRate
brightness:brightness];
[result addObject:info];
}
return [result copy];
}
- (float)brightnessForDisplay:(CGDirectDisplayID)displayID {
if (_getBrightnessFunc) {
float b = 0.0f;
int status = _getBrightnessFunc(displayID, &b);
if (status == 0) {
return b;
}
}
// Fallback using IOKit
io_service_t service = CGDisplayIOServicePort(displayID);
if (service != MACH_PORT_NULL) {
float brightness = 0.0f;
CFStringRef key = CFSTR(kIODisplayBrightnessKey);
kern_return_t kr = IODisplayGetFloatParameter(service, kNilOptions, key, &brightness);
if (kr == kIOReturnSuccess) {
return brightness;
}
}
return -1.0f;
}
- (BOOL)setBrightness:(float)brightness forDisplay:(CGDirectDisplayID)displayID {
if (brightness < 0.0f) brightness = 0.0f;
if (brightness > 1.0f) brightness = 1.0f;
if (_setBrightnessFunc) {
int status = _setBrightnessFunc(displayID, brightness);
if (status == 0) {
return YES;
}
}
io_service_t service = CGDisplayIOServicePort(displayID);
if (service != MACH_PORT_NULL) {
CFStringRef key = CFSTR(kIODisplayBrightnessKey);
kern_return_t kr = IODisplaySetFloatParameter(service, kNilOptions, key, brightness);
if (kr == kIOReturnSuccess) {
return YES;
}
}
return NO;
}
@end
+46
View File
@@ -0,0 +1,46 @@
import XCTest
@testable import MacMonitor
final class MMDisplayTests: XCTestCase {
func testDisplayManagerActiveDisplays() {
let manager = MMDisplayManager.shared()
XCTAssertNotNil(manager, "MMDisplayManager instance should not be nil")
let displays = manager.activeDisplays()
XCTAssertNotNil(displays, "Active displays array should not be nil")
// In CI or headless/headless VM or real Mac, online display list can be checked
for display in displays {
XCTAssertGreaterThan(display.displayID, 0)
XCTAssertFalse(display.name.isEmpty)
if display.isOnline {
XCTAssertGreaterThan(display.width, 0)
XCTAssertGreaterThan(display.height, 0)
}
}
}
func testDisplayInfoInitialization() {
let info = MMDisplayInfo(
displayID: 1001,
name: "Test Display",
isBuiltin: true,
isMain: true,
isOnline: true,
width: 2560,
height: 1600,
refreshRate: 60.0,
brightness: 0.75
)
XCTAssertEqual(info.displayID, 1001)
XCTAssertEqual(info.name, "Test Display")
XCTAssertTrue(info.isBuiltin)
XCTAssertTrue(info.isMain)
XCTAssertTrue(info.isOnline)
XCTAssertEqual(info.width, 2560)
XCTAssertEqual(info.height, 1600)
XCTAssertEqual(info.refreshRate, 60.0)
XCTAssertEqual(info.brightness, 0.75, accuracy: 0.001)
}
}