Taking screenshots of web pages with macruby

Whilst playing around with the very exciting macruby last weekend, I thought I'd try building a web page screenshot grabber, based on Ben Curtis' code. The code was very easy to change translate from rubycocoa, looks cleaner and seems to work really well:

framework 'Cocoa'
framework 'WebKit'

class Snapper
  attr_accessor :options, :view, :window
  
  def initialize(options = {})
    @options = options
    initialize_view
  end
  
  def save(url, file)
    view.setFrameLoadDelegate(self)
    # Tell the webView what URL to load.
    frame = view.mainFrame
    req = NSURLRequest.requestWithURL(NSURL.URLWithString(url))
        frame.loadRequest req
    
    while view.isLoading  && !timed_out?
      NSRunLoop.currentRunLoop.runUntilDate NSDate.date
    end
    
    if @failedLoading
      puts "Failed to load page at: #{url}"
    else
      docView = view.mainFrame.frameView.documentView
      docView.window.setContentSize(docView.bounds.size)
      docView.setFrame(view.bounds)
    
      docView.setNeedsDisplay(true)
      docView.displayIfNeeded
      docView.lockFocus
    
      bitmap = NSBitmapImageRep.alloc.initWithFocusedViewRect(docView.bounds)
      docView.unlockFocus

      # Write the bitmap to a file as a PNG
      representation = bitmap.representationUsingType(NSPNGFileType, properties:nil)
      representation.writeToFile(file, atomically:true)
      bitmap.release
    end
  end
  
  private
  
  def webView(view, didFailLoadWithError:error, forFrame:frame)
    @failedLoading = true
  end
  
  def webView(view, didFailProvisionalLoadWithError:error, forFrame:frame)
    @failedLoading = true
  end
  
  def initialize_view
    NSApplication.sharedApplication    
    
    self.view = WebView.alloc.initWithFrame([0, 0, 1024, 600])
    self.window = NSWindow.alloc.initWithContentRect([0, 0, 1024, 600],
      styleMask:NSBorderlessWindowMask, backing:NSBackingStoreBuffered, defer:false)
      
    window.setContentView(view)    
    # Use the screen stylesheet, rather than the print one.
    view.setMediaStyle('screen')
    # Set the user agent to Safari, to ensure we get back the exactly the same content as 
    # if we browsed directly to the page
    view.setCustomUserAgent 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-us)' +
        'AppleWebKit/531.21.8 (KHTML, like Gecko) Version/4.0.4 Safari/531.21.10'
    # Make sure we don't save any of the prefs that we change.
    view.preferences.setAutosaves(false)
    # Set some useful options.
    view.preferences.setShouldPrintBackgrounds(true)
    view.preferences.setJavaScriptCanOpenWindowsAutomatically(false)
    view.preferences.setAllowsAnimatedImages(false)
    # Make sure we don't get a scroll bar.
    view.mainFrame.frameView.setAllowsScrolling(false)
  end
  
  def timed_out?
    @start ||= Time.now
    (Time.now.to_i - @start.to_i) > (options[:timeout] || 30)
  end
end

To use:

macruby -r snapper.rb -e "Snapper.new.save('http://tomafro.net', 'tomafro.net.png')"

The next step is to add some command line options, then try compilation and deployment with macrubyc and macruby_deploy

Tip: Open new tab in OS X Terminal

Another simple shell function, this time just for OS X.

Usage is simple: tab <command> opens a new tab in Terminal, and runs the given command in the current working directory. For example tab script/server would open a new tab and run script/server.

tab () {
  osascript 2>/dev/null <<EOF
    tell application "System Events"
      tell process "Terminal" to keystroke "t" using command down
    end
    tell application "Terminal"
      activate
      do script with command "cd $PWD; $*" in window 1
    end tell
  EOF
}

Kernel specific ZSH dotfiles

I work on a number of different machines, OS X based for development and Linux based for hosting. I've added various aliases and other commands to my shell, and use a github repository to share this configuration between these machines.

This works well, but while most commands work commonly across Linux and OS X, there are some nasty differences. One example is ls which takes different arguments on both systems; the default ls alias I use on OS X doesn't work on Linux. So how can we accommodate these differences, without removing all the shared configuration?

The answer is really simple. Create kernel specific configuration files, and use a case statement to load the correct one. The main obstacle was finding a way to distinguish between each kernel. In the end I found the $system_name environment variable, which is set on both OS X and the servers I use.

Here's the code:

case $system_name in
  Darwin*)
    source ~/.houseshare/zsh/kernel/darwin.zsh
    ;;
  *)
    source ~/.houseshare/zsh/kernel/linux.zsh
    ;;;
esac

As I said, simple.

dscl - the easy way to add hosts on OS X

As a web developer, I often want several host names pointing at my local machine whilst developing and testing applications. I may want to use Apache virtual hosts to serve multiple apps at once, or use subdomains to distinguish different accounts within a single application.

Previously, to set these host names up, I would manually edit /etc/hosts, adding entries like:

127.0.0.1      twitter-killer.localhost
127.0.0.1      my-url-shortener-is-better-than-yours.localhost
127.0.0.1      yet-another-half-baked-idea.localhost

This worked well on one level, but on another it just seemed wrong. It's very manual, prone to error and pretty hard to script. Recently though, thanks to a hint from Chris Parsons, I've found another way: using dscl.

dscl, or Directory Service command line utility to give it its full name, has many uses. For a web developer, the most relevant is probably the ability to add, list and create local directory entries under /Local/Defaults/Hosts in the directory (not the file system). These act like lines in /etc/hosts but can be manipulated easily from the command line.

To add an entry (easily the most interesting command) use:

sudo dscl localhost -create /Local/Default/Hosts/twitter-killer.localhost IPAddress 127.0.0.1

Once entries have been added, listing them is just as simple:

sudo dscl localhost -list /Local/Default/Hosts IPAddress

This produces a list in the form:

twitter-killer.localhost                         127.0.0.1      
my-url-shortener-is-better-than-yours.localhost  127.0.0.1      
yet-another-half-baked-idea.localhost            127.0.0.1      

Finally, you can remove entries with:

sudo dscl localhost -delete /Local/Default/Hosts/twitter-killer.localhost

Once you've mastered these commands, the possibilities are endless. Here's a rake task to add all subdomains used in a project:

class Application
  def self.subdomains
    Client.all.collect {|client| client.subdomain }
  end
end

namespace :subdomains do
  task :add do
    Application.subdomains.each do |subdomain|
      `sudo dscl localhost -create /Local/Default/Hosts/#{subdomain}.app.localhost IPAddress 127.0.0.1`
    end
  end
  
  task :remove do
    Application.subdomains.each do |subdomain|
      `sudo dscl localhost -delete /Local/Default/Hosts/#{subdomain}.app.localhost`
    end
  end
end

Or if you're using passenger for development, you can use a tool like James Adam's hostess to automatically set up the host entry and virtual host entry for a site in one simple command.