JakubArnold

Light Table Plugin Tutorial

Jan 13, 2014

I've been playing around with Light Table since the day its source code was released (even made a tiny Ruby plugin).

First of all, Light Table is based on the BOT architecture. Which means there are three core concepts: behaviors, objects and tags. If you have any experience with Node.js or event driven programming, you'll have an easy time understanding the concepts.

Imagine you have a button which listens on a click event and displays a notice to the user when it's clicked

Using jQuery that could be as simple as the following

<input class="my-button" type="submit" value="Do work"/>
$(".my-button").click(function() {
  showProgress("I'm doing some heavy lifting");
});

But there are problems with this approach, especially from the Light Table's point of view. First of all there's no way to see the callback after it's been attached to the element. Which means you also can't change it easily at runtime. BOT allows us to decouple the object (the button) from the actual behavior it triggers (click).

Here's an implementation in ClojureScript. If you want to follow along with the tutorial, create a new file, for example /tmp/tutorial.cljs, press Ctrl-Space, type Add Connection and select Light Table UI. This will allow you to evaluate the ClojureScript directly into the running Light Table instance. But before continuing, add the following requires at the top of your file.

(ns lt.tutorial
  (:require [lt.object :as object]
            [lt.objs.tabs :as tabs]
            [lt.objs.statusbar :as statusbar]
            [lt.objs.notifos :as notifos]
            [lt.util.js :as util])
  (:require-macros [lt.macros :refer [behavior defui]]))

From now on you should just be able to evaluate the current form under the cursor with Cmd-Enter.

Next we need to define our button, using the defui macro

(defui work-button [this]
  [:input {:type "submit" :value "Do work"}]
  :click #(object/raise this :clicked %))

This bit of code is fairly obvious, it results in a <input type="submit" value="Do work"/> with a click handler bound to our callback. #(object/raise this :click %) is just a shorthand for (fn [e] (object/raise this :click e)), where object/raise raises an event on the target object, in this case a click event. It has nothing to do with exceptions, despite its name.

Next we need to define our worker object.

(object/object* ::worker
                :name "A hard worker"
                :behaviors [::work-on-click]
                :init (fn [this] (work-button this)))

It's a hard worker who works when you click on it. Also note that the value returned from the :init function is used when the object is placed inside a tab, in this case it returns our button, bound to this object.

The behavior we're after will use the beautiful notifos library from Light Table, which displays these wonderful moving-squares-in-a-circle progress indicators.

(behavior ::work-on-click
          :triggers #{:clicked}
          :reaction (fn [this] 
                      (notifos/working "Doing some heavy lifting!")
                      (util/wait 10000 #(statusbar/loader-set 0))))

The behavior name has to be the same as in the object's :behaviors list. It has a set of triggers which trigger the :reaction function with the object passed in as an argument. In our case we'll just display a working indicator and then hide it after 10 seconds.

Now we're ready to create the object and add it as a tab.

(let [worker (object/create ::worker)]
  (tabs/add! worker)
  (tabs/active! worker))

A new tab should appear with a button filling its content. When you click the button you should see a small progress bar at the bottom of the page, which will automatically disappear after 10 seconds.

Now you might've noticed that the tab can't be closed. This is because there is no default behavior for closing a tab, since some tabs might want to prompt the user to save a file, others might have a completely different implementation. The good thing is that we can add this easily without having to restart Light Table.

We'll add another behavior which responds to the :close event (taken from docs.cljs)

(behavior ::on-close-destroy
          :triggers #{:close}
          :reaction (fn [this]
                      (when-let [ts (:lt.objs.tabs/tabset @this)]
                        (when (= (count (:objs @ts)) 1)
                          (tabs/rem-tabset ts)))
                      (object/raise this :destroy)))

Next we need to tell our object to use this behavior by simply adding it to the behaviors list.

(object/object* ::worker
                :name "A hard worker"
                :behaviors [::work-on-click ::on-close-destroy]
                :init (fn [this] (work-button this)))

You don't need to restart anything, just eval the behavior and the object definiton and you should be able to close the tab :) That's how dynamic Light Table is.

For those who want to see the entire result, here's a link to a gist and also a gif screencast of the whole process :)

screencast

This tutorial is really just a small introduction to what Light Table can do, but it should give you a little bit of insight into how dynamic the whole system actually is.


Discuss this post on Hacker News and Reddit

Want to hear about my upcoming book,
Haskell by Example?

Haskell by Example Book

Subscribe to receive updates and free content from the book. You'll also get a discount when the final version of the book is released.