Skip to content
Biz & IT

Building desktop Linux applications with JavaScript

Ars takes a close look at Seed, a new framework that allows software …

Ryan Paul | 0
Story text

A lingua franca for application extension

During his keynote presentation at OSCON last year, Ubuntu founder Mark Shuttleworth described application extensibility as an important enabler of innovation and user empowerment. Citing the Firefox web browser and its rich ecosystem of add-ons as an example, Shuttleworth suggested that the Linux community could deliver a lot of extra value by making scriptable automation and plugin capabilities available pervasively across the entire desktop stack.

The concept is very compelling and has a long history. There are many examples from which to draw inspiration, because similar capabilities are present in virtually every platform. Long ago, scripting languages called Guile and Tcl were the dominant extension languages of the Linux platform. Although both still exist today, they are somewhat anachronistic and are no longer widely used.

In today's world, demand for application extensibility is rising, and there is a strong need for interoperability between extension systems. To accommodate those requirements, it's a clear that the desktop must have a lightweight lingua franca for scripting and extension. To gain traction on the desktop, the language has to be widely known, easy-to-use, and naturally conducive to rapid development. JavaScript, which meets all of these requirements, is becoming a compelling contender for the role. The ubiquity of web development expertise guarantees broad JavaScript familiarity, and the growing trend of convergence between the desktop and the web also contribute to JavaScript's suitability as a desktop extension solution.

The developers of the open-source Qt toolkit, the underlying framework of the KDE desktop environment, figured this out a few years ago and introduced a native scripting engine based on JavaScript's syntax (it's a standards-compliant implementation of ECMA-262) directly in the toolkit. It leverages Qt's object system to provide dynamic access to native Qt widgets, with full support for Qt's signal and slot system.

We recently took a close look at how these scripting capabilities are used in Amarok, KDE's popular open source audio player. A significant amount of Amarok's functionality was implemented with scripting and, as I demonstrated in the article, it's trivially easy for end users to add a wide range of additional features.

The GNOME platform could soon have similar support for scripting. Two projects have emerged that aim to bring rich JavaScript bindings and embedded scripting functionality to GTK+ application developers. The Seed project is based on WebKit's JavaScriptCore engine and the Gjs project leverages Mozilla's SpiderMonkey engine.

GObject introspection

Both projects take advantage of a new and highly experimental introspection feature that is being integrated into GObject, the type system and object model framework that is used in GTK+ and other libraries within the GNOME ecosystem. The GObject introspection system supplies programmatically-accessible type metadata for GObject-based libraries.

Static analysis tools automatically generate the metadata by analyzing header files and extracting annotations from source code. The metadata is then stored in XML-based GObject Introspection Repository (GIR) files. One of the numerous advantages of having this type metadata available is that it dramatically reduces the complexity inherent in supplying language bindings for libraries.

One of the earliest experiments that demonstrated the viability of using GObject introspection for dynamically generating language bindings was the PyBank binding framework. GObject introspection developer Johan Dahlin showed that it was possible to generate bindings at runtime as needed using the GObject metadata. Seed and Gjs use a similar approach.

Planting the Seed

I took a close look at Seed and used it to build a few experimental utilities. It is still under very heavy development and is far from being ready for widespread production use, but it is mature enough to facilitate the development of basic GTK+ programs. For my tests, I installed Seed in a virtualized Ubuntu 8.10 environment with the Seed Personal Package Archive.

Unlike many other scripting languages, JavaScript's standard library is very slim and doesn't come with very much additional infrastructure. Developers who build GTK+ applications with Seed will have to use other GObject-based libraries from the GNOME stack in order to produce full applications. For example, native file access and basic remote data retrieval can be done with the Gio library.

By default, Seed has access to all of the libraries for which GIR data is available. The gobject-introspection-repository package in the PPA includes type metadata for a big chunk of the stack, including Cairo, Clutter, Avahi, GTK+, GConf, OpenGL, GooCanvas, WebKit, Poppler, Pango, libsoup, libnotify, and libxml2.

Seed exposes the APIs of those libraries in a JavaScript-friendly way with good support for type conversion. Object properties are accessible as regular JavaScript attributes, JavaScript functions can be connected to signals as callbacks, you can use JavaScript's key/value associative arrays to pass named parameters into constructors, and you can subclass widgets and create new GObject types that implement their own signals. This means that using GObject libraries in JavaScript is very clean and seamless.

Individual scripts can be executed with the Seed runtime, just like Python and Ruby scripts. This makes it possible to build applications entirely with JavaScript. Seed provides several functions for importing modules and displaying information at the console. The following example will display a window with a single button. When the button is pressed, it will display some text at the console.

#!/usr/bin/env seed

// Import libraries that are used by the program
Seed.import_namespace("Gtk");

// Initialize GTK+
Gtk.init(null, null);

// Create the main application window and set the title
var window = new Gtk.Window({title: "Hello World"});
// Make the program terminate when the window is closed
window.signal.hide.connect(Gtk.main_quit);

// Create a button with a label
var button = new Gtk.Button({label: "Click Me!"});

// Make the button display text when it is pressed
// It passes an inline anonymous function to the signal handler
button.signal.clicked.connect(function(w) {
  Seed.print("Hello world!");
});

// Add the button to the window and display everything
window.add(button);
window.show_all();

// Start the main GTK+ loop and initiate the program
Gtk.main();

The flexibility of JavaScript's prototype-oriented object model opens the door for some really bizarre and awesome tricks. For example, I discovered that it is possible to dynamically monkey-patch the imported GObject libraries.

As an experiment, I added a "clear" method to the GTK+ Container base class. The method iterates over the contents of the container and removes all of the items. Adding this method to the Gtk.container.prototype object automatically makes it available in all widgets that descend from the Container class, including frames and boxes. I also experimented with extending the behavior of existing methods by redefining them—a capability that is extremely useful for certain kinds of debugging tasks.

#!/usr/bin/env seed

Seed.import_namespace("Gtk");

Gtk.init(null, null);

// This demonstrates how to add a new function to an object that is imported
// from GObject-based libraries. The function will be available in all
// subclasses of the Container object, including boxes and frames.
Gtk.Container.prototype.clear = function() {
  var container = this;
  container.foreach(function(i) {container.remove(i)});
}

// This demonstrates how to extend or reeimplement a function in an object
// that is imported from GObject-based libraries. This is a bit dangerous
// but it is sometimes very useful for debugging purposes.
Gtk.Container.prototype._add = Gtk.Container.prototype.add;
Gtk.Container.prototype.add = function(obj) {
  Seed.print("Adding an object to " + this);
  this._add(obj);
}

var win = new Gtk.Window({title: "Monkey patch test"});
win.signal.hide.connect(Gtk.main_quit);

var vb = new Gtk.VBox({spacing: 5});
var frame = new Gtk.Frame();
var btn = new Gtk.Button({label: "Click Me!"});

btn.signal.clicked.connect(function(w) {vb.clear()});

frame.add(btn);
vb.pack_start(frame);
win.add(vb);

win.show_all();
Gtk.main();

The example above is admittedly eccentric and it's not the kind of thing that you would want to do in every application, but I think it's a very compelling demonstration of the power and mutability that developers get by using GObject-based libraries in JavaScript.

A complete application

To test Seed's viability for a more substantive task, I decided to build a little Twitter search utility, much like the one I made for my recent article about JavaFX. The utility allows the user to input a query string, and then it will display the most recent Twitter messages that match the query. It uses Gio to retrieve JSON data from Twitter's search service and then it parses the data and displays the messages in a scrollable pane.

#!/usr/bin/env seed

// Import libraries that are used by the program
Seed.import_namespace("Gtk");
Seed.import_namespace("Gdk");
Seed.import_namespace("Gio");

// Pretty.js is John Resig's date display library. It downloaded it and
// put it in the same directory as this script. You can easily use a lot
// of existing JS libs and import them into Seed applications at runtime.
Seed.include("pretty.js");

// Initialize GTK+
Gtk.init(null, null);

// Create the main application window and set the title
var window = new Gtk.Window({"title": "Twitter Search", "border-width": 5});
// Make the program terminate when the window is closed
window.signal.hide.connect(Gtk.main_quit);

// Modify the default style so that TextView widgets look like labels
Gtk.rc_parse_string(
  'style "tv" {base[NORMAL] = @bg_color} widget_class "*GtkTextView" style "tv"');

// Define a function for retrieving Twitter search info and parsing the data
// The get_contents method is not part of the underlying library, Seed adds it
// as a convenience mechanism for JavaScript applications. There are several
// Seed-specific library additions. You can see them all in /usr/share/seed.
function twitter_search(query) {
  return JSON.parse(Gio.file_new_for_uri(
    "http://search.twitter.com/search.json?q=" + query).read().get_contents());
}

// This function generates the GTK+ widgets that display the retrieved messages
function make_block(data) {
  var vbox = new Gtk.VBox({"spacing": 10, "border-width": 5});

  // The text styling for the heading is done with simple Pango markup.
  var heading = new Gtk.Label({
    "use-markup": true,
    "label": "" + data.from_user + " " +
             "(" + prettyDate(data.created_at) + ")"
  });

  // The message text is displayed in a TextView widget because the GTK+ label
  // widget completely sucks at wrapping text. The TextView will look like a
  // label because of the RC change at the top of the script.
  var message = new Gtk.TextView({"wrap-mode": 2, "editable": false});
  message.buffer.text = data.text;
  
  heading.set_alignment(0, 0);
  vbox.pack_start(heading);
  vbox.pack_start(message);

  var frame = new Gtk.Frame({"border-width": 5});
  frame.add(vbox);
  return frame;
}

// Create the message container and put it in a scrollable window
var messages = new Gtk.VBox();
var scroll = new Gtk.ScrolledWindow();
scroll.add_with_viewport(messages);
scroll.set_policy(1, 1);

// Create the input textbox and the search button
var textbox = new Gtk.Entry();
var button = new Gtk.Button({"label": "_Search", "use-underline": true});

// Define the behavior for the button press by associating an anonymous
// function with the button's click signal handler
button.signal.clicked.connect(function(w) {
  // Use the string in the textbox as a query for a Twitter search
  var results = twitter_search(textbox.get_text()).results
  // Clear the contents of the message list
  messages.foreach(function(m) {messages.remove(m)});
  // Generate the message blocks and append each one to the message list
  results.forEach(function(m) {messages.pack_start(make_block(m))});
  // Show all of the messages that have been added to the list
  messages.show_all();
});


// Pack the remaining widgets into the window layout
var searchbox = new Gtk.HBox();
searchbox.pack_start(textbox, true, true);
searchbox.pack_start(button);

var layout = new Gtk.VBox({"spacing": 5});
layout.pack_start(searchbox);
layout.pack_start(scroll, true, true);

window.add(layout);
window.show_all();

// Start the main GTK+ loop and initiate the program
Gtk.main();

There are a few specific things in the program that are worth pointing out. It uses John Resig's pretty.js library to display how much time has elapsed since each message was posted. This is significant because it shows how easy it is to take existing JavaScript code and use it in a Seed application.

I originally wanted to make the program display the profile image of each user alongside the message, but I ran into some difficulty. The most practical way to do it would be to use Gio to retrieve the remote image data and then use a PixbufLoader to translate the data into a pixbuf that can be displayed with a Gtk.Image widget. Unfortunately, I ran into some trouble with PixbufLoader and couldn't get it to read the data.

In this case, my problem seemed to be a Seed type handling bug of some kind. As an aside, it's worth mentioning that this kind of thing will hopefully be less problematic in the future when Gio is supported more pervasively in GTK+.

Another interesting bit that you might have noticed is that the program uses "forEach" in one case and "foreach" in another. This is because JavaScript's array object uses forEach but GTK+ container objects use foreach. It's kind of tricky and you have to be really mindful of those subtle differences in API conventions.

This sample script is designed to run on top of Seed as an application, but that's only one usage scenario. It's possible to embed libseed in existing C applications and use it as an extension system. The lead Seed developer, Robert Carr, is currently collaborating with the developers of GNOME's Epiphany web browser to make it possible for Epiphany extensions to be written in JavaScript via Seed.

Carr is also collaborating with the developers of Vala, an object-oriented programming language compiler and framework that offers a syntax which resembles C# and compiles down to native C code. Seed could eventually make it possible to seamlessly embed JavaScript in Vala code. These experiments illuminate the significant potential of using JavaScript as an extension language for building plugins and coding higher-level application logic.

Seed and Gjs

Gjs is another major implementation of a GObject bridge for JavaScript. It has a lot in common with Seed and aims to serve a similar purpose. Like Seed, it leverages GObject introspection for exposing native APIs and it is designed to be easily embedded in existing C applications. The primary difference between Seed and Gjs is the underlying JavaScript engine. Seed uses WebKit's JavaScriptCore whereas Gjs uses Mozilla's SpiderMonkey engine.

Seed and Gjs each offer a slightly different set of advantages and it isn't immediately clear which one will gain dominance. Seed currently has the upper hand in runtime performance and is also currently more complete, more thoroughly documented, and more accessible to third-party developers. WebKit is rapidly becoming the preferred solution for embedding HTML rendering in conventional desktop applications, so adopting JavaScriptCore for embedded scripting seems like a natural choice.

The most significant advantage of Gjs is that SpiderMonkey is on the cutting edge of the JavaScript standard and supports several very useful and important new JavaScript features that aren't yet included in WebKit's JavaScriptCore. Some examples are support for the "let" keyword, generators and iterators, and array comprehensions. These syntactic features simplify application development and make JavaScript more powerful.

Gjs is already being used extensively by Litl, a secretive startup that employs several prominent GNOME developers. Litl developer Havoc Pennington wrote a mailing list post stating that it would not be possible at this point for Litl to adopt Seed instead of Gjs, but he recommended several potential areas where the developers behind the two projects could collaborate to boost interoperability. These proposals were well-received and appear to have helped build consensus on several issues.

Carr has agreed to change Seed's enum format and import approach to make them consistent with Gjs. Carr says that there are ongoing discussions about how to bring the two into alignment in several other areas, including signal handling. The developers are also evaluating the possibility of moving the two projects into a single repository, collaborating on a shared test suite, and establishing a uniform embedding API for C applications. There is clearly some good progress being made on interoperability between the two solutions.

Conclusion

Seed and Gjs have the potential to bring a lot of value to the GNOME platform. The availability of a desktop-wide embeddable scripting language for application extension and plugin writing will enable users to add lots of rich new functionality to the environment. As this technology matures and it becomes more tightly integrated with other language frameworks such as Vala, it could change the way that GNOME programmers approach application development. JavaScript could be used as high-level glue for user interface manipulation and rapid prototyping while Vala or C are used for performance-sensitive tasks.

Further reading

Photo of Ryan Paul
Ryan Paul Ars Editor Emeritus
Ryan is an Ars editor emeritus in the field of open source, and and still contributes regularly. He manages developer relations at Montage Studio.
0 Comments