Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6473a1a4b2 | ||
|
|
f34bb6bafe | ||
|
|
9b3f6ae50e |
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user