mirror_displays() now checks CGDisplayMirrorsDisplay() for each secondary before calling CGBeginDisplayConfiguration — avoids a needless display reconfiguration (and potential flicker) when already mirrored. extend_displays() already had this check; no change needed there.
378 lines
15 KiB
Objective-C
378 lines
15 KiB
Objective-C
/*
|
||
* display_control.m
|
||
* Native macOS CLI for programmatic display mirror/extend control.
|
||
*
|
||
* Derived from the OSIT MasterKey app (LegacyUtilities.m, Ian Kohl 2019).
|
||
* Uses CoreGraphics APIs — no external dependencies (no displayplacer/Homebrew required).
|
||
*
|
||
* Build: run scripts/build-display-control.sh on a Mac (requires Xcode Command Line Tools)
|
||
* Usage: display_control <mirror|extend|status>
|
||
*/
|
||
|
||
#import <Cocoa/Cocoa.h>
|
||
#include <Carbon/Carbon.h>
|
||
#include <math.h>
|
||
|
||
#define MAX_DISPLAYS 8
|
||
#define MAX_MODES 256
|
||
|
||
typedef struct { long src_index; size_t w, h, pw, ph; double refresh; } ModeEntry;
|
||
|
||
static int mirror_displays(void) {
|
||
CGDirectDisplayID onlineDspys[MAX_DISPLAYS] = {0};
|
||
CGDisplayCount numOnline = 0;
|
||
|
||
CGGetOnlineDisplayList(MAX_DISPLAYS, onlineDspys, &numOnline);
|
||
|
||
if (numOnline < 2) {
|
||
fprintf(stderr, "No secondary display detected (%u online).\n", numOnline);
|
||
return 1;
|
||
}
|
||
|
||
CGDirectDisplayID mainID = CGMainDisplayID();
|
||
|
||
// Idempotency: if every secondary is already mirroring mainID, nothing to do.
|
||
CGDisplayCount alreadyMirrored = 0;
|
||
for (CGDisplayCount i = 0; i < numOnline; i++) {
|
||
if (onlineDspys[i] != mainID && CGDisplayMirrorsDisplay(onlineDspys[i]) == mainID)
|
||
alreadyMirrored++;
|
||
}
|
||
if (alreadyMirrored == numOnline - 1) {
|
||
printf("Displays already mirrored.\n");
|
||
return 0;
|
||
}
|
||
|
||
CGDisplayConfigRef config;
|
||
CGError err = CGBeginDisplayConfiguration(&config);
|
||
if (err != kCGErrorSuccess) {
|
||
fprintf(stderr, "CGBeginDisplayConfiguration failed: %d\n", err);
|
||
return 1;
|
||
}
|
||
|
||
BOOL any_configured = NO;
|
||
for (CGDisplayCount i = 0; i < numOnline; i++) {
|
||
if (onlineDspys[i] != mainID) {
|
||
err = CGConfigureDisplayMirrorOfDisplay(config, onlineDspys[i], mainID);
|
||
if (err != kCGErrorSuccess) {
|
||
fprintf(stderr, "CGConfigureDisplayMirrorOfDisplay failed: %d\n", err);
|
||
CGCancelDisplayConfiguration(config);
|
||
return 1;
|
||
}
|
||
any_configured = YES;
|
||
}
|
||
}
|
||
|
||
if (!any_configured) {
|
||
CGCancelDisplayConfiguration(config);
|
||
fprintf(stderr, "No secondary displays to mirror.\n");
|
||
return 1;
|
||
}
|
||
|
||
err = CGCompleteDisplayConfiguration(config, kCGConfigurePermanently);
|
||
if (err != kCGErrorSuccess) {
|
||
fprintf(stderr, "CGCompleteDisplayConfiguration failed: %d\n", err);
|
||
return 1;
|
||
}
|
||
|
||
printf("Mirrored %u display(s).\n", numOnline - 1);
|
||
return 0;
|
||
}
|
||
|
||
static int extend_displays(void) {
|
||
CGDirectDisplayID onlineDspys[MAX_DISPLAYS] = {0};
|
||
CGDirectDisplayID activeDspys[MAX_DISPLAYS] = {0};
|
||
CGDisplayCount numOnline = 0, numActive = 0;
|
||
|
||
CGGetOnlineDisplayList(MAX_DISPLAYS, onlineDspys, &numOnline);
|
||
CGGetActiveDisplayList(MAX_DISPLAYS, activeDspys, &numActive);
|
||
|
||
if (numOnline < 2) {
|
||
fprintf(stderr, "No secondary display detected (%u online).\n", numOnline);
|
||
return 1;
|
||
}
|
||
|
||
if (numActive >= numOnline) {
|
||
printf("Displays already extended.\n");
|
||
return 0;
|
||
}
|
||
|
||
CGDirectDisplayID mainID = CGMainDisplayID();
|
||
|
||
CGDisplayConfigRef config;
|
||
CGError err = CGBeginDisplayConfiguration(&config);
|
||
if (err != kCGErrorSuccess) {
|
||
fprintf(stderr, "CGBeginDisplayConfiguration failed: %d\n", err);
|
||
return 1;
|
||
}
|
||
|
||
for (CGDisplayCount i = 0; i < numOnline; i++) {
|
||
if (onlineDspys[i] != mainID) {
|
||
// kCGNullDirectDisplay as master = un-mirror (extend)
|
||
err = CGConfigureDisplayMirrorOfDisplay(config, onlineDspys[i], kCGNullDirectDisplay);
|
||
if (err != kCGErrorSuccess) {
|
||
fprintf(stderr, "CGConfigureDisplayMirrorOfDisplay(null) failed: %d\n", err);
|
||
CGCancelDisplayConfiguration(config);
|
||
return 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
err = CGCompleteDisplayConfiguration(config, kCGConfigurePermanently);
|
||
if (err != kCGErrorSuccess) {
|
||
fprintf(stderr, "CGCompleteDisplayConfiguration failed: %d\n", err);
|
||
return 1;
|
||
}
|
||
|
||
printf("Displays extended.\n");
|
||
return 0;
|
||
}
|
||
|
||
static void print_status(void) {
|
||
CGDirectDisplayID onlineDspys[MAX_DISPLAYS] = {0};
|
||
CGDirectDisplayID activeDspys[MAX_DISPLAYS] = {0};
|
||
CGDisplayCount numOnline = 0, numActive = 0;
|
||
|
||
CGGetOnlineDisplayList(MAX_DISPLAYS, onlineDspys, &numOnline);
|
||
CGGetActiveDisplayList(MAX_DISPLAYS, activeDspys, &numActive);
|
||
|
||
printf("online=%u active=%u %s\n",
|
||
numOnline, numActive,
|
||
(numOnline > 1 && numActive < numOnline) ? "mirrored" : "extended");
|
||
}
|
||
|
||
// ── list-modes ──────────────────────────────────────────────────────────────
|
||
// Outputs a JSON array describing every online display and its available modes.
|
||
|
||
static void list_modes(void) {
|
||
CGDirectDisplayID displays[MAX_DISPLAYS];
|
||
CGDisplayCount count = 0;
|
||
CGGetOnlineDisplayList(MAX_DISPLAYS, displays, &count);
|
||
CGDirectDisplayID mainID = CGMainDisplayID();
|
||
|
||
printf("[\n");
|
||
for (CGDisplayCount d = 0; d < count; d++) {
|
||
CGDirectDisplayID dID = displays[d];
|
||
CGDisplayModeRef currentMode = CGDisplayCopyDisplayMode(dID);
|
||
size_t curW = CGDisplayModeGetWidth(currentMode);
|
||
size_t curH = CGDisplayModeGetHeight(currentMode);
|
||
double curR = CGDisplayModeGetRefreshRate(currentMode);
|
||
size_t curPW = CGDisplayModeGetPixelWidth(currentMode);
|
||
size_t curPH = CGDisplayModeGetPixelHeight(currentMode);
|
||
|
||
// Include HiDPI duplicate entries so scaled modes are visible.
|
||
CFStringRef optKeys[] = { kCGDisplayShowDuplicateLowResolutionModes };
|
||
CFBooleanRef optVals[] = { kCFBooleanTrue };
|
||
CFDictionaryRef opts = CFDictionaryCreate(NULL,
|
||
(const void **)optKeys, (const void **)optVals, 1,
|
||
&kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
||
CFArrayRef allModes = CGDisplayCopyAllDisplayModes(dID, opts);
|
||
CFRelease(opts);
|
||
CFIndex total = CFArrayGetCount(allModes);
|
||
|
||
// Collect usable modes first so we know the count for comma handling.
|
||
ModeEntry usable[MAX_MODES];
|
||
int usable_count = 0;
|
||
for (CFIndex m = 0; m < total && usable_count < MAX_MODES; m++) {
|
||
CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, m);
|
||
if (!CGDisplayModeIsUsableForDesktopGUI(mode)) continue;
|
||
usable[usable_count++] = (ModeEntry){
|
||
.src_index = (long)m,
|
||
.w = CGDisplayModeGetWidth(mode),
|
||
.h = CGDisplayModeGetHeight(mode),
|
||
.pw = CGDisplayModeGetPixelWidth(mode),
|
||
.ph = CGDisplayModeGetPixelHeight(mode),
|
||
.refresh = CGDisplayModeGetRefreshRate(mode)
|
||
};
|
||
}
|
||
|
||
printf(" {\n");
|
||
printf(" \"index\": %u,\n", d);
|
||
printf(" \"id\": %u,\n", (unsigned int)dID);
|
||
printf(" \"is_main\": %s,\n", dID == mainID ? "true" : "false");
|
||
printf(" \"current_width\": %zu,\n", curW);
|
||
printf(" \"current_height\": %zu,\n", curH);
|
||
printf(" \"current_refresh\": %.2f,\n", curR);
|
||
printf(" \"current_pixel_width\": %zu,\n", curPW);
|
||
printf(" \"current_pixel_height\": %zu,\n", curPH);
|
||
printf(" \"modes\": [\n");
|
||
for (int i = 0; i < usable_count; i++) {
|
||
ModeEntry *e = &usable[i];
|
||
int isCurrent = (e->w == curW && e->h == curH &&
|
||
e->pw == curPW && e->ph == curPH &&
|
||
fabs(e->refresh - curR) < 0.5);
|
||
printf(" {\"index\":%ld,\"width\":%zu,\"height\":%zu,"
|
||
"\"refresh\":%.2f,\"pixel_width\":%zu,\"pixel_height\":%zu,"
|
||
"\"hidpi\":%s,\"is_current\":%s}%s\n",
|
||
e->src_index, e->w, e->h, e->refresh, e->pw, e->ph,
|
||
(e->pw > e->w || e->ph > e->h) ? "true" : "false",
|
||
isCurrent ? "true" : "false",
|
||
(i < usable_count - 1) ? "," : "");
|
||
}
|
||
printf(" ]\n");
|
||
printf(" }%s\n", (d < count - 1) ? "," : "");
|
||
|
||
CGDisplayModeRelease(currentMode);
|
||
CFRelease(allModes);
|
||
}
|
||
printf("]\n");
|
||
}
|
||
|
||
// ── set-mode ─────────────────────────────────────────────────────────────────
|
||
// display_idx : index from list-modes (0 = primary, 1 = first external, ...)
|
||
// req_w/h : logical width × height (what macOS calls "looks like X×Y")
|
||
// req_refresh : 0 = pick highest available; >0 = must be within 1 Hz
|
||
// force_hidpi : 1 = HiDPI only; -1 = non-HiDPI only; 0 = auto
|
||
// auto prefers HiDPI on the built-in, non-HiDPI on externals
|
||
|
||
static int set_mode(int display_idx, size_t req_w, size_t req_h,
|
||
double req_refresh, int force_hidpi) {
|
||
CGDirectDisplayID displays[MAX_DISPLAYS];
|
||
CGDisplayCount count = 0;
|
||
CGGetOnlineDisplayList(MAX_DISPLAYS, displays, &count);
|
||
|
||
if (display_idx < 0 || (CGDisplayCount)display_idx >= count) {
|
||
fprintf(stderr, "Display index %d out of range (0..%u).\n",
|
||
display_idx, count > 0 ? count - 1 : 0);
|
||
return 1;
|
||
}
|
||
|
||
CGDirectDisplayID dID = displays[display_idx];
|
||
int isMain = (dID == CGMainDisplayID());
|
||
|
||
CFStringRef optKeys[] = { kCGDisplayShowDuplicateLowResolutionModes };
|
||
CFBooleanRef optVals[] = { kCFBooleanTrue };
|
||
CFDictionaryRef opts = CFDictionaryCreate(NULL,
|
||
(const void **)optKeys, (const void **)optVals, 1,
|
||
&kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
||
CFArrayRef allModes = CGDisplayCopyAllDisplayModes(dID, opts);
|
||
CFRelease(opts);
|
||
CFIndex total = CFArrayGetCount(allModes);
|
||
|
||
CGDisplayModeRef bestMode = NULL;
|
||
double bestRefresh = -1.0;
|
||
int bestScore = -1; // higher = more preferred
|
||
|
||
for (CFIndex m = 0; m < total; m++) {
|
||
CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, m);
|
||
if (!CGDisplayModeIsUsableForDesktopGUI(mode)) continue;
|
||
|
||
size_t w = CGDisplayModeGetWidth(mode);
|
||
size_t h = CGDisplayModeGetHeight(mode);
|
||
if (w != req_w || h != req_h) continue;
|
||
|
||
double refresh = CGDisplayModeGetRefreshRate(mode);
|
||
if (req_refresh > 0.0 && fabs(refresh - req_refresh) > 1.0) continue;
|
||
|
||
size_t pw = CGDisplayModeGetPixelWidth(mode);
|
||
int isHiDPI = (pw > w);
|
||
|
||
if (force_hidpi == 1 && !isHiDPI) continue;
|
||
if (force_hidpi == -1 && isHiDPI) continue;
|
||
|
||
// Score: prefer HiDPI on main display, non-HiDPI on external.
|
||
int score = (force_hidpi == 0)
|
||
? ((isMain && isHiDPI) || (!isMain && !isHiDPI)) ? 1 : 0
|
||
: 0;
|
||
|
||
if (bestMode == NULL || score > bestScore ||
|
||
(score == bestScore && refresh > bestRefresh)) {
|
||
bestMode = mode;
|
||
bestRefresh = refresh;
|
||
bestScore = score;
|
||
}
|
||
}
|
||
|
||
if (!bestMode) {
|
||
fprintf(stderr, "No matching mode for display %d: %zux%zu",
|
||
display_idx, req_w, req_h);
|
||
if (req_refresh > 0.0) fprintf(stderr, " @%.0fHz", req_refresh);
|
||
if (force_hidpi == 1) fprintf(stderr, " [HiDPI required]");
|
||
if (force_hidpi == -1) fprintf(stderr, " [non-HiDPI required]");
|
||
fprintf(stderr, ".\nRun: display_control list-modes\n");
|
||
CFRelease(allModes);
|
||
return 1;
|
||
}
|
||
|
||
size_t setW = CGDisplayModeGetWidth(bestMode);
|
||
size_t setH = CGDisplayModeGetHeight(bestMode);
|
||
double setR = CGDisplayModeGetRefreshRate(bestMode);
|
||
size_t setPW = CGDisplayModeGetPixelWidth(bestMode);
|
||
size_t setPH = CGDisplayModeGetPixelHeight(bestMode);
|
||
|
||
CGDisplayConfigRef config;
|
||
CGError err = CGBeginDisplayConfiguration(&config);
|
||
if (err != kCGErrorSuccess) {
|
||
fprintf(stderr, "CGBeginDisplayConfiguration failed: %d\n", err);
|
||
CFRelease(allModes);
|
||
return 1;
|
||
}
|
||
|
||
err = CGConfigureDisplayWithDisplayMode(config, dID, bestMode, NULL);
|
||
if (err != kCGErrorSuccess) {
|
||
fprintf(stderr, "CGConfigureDisplayWithDisplayMode failed: %d\n", err);
|
||
CGCancelDisplayConfiguration(config);
|
||
CFRelease(allModes);
|
||
return 1;
|
||
}
|
||
|
||
err = CGCompleteDisplayConfiguration(config, kCGConfigurePermanently);
|
||
if (err != kCGErrorSuccess) {
|
||
fprintf(stderr, "CGCompleteDisplayConfiguration failed: %d\n", err);
|
||
CFRelease(allModes);
|
||
return 1;
|
||
}
|
||
|
||
printf("Set display %d to %zux%zu @%.0fHz (pixel %zux%zu).\n",
|
||
display_idx, setW, setH, setR, setPW, setPH);
|
||
CFRelease(allModes);
|
||
return 0;
|
||
}
|
||
|
||
// ── main ─────────────────────────────────────────────────────────────────────
|
||
|
||
int main(int argc, const char * argv[]) {
|
||
@autoreleasepool {
|
||
if (argc < 2) {
|
||
fprintf(stderr, "Usage: display_control <mirror|extend|status|list-modes|set-mode>\n");
|
||
return 1;
|
||
}
|
||
|
||
const char *cmd = argv[1];
|
||
|
||
if (strcmp(cmd, "mirror") == 0) {
|
||
return mirror_displays();
|
||
} else if (strcmp(cmd, "extend") == 0) {
|
||
return extend_displays();
|
||
} else if (strcmp(cmd, "status") == 0) {
|
||
print_status();
|
||
return 0;
|
||
} else if (strcmp(cmd, "list-modes") == 0) {
|
||
list_modes();
|
||
return 0;
|
||
} else if (strcmp(cmd, "set-mode") == 0) {
|
||
if (argc < 5) {
|
||
fprintf(stderr, "Usage: display_control set-mode <display_index> <width> <height> [--refresh <hz>] [--hidpi] [--no-hidpi]\n");
|
||
return 1;
|
||
}
|
||
int display_idx = atoi(argv[2]);
|
||
size_t req_w = (size_t)atol(argv[3]);
|
||
size_t req_h = (size_t)atol(argv[4]);
|
||
double req_refresh = 0.0;
|
||
int force_hidpi = 0;
|
||
for (int i = 5; i < argc; i++) {
|
||
if (strcmp(argv[i], "--refresh") == 0 && i + 1 < argc) {
|
||
req_refresh = atof(argv[++i]);
|
||
} else if (strcmp(argv[i], "--hidpi") == 0) {
|
||
force_hidpi = 1;
|
||
} else if (strcmp(argv[i], "--no-hidpi") == 0) {
|
||
force_hidpi = -1;
|
||
}
|
||
}
|
||
return set_mode(display_idx, req_w, req_h, req_refresh, force_hidpi);
|
||
} else {
|
||
fprintf(stderr, "Unknown command: %s\nUsage: display_control <mirror|extend|status|list-modes|set-mode>\n", cmd);
|
||
return 1;
|
||
}
|
||
}
|
||
}
|