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.
reader comments