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:
require 'prawn'; Prawn::Document.generate("hello.pdf") { text "Hello World!!" }
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:
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
Installing the gems by running bundle
(or bundle install
)
Creating a basic guard file template with bundle exec guard init shell
Tweaking the Guardfile to execute any ruby scripts whenever they change
# Guardfile
guard :shell do
watch(/(.*).rb/) do |m|
`ruby #{m[0]}`
end
end
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:
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!