Matthew Lindfield Seager

Matthew Lindfield Seager

Shortening the Feedback Loop - Automatic PDF Refresh on Source Change

I’ve been exploring ways to generate nicely formatted PDFs from a Ruby on Rails app (without trying to convert HTML to PDF). As part of that exploration I’ve been looking into Prawn, a fast and powerful PDF generator written in Ruby.

I find one of the most important parts of learning a new technology is to shorten the feedback loop between making a change and seeing the result of that change. When using Prawn out of the box the feedback loop looks a little bit like this:

  1. Write some ruby code in my text editor e.g. In hello.rb: require 'prawn'; Prawn::Document.generate("hello.pdf") { text "Hello World!!" }
  2. Switch to Terminal to run the code and compile the PDF
  3. Open the PDF (or switch to Preview to view the changes)
  4. Switch back to my text editor

Rinse and repeat.

That’s not terrible, particularly since the Preview app on macOS automatically refreshes the document when you bring it to the front, but all that keyboarding/mousing/track-padding adds up when you’re trying to learn a technology and making lots of little changes. In addition to being a little slow, most of those steps never change. In fact, steps 2 through 4 are identical every time. This process is an ideal candidate for automation, for making steps 2-4 happen automatically every time I complete step 1. Here’s how I did it (with caveats).


I started with a very basic Prawn script (based on the README) for testing:

# hello.rb
require 'prawn'

script_name_sans_extension = File.basename(__FILE__, '.rb')
Prawn::Document.generate("#{script_name_sans_extension}.pdf") do
  text "Hello World!!"
end

The first step is creating a trigger that detects when the Ruby script(s) are saved. For this I chose Guard, a command line tool for responding to file system modifications. Guard is very handy for Test Driven Development, you can set it up to run tests automatically when your code changes. That’s pretty much exactly what I want to do here!

Since I already have Ruby and Bundler configured on my machine this step was as simple as:

  1. Adding a Gemfile to my Prawn script folder:

    # Gemfile
    source '[rubygems.org](https://rubygems.org)'
    
    gem 'prawn'
    
    group :development do
      gem 'guard'
      gem 'guard-shell'
    end
    
  2. Installing the gems by running bundle (or bundle install)

  3. Creating a basic guard file template with bundle exec guard init shell

  4. Tweaking the Guardfile to execute any ruby scripts whenever they change

    # Guardfile
    guard :shell do
      watch(/(.*).rb/) do |m|
        `ruby #{m[0]}`
      end
    end
    
  5. Leaving guard running while I work on the scripts with bundle exec guard

Now whenever I save the Ruby script, Guard detects the change and immediately compiles it (the `ruby #{m[0]}` line in the script above). So that’s step 2 taken care of, now for steps 3 & 4…


Step 3 is easy on its own. You can just add a simple `open "#{m[1]}.pdf"` inside the watch block. Then, every time you save, a few moments later Preview will be brought to the front and the PDF reloaded. If you’re working on a single screen, you might want to stop the script there. Your workflow will now be:

  1. Save [,compile and view]
  2. Switch back to text editor to make more changes

Rinse and repeat.


If you’re working with multiple screens (or have one very large screen) there is a way to mostly automate step 4 as well. The main problem is we need to take note of the current frontmost application, open Preview, and then reopen the previous application.

To dynamically take note of the current application and switch back to it requires AppleScript. To call AppleScript from the shell just use osascript. We’ll also need to remove the open call we made for Step 3. Our Guardfile then becomes:

    # Guardfile
    guard :shell do
      watch(/(.*).rb/) do |m|
        `ruby #{m[0]}`
        `osascript<<EOF
          tell application "System Events"
            set frontApp to name of first application process whose frontmost is true
          end tell
          activate application "Preview"
          activate application frontApp
        EOF`
      end
    end

If you’re happy to hardcode your editor, you can skip all that malarky and just add one more open line after the one we added for step 3: `open -a "/Applications/TextEdit.app"` (replacing TextEdit.app with your editor of choice). Your Guardfile will then look like:

    # Guardfile
    guard :shell do
      watch(/(.*).rb/) do |m|
        `ruby #{m[0]}`
        `open "#{m[1]}.pdf"`
        `open -a "/Applications/TextEdit.app"`
      end
    end

I said “mostly” above because there is a downside (or two) with both these options. The main downside is that bringing your text editor back to the front brings all it’s windows to the front too. You’ll have to arrange it so that the Preview window isn’t being covered by another text editor window when the app is brought back to the foreground (hence the need for plenty of screen real estate).

Another thing I noticed is that sometimes (but not always) a different text editor window would become uppermost when the focus came back. I can’t be sure but it seemed to happen more often when the editor window I was using wasn’t on the “main” screen. Moving it over to my main display seemed to fix the issue.

Another option would be to either close all your other text editor windows or muck around with trying to specify which window to bring to the foreground. I decided not to spend any more time on it since it was working well enough for me. If you want to try it out, take a look at the accessibility methods to figure out which window is frontmost. According to one StackOverflow comment it’s something along the lines of tell (1st window whose value of attribute "AXMain" is true)


Hopefully this proves interesting to someone. Even if you don’t care about Prawn you can adapt this technique to any format that requires a compilation step. Markdown and Latex spring to mind. You could even trigger a complete publishing workflow!