├── .gitignore ├── README.md └── fb-rotate.c /.gitignore: -------------------------------------------------------------------------------- 1 | # xcode noise 2 | *.mode1v3 3 | *.mode2v3 4 | *.pbxuser 5 | *.perspective 6 | *.perspectivev3 7 | *.pyc 8 | *~.nib/ 9 | */build/* 10 | *.app/ 11 | 12 | 13 | 14 | # Textmate - if you build your xcode projects with it 15 | *.tm_build_errors 16 | 17 | # old skool 18 | .svn 19 | 20 | # osx noise 21 | .DS_Store 22 | profile 23 | 24 | ## generic files to ignore 25 | *~ 26 | *.lock 27 | *.DS_Store 28 | *.swp 29 | *.out 30 | 31 | # for this git directory: fb-rotate 32 | fb-rotate 33 | 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Display Rotation for the Mac: fb-rotate 3 | ======================================= 4 | 5 | A Unix utility able to rotate the display on any Mac, including the internal display on Apple notebooks, and able to switch the primary display, the one with the menu bar, back and forth between displays. 6 | 7 | WARNING! 8 | ======== 9 | 10 | On Recent Macbooks running Sierra and El Capitan: 11 | 12 | After a 90 or 270 degree rotation, the dislay fails, the computer and applications are still running, keyboard works, but the display is black. After a reset and restart, the screen is turned and fb-rotate continues to work for everything (including the -i and -l options in my tests), but the 90 and 270 rotations again cause the display to fail. 13 | 14 | The 180 degree rotation works fine. 15 | 16 | This utility depends on a private Apple API. We have had a ten year run. It was bound to stop working eventually. I will keep playing, but I'm not hopeful. 17 | 18 | WARNING! 19 | ======== 20 | 21 | Compiling fb-rotate 22 | ------------------- 23 | 24 | Assuming you have Xcode installed, you can compile the C-code yourself on any Mac OS from 10.3 to 10.11. 25 | 26 | In the Terminal app, after you've changed the current directory to the one `fb-rotate.c` is stored, using 27 | 28 | cd 29 | 30 | then, 31 | 32 | gcc -w -o fb-rotate fb-rotate.c -framework IOKit -framework ApplicationServices 33 | 34 | will compile the utility. 35 | 36 | 37 | Use of fb-rotate 38 | ---------------- 39 | 40 | The l-option (list): 41 | 42 | fb-rotate -l 43 | 44 | will list the display id's, e.g. in Terminal, 45 | 46 | $ ./fb-rotate -l 47 | Display ID Resolution 48 | 0x19156030 1280x800 [main display] 49 | 0x76405c2d 1344x1008 50 | 51 | The i-option (info): 52 | 53 | fb-rotate -i 54 | 55 | will list the display id's with other information, e.g. 56 | 57 | $ ./fb-rotate -i 58 | # Display_ID Resolution ____Display_Bounds____ Rotation 59 | 0 0x19156030 1280x800 0 0 1280 800 0 [main][internal] 60 | 1 0x76405c2d 1344x1008 1280 0 2624 1008 0 61 | Mouse Cursor Position: ( 528 , 409 ) 62 | 63 | (Unlike the file: `com.apple.windowserver.plist`, fb-rotate's information is always accurate and current.) 64 | 65 | The d (display) and r (rotate) options : 66 | 67 | fb-rotate -d 0 -r 180 68 | 69 | will rotate the main display 180 degrees, e.g. 70 | 71 | $ ./fb-rotate -d 0 -r 180 72 | $ ./fb-rotate -i 73 | # Display_ID Resolution ____Display_Bounds____ Rotation 74 | 0 0x19156030 1280x800 0 0 1280 800 180 [main][internal] 75 | 1 0x76405c2d 1344x1008 1280 0 2624 1008 0 76 | Mouse Cursor Position: ( 1047 , 359 ) 77 | 78 | (You can rotate to the 0, 90 and 270 degree orientations as well.) 79 | 80 | Also, 81 | 82 | fb-rotate -d -r 0 83 | 84 | will rotate the display with the indicated ID back to the standard orientation, e.g. 85 | 86 | $ ./fb-rotate -d 0x19156030 -r 0 87 | $ ./fb-rotate -i 88 | # Display_ID Resolution ____Display_Bounds____ Rotation 89 | 0 0x19156030 1280x800 0 0 1280 800 0 [main][internal] 90 | 1 0x76405c2d 1344x1008 1280 0 2624 1008 0 91 | Mouse Cursor Position: ( 226 , 103 ) 92 | 93 | (Again, you can also rotate to the 90, 180 and 270 degree orientations.) 94 | 95 | Further, there are shortcuts: 96 | 97 | When using the `-d` option, 98 | 99 | - `-1` is a short cut for the `` of the internal monitor, 100 | - `0` is a short cut for the `` of the main monitor, 101 | - `1` is a short cut for the `` of the first non-internal monitor. 102 | 103 | When using the `-r` option, 104 | 105 | - `-r 1` toggles between the 0 and 90 degree orientations. 106 | 107 | 108 | Finally, the m-option (main): 109 | 110 | fb-rotate -d -m 111 | 112 | will set the display with the indicated ID to be the primary (main) display that has the menu bar, e.g. 113 | 114 | $ ./fb-rotate -d 0x76405c2d -m 115 | $ ./fb-rotate -i 116 | # Display_ID Resolution ____Display_Bounds____ Rotation 117 | 1 0x76405c2d 1344x1008 0 0 1344 1008 0 [main] 118 | 0 0x19156030 1280x800 -1280 0 0 800 0 [internal] 119 | Mouse Cursor Position: ( 1122 , 438 ) 120 | 121 | 122 | 123 | Downloads 124 | --------- 125 | 126 | [A binary version of fb-rotate][fb-rotate] is available at Modbookish, a forum focused on the [Axiotron Modbook][Modbook]. 127 | 128 | 129 | Caveats 130 | ------- 131 | 132 | Warning: Some white MacBooks (2006-2008), namely those using Intel's integrated graphics, have difficulty rotating to the 90º or 270º orientations and the resulting display may be difficult to use. 133 | 134 | 135 | Credits and License 136 | ------------------- 137 | 138 | The original code for fb-rotate comes from a programming example in 139 | the book **Mac OS X Internals: A Systems Approach** by Amit Singh (© 2006). The source is made available under the GNU General Public License (GPL). For more information, see the book's associated web site: [http://osxbook.com][osxbook] 140 | 141 | Changes were made by [Eric Nitardy][ericn] (© 2010) which have to be made available under the same license. 142 | 143 | [osxbook]: http://osxbook.com 144 | [ericn]: http://cdlbb.github.com 145 | [fb-rotate]: http://modbookish.lefora.com/topic/3513246/A-Unix-Utility-to-Change-the-Primary-Display-on-OSX/ 146 | [Modbook]: http://www.modbook.com 147 | 148 | 149 | -------------------------------------------------------------------------------- /fb-rotate.c: -------------------------------------------------------------------------------- 1 | // fb-rotate.c 2 | // 3 | // Compile with: 4 | // gcc -w -o fb-rotate fb-rotate.c -framework IOKit -framework ApplicationServices 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #define PROGNAME "fb-rotate" 11 | #define MAX_DISPLAYS 16 12 | 13 | // kIOFBSetTransform comes from 14 | // in the source for the IOGraphics family 15 | 16 | 17 | enum { 18 | kIOFBSetTransform = 0x00000400, 19 | }; 20 | 21 | void 22 | usage(void) 23 | { 24 | fprintf(stderr, "usage: %s -l\n" 25 | " %s -i\n" 26 | " %s -d -m\n" 27 | " %s -d -r <0|90|180|270|1>\n" 28 | "\n" 29 | "-r 1 signfies 90 if currently not rotated; otherwise 0 (i.e. toggle)\n" 30 | "\n" 31 | "-d -1 can be used for the of the internal monitor\n" 32 | "-d 0 can be used for the of the main monitor\n" 33 | "-d 1 can be used for the of the first non-internal monitor\n", 34 | PROGNAME, PROGNAME, PROGNAME, PROGNAME); 35 | exit(1); 36 | } 37 | 38 | void 39 | listDisplays(void) 40 | { 41 | CGDisplayErr dErr; 42 | CGDisplayCount displayCount, i; 43 | CGDirectDisplayID mainDisplay; 44 | CGDisplayCount maxDisplays = MAX_DISPLAYS; 45 | CGDirectDisplayID onlineDisplays[MAX_DISPLAYS]; 46 | 47 | mainDisplay = CGMainDisplayID(); 48 | 49 | dErr = CGGetOnlineDisplayList(maxDisplays, onlineDisplays, &displayCount); 50 | if (dErr != kCGErrorSuccess) { 51 | fprintf(stderr, "CGGetOnlineDisplayList: error %d.\n", dErr); 52 | exit(1); 53 | } 54 | 55 | printf("Display ID Resolution\n"); 56 | for (i = 0; i < displayCount; i++) { 57 | CGDirectDisplayID dID = onlineDisplays[i]; 58 | printf("0x%-14x %lux%lu %32s", dID, 59 | CGDisplayPixelsWide(dID), CGDisplayPixelsHigh(dID), 60 | (dID == mainDisplay) ? "[main display]\n" : "\n"); 61 | } 62 | 63 | exit(0); 64 | } 65 | 66 | void 67 | infoDisplays(void) 68 | { 69 | CGDisplayErr dErr; 70 | CGDisplayCount displayCount, i; 71 | CGDirectDisplayID mainDisplay; 72 | CGDisplayCount maxDisplays = MAX_DISPLAYS; 73 | CGDirectDisplayID onlineDisplays[MAX_DISPLAYS]; 74 | 75 | CGEventRef ourEvent = CGEventCreate(NULL); 76 | CGPoint ourLoc = CGEventGetLocation(ourEvent); 77 | 78 | CFRelease(ourEvent); 79 | 80 | mainDisplay = CGMainDisplayID(); 81 | 82 | dErr = CGGetOnlineDisplayList(maxDisplays, onlineDisplays, &displayCount); 83 | if (dErr != kCGErrorSuccess) { 84 | fprintf(stderr, "CGGetOnlineDisplayList: error %d.\n", dErr); 85 | exit(1); 86 | } 87 | 88 | printf("# Display_ID Resolution ____Display_Bounds____ Rotation\n"); 89 | for (i = 0; i < displayCount; i++) { 90 | CGDirectDisplayID dID = onlineDisplays[i]; 91 | printf("%-2d 0x%-10x %4lux%-4lu %5.0f %5.0f %5.0f %5.0f %3.0f %s%s%s", 92 | CGDisplayUnitNumber (dID), dID, 93 | CGDisplayPixelsWide(dID), CGDisplayPixelsHigh(dID), 94 | CGRectGetMinX (CGDisplayBounds (dID)), 95 | CGRectGetMinY (CGDisplayBounds (dID)), 96 | CGRectGetMaxX (CGDisplayBounds (dID)), 97 | CGRectGetMaxY (CGDisplayBounds (dID)), 98 | CGDisplayRotation (dID), 99 | (CGDisplayIsActive (dID)) ? "" : "[inactive]", 100 | (dID == mainDisplay) ? "[main]" : "", 101 | (CGDisplayIsBuiltin (dID)) ? "[internal]\n" : "\n"); 102 | } 103 | 104 | printf("Mouse Cursor Position: ( %5.0f , %5.0f )\n", 105 | (float)ourLoc.x, (float)ourLoc.y); 106 | 107 | exit(0); 108 | } 109 | 110 | void 111 | setMainDisplay(CGDirectDisplayID targetDisplay) 112 | { 113 | int deltaX, deltaY, flag; 114 | CGDisplayErr dErr; 115 | CGDisplayCount displayCount, i; 116 | CGDirectDisplayID mainDisplay; 117 | CGDisplayCount maxDisplays = MAX_DISPLAYS; 118 | CGDirectDisplayID onlineDisplays[MAX_DISPLAYS]; 119 | CGDisplayConfigRef config; 120 | 121 | mainDisplay = CGMainDisplayID(); 122 | 123 | if (mainDisplay == targetDisplay) { 124 | exit(0); 125 | } 126 | 127 | dErr = CGGetOnlineDisplayList(maxDisplays, onlineDisplays, &displayCount); 128 | if (dErr != kCGErrorSuccess) { 129 | fprintf(stderr, "CGGetOnlineDisplayList: error %d.\n", dErr); 130 | exit(1); 131 | } 132 | 133 | flag = 0; 134 | for (i = 0; i < displayCount; i++) { 135 | CGDirectDisplayID dID = onlineDisplays[i]; 136 | if (dID == targetDisplay) { flag = 1; } 137 | } 138 | if (flag == 0) { 139 | fprintf(stderr, "No such display ID: 0x%-10x.\n", targetDisplay); 140 | exit(1); 141 | } 142 | 143 | deltaX = -CGRectGetMinX (CGDisplayBounds (targetDisplay)); 144 | deltaY = -CGRectGetMinY (CGDisplayBounds (targetDisplay)); 145 | 146 | CGBeginDisplayConfiguration (&config); 147 | 148 | for (i = 0; i < displayCount; i++) { 149 | CGDirectDisplayID dID = onlineDisplays[i]; 150 | 151 | CGConfigureDisplayOrigin (config, dID, 152 | CGRectGetMinX (CGDisplayBounds (dID)) + deltaX, 153 | CGRectGetMinY (CGDisplayBounds (dID)) + deltaY ); 154 | } 155 | 156 | CGCompleteDisplayConfiguration (config, kCGConfigureForSession); 157 | 158 | 159 | exit(0); 160 | } 161 | 162 | 163 | CGDirectDisplayID 164 | InternalID(void) { 165 | // returns the ID of the internal monitor; 166 | CGDisplayErr dErr; 167 | CGDisplayCount displayCount, i; 168 | CGDisplayCount maxDisplays = MAX_DISPLAYS; 169 | CGDirectDisplayID onlineDisplays[MAX_DISPLAYS]; 170 | CGDirectDisplayID fallbackID = 0; 171 | dErr = CGGetOnlineDisplayList(maxDisplays, onlineDisplays, &displayCount); 172 | if (dErr != kCGErrorSuccess) { 173 | fprintf(stderr, "CGGetOnlineDisplayList: error %d.\n", dErr); 174 | exit(1); 175 | } 176 | for (i = 0; i < displayCount; i++) { 177 | CGDirectDisplayID dID = onlineDisplays[i]; 178 | if ((CGDisplayIsBuiltin (dID))) { 179 | return dID; 180 | } 181 | } 182 | return fallbackID; 183 | } 184 | 185 | 186 | CGDirectDisplayID 187 | nonInternalID(void) { 188 | // returns the ID of the first active monitor that is not internal or 0 if only one monitor; 189 | CGDisplayErr dErr; 190 | CGDisplayCount displayCount, i; 191 | CGDisplayCount maxDisplays = MAX_DISPLAYS; 192 | CGDirectDisplayID onlineDisplays[MAX_DISPLAYS]; 193 | CGDirectDisplayID fallbackID = 0; 194 | dErr = CGGetOnlineDisplayList(maxDisplays, onlineDisplays, &displayCount); 195 | if (dErr != kCGErrorSuccess) { 196 | fprintf(stderr, "CGGetOnlineDisplayList: error %d.\n", dErr); 197 | exit(1); 198 | } 199 | for (i = 0; i < displayCount; i++) { 200 | CGDirectDisplayID dID = onlineDisplays[i]; 201 | if (!(CGDisplayIsBuiltin (dID)) && (CGDisplayIsActive (dID))) { 202 | return dID; 203 | } 204 | } 205 | return fallbackID; 206 | } 207 | 208 | 209 | 210 | CGDirectDisplayID 211 | cgIDfromU32(uint32_t preId) 212 | { 213 | CGDisplayErr dErr; 214 | CGDisplayCount displayCount, i; 215 | CGDisplayCount maxDisplays = MAX_DISPLAYS; 216 | CGDirectDisplayID onlineDisplays[MAX_DISPLAYS]; 217 | CGDirectDisplayID postId = preId; 218 | 219 | dErr = CGGetOnlineDisplayList(maxDisplays, onlineDisplays, &displayCount); 220 | if (dErr != kCGErrorSuccess) { 221 | fprintf(stderr, "CGGetOnlineDisplayList: error %d.\n", dErr); 222 | exit(1); 223 | } 224 | for (i = 0; i < displayCount; i++) { 225 | CGDirectDisplayID dID = onlineDisplays[i]; 226 | if ((dID == preId) || (dID == postId) || 227 | (onlineDisplays[i] == preId) || (onlineDisplays[i] == postId)) { 228 | return dID; 229 | } 230 | } 231 | fprintf(stderr, " Could not find a matching id in onlineDisplays!\n"); 232 | exit(1); 233 | } 234 | 235 | IOOptionBits 236 | angle2options(long angle) 237 | { 238 | static IOOptionBits anglebits[] = { 239 | (kIOFBSetTransform | (kIOScaleRotate0) << 16), 240 | (kIOFBSetTransform | (kIOScaleRotate90) << 16), 241 | (kIOFBSetTransform | (kIOScaleRotate180) << 16), 242 | (kIOFBSetTransform | (kIOScaleRotate270) << 16) 243 | }; 244 | 245 | if ((angle % 90) != 0) // Map arbitrary angles to a rotation reset 246 | return anglebits[0]; 247 | 248 | return anglebits[(angle / 90) % 4]; 249 | } 250 | 251 | int 252 | main(int argc, char **argv) 253 | { 254 | int i; 255 | long angle = 0; 256 | long currentRotation = 0; 257 | 258 | io_service_t service; 259 | CGDisplayErr dErr; 260 | CGDirectDisplayID targetDisplay = 0; 261 | IOOptionBits options; 262 | 263 | while ((i = getopt(argc, argv, "d:limr:")) != -1) { 264 | switch (i) { 265 | case 'd': 266 | targetDisplay = (CGDirectDisplayID)strtoul(optarg, NULL, 16); 267 | if (targetDisplay == -1) 268 | targetDisplay = InternalID(); 269 | if (targetDisplay == 0) 270 | targetDisplay = CGMainDisplayID(); 271 | if (targetDisplay == 1) { 272 | targetDisplay = nonInternalID(); 273 | if (targetDisplay == 0) { 274 | fprintf(stderr, "Could not find an active monitor besides the internal one.\n"); 275 | exit(1); 276 | } 277 | } 278 | break; 279 | case 'l': 280 | listDisplays(); 281 | break; 282 | case 'i': 283 | infoDisplays(); 284 | break; 285 | case 'm': 286 | setMainDisplay(targetDisplay); 287 | break; 288 | case 'r': 289 | angle = strtol(optarg, NULL, 10); 290 | break; 291 | default: 292 | break; 293 | } 294 | } 295 | 296 | if (targetDisplay == 0) 297 | usage(); 298 | 299 | if (angle == 1) { 300 | currentRotation = CGDisplayRotation (targetDisplay); 301 | if (currentRotation == 0) { 302 | angle = 90; 303 | } else { 304 | angle = 0; 305 | } 306 | } 307 | 308 | options = angle2options(angle); 309 | 310 | // Get the I/O Kit service port of the target display 311 | // Since the port is owned by the graphics system, we should not destroy it 312 | 313 | // in Yosemite it seems important to have a call to CGGetOnlineDisplayList() before calling 314 | // CGDisplayIOServicePort() or the later replacements CGDisplayVendorNumber() etc. 315 | // otherwise this program can hang. 316 | CGDirectDisplayID td2 = cgIDfromU32(targetDisplay); 317 | service = CGDisplayIOServicePort(td2); 318 | 319 | // We will get an error if the target display doesn't support the 320 | // kIOFBSetTransform option for IOServiceRequestProbe() 321 | dErr = IOServiceRequestProbe(service, options); 322 | if (dErr != kCGErrorSuccess) { 323 | fprintf(stderr, "IOServiceRequestProbe: error %d\n", dErr); 324 | exit(1); 325 | } 326 | 327 | exit(0); 328 | } 329 | --------------------------------------------------------------------------------