Converting SetDisplay to Python

Converting SetDisplay to Python

os x yosemite, display, python logos

Overview


For years we’ve been using the old C program SetDisplay (the code can be seen here, as modified by one of our previous teammates). However we’ve recently been trying to move everything to Python, and to continue that effort I’ve set about porting the C code to Python. While we may lose some performance, we hope to gain portability, readability, and maintainability, which we feel are more valuable in the long term.

A good portion of SetDisplay relies on native Apple OS X APIs, which are not readily modified from a standard Python installation. Fortunately, there is a useful Python library called PyObjC, which exists as a bridge between Python and OS X’s internal configurations (in much the same way that Objective-C works). I intend to keep as much of the rewrite in native Python as possible, while using the PyObjC library to access system features which are not otherwise accessible in Python. (Note that PyObjC is shipped with the OS X built-in Python by default.)

The end result of this project will be a simple, easy-to-use Python script that can handle all of the abilities of the original SetDisplay system (and possibly more?). However, I’ve never used PyObjC before, so my first step will be a strict rewrite of SetDisplay into Python. I won’t fix any issues with the code, and when it’s run it ought to output everything exactly the same way the original SetDisplay did. I hope by doing this I’ll gain some insight into how PyObjC can be used better to our advantage.

Since we love open source software, the Python project can be found over here on GitHub.

Details


All right, let’s dive in. Looking over SetDisplay I see there’s a struct called displayMode. Let’s go ahead an reproduce that:

class DisplayMode(Object):
    def __init__(self):
        self.width = 0
        self.height = 0
        self.bits_per_pixel = 0
        self.refresh = 0

    def __init__(self, width, height, bits_per_pixel, refresh):
        self.width = width
        self.height = height
        self.bits_per_pixel = bits_per_pixel
        self.refresh = refresh

Then there are all of the methods. I’ll fill those in with blank placeholders for the moment. Let’s skip ahead to the main method. Glancing through, there are a lot of values initialized up at the top. Since we’re using Python, we probably won’t need to worry about a good portion of that. I’ll try not to declare variables too far in advance, so we’ll skip all of them for the moment. I’ll also skip over the command-line option-getting portion for now, and we’ll just dive into trying to get information about our displays.

Down on line 320 we have our first real use of one of the Apple libraries: CGGetOnlineDisplayList. Well… I have no idea how to implement this in Python. After poking around, I found this great article which even touches on some of the same methods we’ll have to use! I’ll paraphrase the relevant part for you here. In short, a lot of Objective-C methods take pointers as parameters, which don’t really exist in Python. Additionally, it’s common to set the output of an Objective-C method to an error variable that will be checked later. Ignoring the terribly unPythonic nature of this system (it’s practically barbaric; let’s be honest), we can replicate it pretty easily.

Here’s the C code from SetDisplay:

err = CGGetOnlineDisplayList(MAX_DISPLAYS, displays, &numDisplays);

And here’s the same thing in Python:

(err, displays, numDisplays) = Quartz.CGGetOnlineDisplayList(MAX_DISPLAYS, None, None)

You can see that there’s something funny going on with all those None  values and the tuple output, so I’ll break it down. First, looking at the method call, there are two None  types inserted where we had pointers in the Objective-C call. Python doesn’t have pointers, so we use None . But where do the values go? They’re the extra values that are outputted by the function! Since Python allows for returning tuples, the people who wrote PyObjC decided this was the best compromise they could come up with, and I’m inclined to agree. It’s simple, and it’s consistent throughout the PyObjC library without using pass-through variables like in C.

After the first section of coding, I have some working code! It doesn’t do much except print out the identifier for each attached display, but I’m happy with that. Here’s what I’ve got so far (remembering that all methods were filled in as blanks):

if __name__ == '__main__':
    verbose = True
    should_show_all = False
    should_find_exact = False
    should_find_highest = False
    should_find_closest = False
    enable_mirroring = False
    should_set_display = False

    # Get list of online displays.
    (error, online_displays, displays_count) = Quartz.CGGetOnlineDisplayList(MAX_DISPLAYS, None, None)
    if error:
        print("Cannot get displays ({})".format(error), file=sys.stderr)
        sys.exit(1)
    if verbose:
        print("{} online display(s) found".format(displays_count))
    mode = DisplayMode(1024, 768, 32, 75)
    for i in xrange(displays_count):
        identifier = online_displays[i]
        if verbose and not should_show_all:
            print("------------------------------------")
        original_mode = Quartz.CGDisplayCopyDisplayMode(identifier)
        if not original_mode:
            print("Display 0x{} is invalid".format(identifier), file=sys.stderr)
            sys.exit(1)
        if verbose:
            print("Display 0x{}".format(identifier))
        if should_show_all:
            get_all_modes_for_display(identifier, verbose)
        else:
            if should_find_exact:
                print("------ Exact mode for display -----")
                ref_mode = get_mode_for_display(identifier, 0, mode)
                print("------------------------------------")
            elif should_find_highest:
                print("----- Highest mode for display ----")
                mode.width = sys.maxint
                mode.height = sys.maxint
                mode.bits_per_pixel = sys.maxint
                mode.refresh = sys.maxint
                ref_mode = get_mode_for_display(identifier, 1, mode)
                print("------------------------------------")
           elif should_find_closest:
                print("----- Closest mode for display ----")
                ref_mode = get_mode_for_display(identifier, 1, mode)
                print("------------------------------------")
           else:
                ref_mode = Quartz.CGDisplayModeRef.alloc().init()

           if should_set_display:
               set_display(identifier, ref_mode, enable_mirroring, verbose)

And some test output on my machine:

$ python setdisplay.py
4 online display(s) found
------------------------------------
Display 0x478176570
------------------------------------
Display 0x478160349
------------------------------------
Display 0x478173192
------------------------------------
Display 0x478176723

So it works! Now to add some more functionality. I’m going to try to start with the method modeForDisplay , which I’ve renamed to get_mode_for_display . I’ll post later with further updates.

No Comments

Leave a Reply