Wishing for a Command Line Interface

I have a few courses that share materials and ideas. I used to use the age-old practice of cut and paste to share the slides. As handy as it is, cut and paste causes trouble. If I’m teaching TDD in C and make a change to a discussion that applies also to C++ or or some other language, I’ve got a problem. I’ll never remember to put the change in the other places where the change is needed. This means that later I find myself in front of a group of C++ programmers about to make a big point, and find that the dramatic conclusion added to the C version I made is not in the current slide deck.

As with code, duplication in presentations is a liability. So I needed to refactor my training materials, and need a practical way to deliver them and provide a PDF handout of the presentation.

I use Apple Keynote for my presentations. To minimize duplication in my Keynote files, I needed to split the big Keynote files into modules. Then I could build a specific course’s Keynote from the modules through some concatenation process, but no such luck. I could not find a way to concatenate Keynote files. I wish I could say Keynote helped me solve this problem. Help, not really. But it did not prohibit me from finding a less ideal solution.

(I tried the concatenation approach with apple automater and applescript, and could not find a decent solutions. If you know one, let me know).

I settled on an approach using a Keynote homepage and hyperlinks. This is OK, but it left me with the problem of a single PDF for printing the course handout. Manually it is a pain to create the PDF course materials handout, with 10 to 15 separate Keynote files. Not only a pain, but I am sure the quality of my handouts created manually would suffer. So, I need to automate PDF production so that I can rebuild my handouts at anytime.

Thankfully, using a mac, I have the full power of unix under the hood. Concatenating, watermarking, and covering a set of PDF files is no big deal with an open source command line tool called pdftk and some custom bash scripting to glue it all together. But there is one weak link in this process; it is getting a properly formatted PDF for each of the Keynote files.

When I set off on this journey, I thought it would be a breeze to interact with Keynote on the command line or applescript. Unfortunately there is no command line interface that I can use to ask Keynote ’09 to create a PDF. But there is applescript. Thwarted by the incomplete object model exposed by Keynote to applescript, I could not just interact with Keynote on a programmer level, I have to use applescript to pretend to be a user pressing keys and clicking boxes.

I thought google would quickly lead me to the needed how-to; but I must lack the Keynote/applescript vocabulary to find the perfect example with google. As I searched and researched, I discovered that many of the people that posted applescript/Keynote automation examples must have never run them. Many did not work as presented.

After many searches and some stick-to-itiveness, I put together an applescript to generate a single PDF with the settings I needed. Before I show you the code, it is important to know there are a few prerequisites for this script to work:

  1. In the Keynote print dialog, create a preset named ‘4 per page’, that tells Keynote to print with ‘Layout’ setting that has ‘Pages per sheet’ attribute set to 4.
  2. In /System Preferences/Keyboard/Keyboard Shortcuts/Application Shortcuts/Keynote define key-press control-command-option-P to mean “Save as PDF…”
  3. Before running the script, print as PDF some Keynote file into the directory you want the PDFs to go. This established where the PDF is placed on subsequent keynote print operations. (I might try to make this controlled by the script, but directory navigation is not so simple in Keynote driven by applescript. Let me know if you know an easy way.)

The script takes two inputs: the path of the Keynote presentation file and a second string parameter that should be set to “First” on the first call, and something else on subsequent calls. Here is the refactored top level script:


on run {aFile, fileOrder}
  tell application "Keynote"
    open aFile
    try
      my bringKeynoteToFront()
      my openPrintDialog()
      my setPrintOptions(fileOrder)
      my saveToPdf()
      my closeTheFile()
    end try
  end tell
end run

This moves the focus from the shell terminal window to Keynote.

on bringKeynoteToFront()
  tell application "Keynote"
    activate
  end tell
end bringKeynoteToFront

Pretending to be the user, the script presses command-p, the built-in print command keyboard shortcut.

on openPrintDialog()
  tell application "System Events"
    tell process "Keynote"
      keystroke "p" using {command down}
      repeat until exists sheet 1 of window 1
      end repeat
    end tell
  end tell
end openPrintDialog

This was a little weird to come up with. Even though I found examples of setting a checkbox to a specific state, it does not work. So you have to query the checkbox and click it to the desired state. (If print dialog presets actually remembered all the settings, I would not have had to fiddle with checkboxes in applescript.) Also notice the delays and the waiting for certain text to appear. It's real easy for the script to get ahead of the GUI.

The quoted text below is the actual text in the dialog box. It makes me think this won't work on a non-English installation.

-- set the print options
-- depends on a user defined preset called "4 per page"
on setPrintOptions(fileOrder)
  if fileOrder = "First" then
    tell application "System Events"
      tell process "Keynote"
        repeat with checkboxNumber from 1 to 13
          set box to checkbox checkboxNumber of sheet 1 of window 1
          if value of box is 1 then click box
        end repeat
        set borders to checkbox "Add borders around slides" of 
                                  sheet 1 of window 1
        set margins to checkbox "Use page margins" of sheet 1 of 
                                  window 1
        set individual_slides to radio button "Individual Slides" 
                                  of radio group 1 of sheet 1 of window 1
        set presets_button to pop up button 3 of sheet 1 of 
                                  window 1
        if value of individual_slides is 0 then click 
                                            individual_slides
        if value of borders is 0 then click borders
        if value of margins is 0 then click margins
        if not (value of presets_button is "4 per page") then
          keystroke tab
          delay 0.1
          keystroke "4"
          delay 1
          repeat until value of presets_button is "4 per page"
          end repeat
        end if
      end tell
    end tell
  end if
end setPrintOptions

Here we used the custom applications shortcut key for "Save as PDF...". You'll also find some delays and reading of the GUI text to keep synchronized.

-- save to PDF 
-- depends on a user defined key for "Save as PDF…"
on saveToPdf()
  tell application "System Events"
    tell process "Keynote"
      keystroke "p" using {command down, option down, control down}
      delay 1
      repeat until exists window "Save"
      end repeat
      keystroke return
      delay 2
      repeat until not (exists window "Save")
      end repeat
    end tell
  end tell
end saveToPdf

Finally, close the file with command-w.

on closeTheFile()
  tell application "System Events"
    tell process "Keynote"
      keystroke "w" using {command down}
      delay 0.1
    end tell
  end tell
end closeTheFile

If you are a real applescript expert look this over and tell me the easy way, please!

The applescript entry function, on run shown above, gets called from a bash script like this.


    printToPdf=${APPLE_SCRIPTS_DIR}/PrintKeynoteToPdfDefault-lion.scpt
    file_order="First"
    for file in  $KEYNOTE_NAMES ; do
        echo "Print PDF ${file} ${file_order}"
        osascript ${printToPdf} ${KEYNOTE_PATH}/${file} $file_order
        if [ ! -f ${PDF_TEMP}/${file}.pdf ] ; then
            echo "File not created ${file}.pdf"
            exit
        fi  
    file_order="Not first"
    done

These names should be self-explanatory if you happen to know bash.

  • APPLE_SCRIPTS_DIR
  • KEYNOTE_PATH - Path to the Keynote files
  • KEYNOTE_NAMES - This is a whitespace separated list of Keynote filenames
  • PDF_TEMP
  • Tools that helped

  • Apple Accessibility Inspector - found in /Developer/Applications/Utilities/Accessibility Tools - This helped me find the GUI element names and expected values. My script would not exist without this tool.
  • Applescript editor event log - Handy for running the script stand alone. This is not a sustainable test approach, but I would tweak the script during test and debug (it hurts to say that; I prefer TDD to debugging, but with this I was debugging).
  • Applescript Disappointment Summary

    I groused a little

  • Set a checkbox or radio button- there were examples on the various discussion forums about setting checkboxes, with examples. Unfortunately they don't work. This does not even work for setting a checkbox

    • set value of borders to 1
  • Testing checkboxes - Because you cannot just set the option to the desired state, it is necessary to query the state first. So again I went looking for examples. They showed comparing the state with true or false which does not work, at least in the latest Mac OSX (Lion). That cost a few hours, but I learned a lot. The UIElementInspector helped figure it out. Though figuring out button numbers was strictly trial and error.
  • No decent way to wait for events and sync with the UI - I had to use polling loops and delays to wait for actions to complete, like pressing TAB, selecting preset "4 per page", waiting for save dialogs and progress windows. You can see these polling constructs in openPrintDialog, setPrintOptions, and saveToPdf. I did not bother adding timeouts, though my script did freeze while I was trying to get it to work.
  • Extracting functions added to the duplication - I thought I could create a helper function to test and set a checkbox, but I either gave up too soon or its not possible. I wanted to wrap this code in an intention revealing function to make up for the lack of a way to set a check box. But a borders instance cannot be passed to a helper function.

    • if value of borders is 0 then click borders

    It appears that a function must have the full hierarchy of tell statements adding to the duplication. I decided to accept that duplication between my helper functions just to make the code more readable. Originally the code was one long function like this:

    
    on run {aFile, fileOrder}
      tell application "Keynote"
        open aFile
        try
        tell application "Keynote"
          activate
        end tell
          tell application "System Events"
            tell process "Keynote"
              keystroke "p" using {command down}
              repeat until exists sheet 1 of window 1
              if fileOrder = "First" then
                repeat with checkboxNumber from 1 to 13
                  set box to checkbox checkboxNumber of sheet 1 of window 1
                  if value of box is 1 then click box
                end repeat
                set borders to checkbox "Add borders around slides" of 
                                          sheet 1 of window 1
                set margins to checkbox "Use page margins" of sheet 1 of 
                                          window 1
                set individual_slides to radio button "Individual Slides" of 
                                          radio group 1 of sheet 1 of window 1
                set presets_button to pop up button 3 of sheet 1 of window 1
                if value of individual_slides is 0 then 
                                          click individual_slides
                if value of borders is 0 then click borders
                if value of margins is 0 then click margins
                if not (value of presets_button is "4 per page") then
                  keystroke tab
                  delay 0.1
                  keystroke "4"
                  delay 1
                  repeat until value of presets_button is "4 per page"
                  end repeat
                end if
              end if
              keystroke "p" using {command down, option down, control down}
              delay 1
              repeat until exists window "Save"
              end repeat
              keystroke return
              delay 2
              repeat until not (exists window "Save")
              end repeat
              keystroke "w" using {command down}
              delay 0.1
            end tell
          end tell
        end try
      end tell
    end run
    
    

    Applescript seems pretty lacking, unless I am missing something. If you know a better way to do some of these things, please post a comment. I expect this to be fragile and it is likely to only work on MacOSX Lion with an English language installation.

    Having to interact with Keynote in this clunky way makes me think of the testers for Keynote have a tough challenge writing automated tests. Keynote should have a command line interface or viable applescript interface to test these important features of Keynote.
    Even though its a bit clunky, having this automated was well worth my efforts. Manual tedious repetitive processes lead to inconsistent results and quality.

  • 6 thoughts on “Wishing for a Command Line Interface

    1. Wow James. Thanks for this post. I tried this also some time ago, but gave up a lot sooner than you did 🙂 I think I might actually use your script 🙂

    2. This is the sort of thing I use Keyboard Maestro, combined with AppleScript or Automator, to do.

      But you did a great job with your script. I’ve never had the patience to do interface stuff through AppleScript, which is why I use Keyboard Maestro. 🙂

    3. I’m not sure if this works though, but have you tried this as substitute for closing your keynote? (code below):
      —-for closing keynote document
      tell application “Keynote”
      close document 1
      end tell
      —-for closing keynote
      tell application “Keynote”
      quit
      end tell
      The “document” is somehow universal (works with word, safari, etc). Though you may need to look at the appropriate object (for excel, it’s workbook). To get the right object, in applescript, press command + shift + O to get the dictionary for keynote (hopefully it exists).

      —-You’re code—-
      on closeTheFile()
      tell application “System Events”
      tell process “Keynote”
      keystroke “w” using {command down}
      delay 0.1
      end tell
      end tell
      end closeTheFile

    4. For print settings, if your applescript has the Keynote dictionary, it should have something like this in the objects:
      ——
      print?v : Print the specified object(s)
      print specifier : list of objects to print
      [with properties print settings] : the print settings
      [print dialog boolean] : Should Microsoft Excel show the print dialog?
      ——
      You should be able to set the print settings which i think is your main concern.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Be gone spammers * Time limit is exhausted. Please reload the CAPTCHA.