Biz & IT —

U CAN HAS LOLcats on Linux with Ruby and GTK

Learn how to bring LOLcats to your desktop with Ruby and GTK in this simple …

There are some web sites that I find myself visiting repeatedly throughout the day. One of those web sites is the venerable I Can Has Cheezburger (ICHC) blog, which offers up a steady stream of captioned cat pictures for the amusement of readers. I eventually grew weary of opening up a browser window and navigating to the site every time I wanted my LOLcat fix, so I decided to bring ICHC directly to my desktop by making a simple utility that uses the web site's RSS feed.

As many of my readers know, I'm a very enthusiastic fan of dynamic scripting languages like Python and Ruby. Used in conjunction with the GTK application development toolkit, object-oriented scripting languages provide a very versatile framework for rapid application development. Ruby is particularly nice because it offers a few syntactic features that make GTK programs a bit more concise. For instance, I like being able to use blocks for callbacks and I like being able to condense assignment and interface packing operations into single lines.

I decided to use Ruby and GTK for my LOLcat desktop utility. The program itself is relatively simple—it retrieves the images from ICHC's RSS feed at a set interval and then displays them to the user and scales them so that they fit to the width of the window. Ruby's Net::HTTP object makes it trivially easy to retrieve remote content. I pass a string with the RSS feed URL into the URI::parse method and then I call Net::HTTP.get_response with the URI object as a parameter. That gives me the raw XML in string format.

require "net/http"

$feed_url = "http://feeds.feedburner.com/ICanHasCheezburger"
puts Net::HTTP.get_response(URI.parse($feed_url)).body

There are a few libraries out there for manipulating XML in Ruby, but I prefer REXML, a native Ruby XML parser that supports XPath and provides a DOM API that lets developers leverage Ruby's syntactic sugar. In my program, I create a REXML::Document object to parse the XML from the RSS feed and then I use the REXML::Elements.each method to iterate over "media:content" elements. Next, I extract the URL attribute from each of the media:content elements. That gives me all of the image URLs, but now I have to find a way to display the images.

require "rexml/document"
require "net/http"

$feed_url = "http://feeds.feedburner.com/ICanHasCheezburger"

REXML::Document.new(Net::HTTP.get_response(
  URI.parse($feed_url)).body).elements.each("//media:content") do |e|
    puts e.attributes["url"]
end

I initially considered using Gtk::Image objects to display the pictures, but I decided that it would be easier to handle the output display with HTML. The GtkMozEmbed object allows developers to embed a Gecko HTML renderer as a widget in regular GTK applications. Gecko is the rendering engine used by the Firefox web browser. Unfortunately, Gecko tends to be a bit resource intensive. A very good alternative would be the GTK port of WebKit, but that's not quite ready for widespread use yet.

The GtkMozEmbed component is generally used to display remote content loaded with a URL, but it can also be used to display programatically generated HTML content. To put your own content into a GtkMozEmbed object, you open a stream to a blank page, append your own HTML data, and then close the stream.

require "gtkmozembed"
require "gtk2"

(window = Gtk::Window.new).signal_connect("destroy") {Gtk.main_quit}
gecko = Gtk::MozEmbed.new
window.add(gecko).show_all

gecko.open_stream "file:///", "text/html"
gecko.append_data "<h1>This is a test</h1>"
gecko.close_stream

Gtk.main

The next challenge is getting the program to refresh the display at a set interval. There are some easy ways to do timers with Ruby, but I have found that it's generally hard to make those work well with a GTK main loop, so I just use GTK's own timer mechanism. The Gtk.timeout_add method takes an interval as an argument and allows the developer to provide a block which will be evaluated on that interval. The interval parameter unit is milliseconds, so you generally multiply by 60,000 if you want to specify a certain number of minutes. If the block returns true, then the timer will restart after the operations are performed, otherwise the timer will stop. The Gtk.timeout_add method returns a timeout handler ID, which can be used to remove the timer.

require "gtk2"

# Creating a timer
timer = Gtk.timeout_add(60000) {puts "Timeout!"; true}

# Removing a timer
Gtk.timeout_remove(timer)

# Timers only work in a GTK main loop
Gtk.main

The program allows the user to change the interval by altering a numerical value displayed in a Gtk::SpinButton widget. To bind the SpinButton value to the timer, one has to connect a callback to the value-changed signal and use that to remove the existing timer and create a new one with the new value.

require "gtk2"

(window = Gtk::Window.new).signal_connect("destroy") {Gtk.main_quit}
interval = Gtk::SpinButton.new(1,100,1)
window.add(interval).show_all

interval.signal_connect("value-changed") do |w|
  Gtk.timeout_remove($timer) if $timer
  $timer = Gtk.timeout_add(60000 * w.value) {puts "Timeout!"; true}
end

Gtk.main

Those aspects of the program represent virtually all of its functionality. The rest of the code is primarily for creating the toolbar and other aspects of the user interface. The following is the complete source code for the application with comments that explain how it works:

#!/usr/bin/env ruby

require "net/http"
require "rexml/document"

require "gtk2"
require "gtkmozembed"

$feed_url = "http://feeds.feedburner.com/ICanHasCheezburger"

class DeskCat < Gtk::Window
  def initialize
    super Gtk::Window::TOPLEVEL
    # Make the program end when the window is closed
    signal_connect("destroy") {Gtk.main_quit}
    # Set the window title and default size
    self.title = "You Has LOLCAT"
    set_default_size(300,500)
    # Add a toolbar, Gecko rendeirng widget, and statusbar to the window
    self << vbox = Gtk::VBox.new
    vbox.pack_start @toolBar = Gtk::Toolbar.new, false, false
    vbox.pack_start @webBrowse = Gtk::MozEmbed.new
    vbox.pack_start @statusBar = Gtk::Statusbar.new, false, false
    # Populate the toolbar
    @toolBar.append(Gtk::ToolButton.new(Gtk::Stock::REFRESH)).signal_connect("clicked") {update}
    @toolBar.append(Gtk::ToolButton.new(Gtk::Stock::QUIT)).signal_connect("clicked") {Gtk.main_quit}
    @toolBar.append(Gtk::ToolButton.new(Gtk::Stock::HOME)).signal_connect("clicked") {fork {`firefox 'http://icanhascheezburger.com'`}}
    @toolBar.append Gtk::SeparatorToolItem.new
    @toolBar.append Gtk::ToolItem.new.add(Gtk::Label.new("Update interval: "))
    @toolBar.append Gtk::ToolItem.new.add(@updateInterval = Gtk::SpinButton.new(1,100,1))
    # Adjust the timer when the update interval value is changed
    @updateInterval.signal_connect("value-changed") {|w| setup_timer}
    # Set the default update interval value to 30
    @updateInterval.value = 30
  end

  def setup_timer
    # If a timer is already set up, remove it
    Gtk.timeout_remove(@timeOut) if @timeOut
    # Create a timer that causes update every x number of minutes
    @timeOut = Gtk.timeout_add(
      60000 * @updateInterval.value) {self.update; true}
  end

  def update
    # Load and parse the RSS content
    doc = REXML::Document.new(Net::HTTP.get_response(URI.parse($feed_url)).body)
    # Set the time of the last update in the toolbar
    @statusBar.push(0, Time.now.strftime("Last update: %I:%M:%S %p"))
    # Prepare to push html data into the Gecko widget
    @webBrowse.open_stream "file:///", "text/html"
    # Put the pictures in the Gecko widget...
    @webBrowse.append_data %(<body style="background-color: #F7F7F7">)
    # Iterate through all of the media:content elements in the feed
    doc.elements.each("//media:content") do |x|
      # Extract the URL of the image
      img = x.attributes["url"]
      # Generate the html for an image and add it to the Gecko widget
      @webBrowse.append_data %(<div style="border: 3px outset grey"><img width="100%" src="#{img}" /></div><br />)
    end
    @webBrowse.close_stream
  end
end

# Create the window, set the timer, and perform an update
w = DeskCat.new.show_all; w.setup_timer; w.update
Gtk.main

The source code is impressively concise and relatively intuitive. Not including comments, blank lines, and lines that only contain the end keyword, the entire program is only 30 lines of code. It also worth noting that since the program is designed to use standard RSS feeds, it will work perfectly with feeds from Flickr and possibly other sites. One need only replace the $feed_url variable value with a different URL. A more complete version of this program could potentially offer a preference dialog which enables the user to specify the feed address and supports preference persistence with GConf, but I'll leave that as an exercise to the reader.

The Ruby GTK bindings are not installed by default in any mainstream Linux distributions, but are generally easy to install from package repositories. To run this application on Ubuntu, you will need to install the ruby-gnome2 and libgtk-mozembed-ruby packages. This tutorial is intended to serve as a starting point for development with Ruby and GTK, but more comprehensive documentation and other tutorials are available from the Ruby-GNOME2 web site.

Channel Ars Technica