Personal tools

Labs/Ubiquity/Ubiquity 0.5 Author Tutorial

From MozillaWiki

Jump to: navigation, search

Back to Labs/Ubiquity.

Author: Aza Raskin, Blair McBride, Abimanyu Raja, Jono DiCarlo, Atul Varma

Contents

The Ubiquity version 0.5 Command Tutorial

The great power of Ubiquity—from a developer standpoint—is how easy it is to create commands. With only a couple of lines of Javascript, Ubiquity enables even casual web developers to drastically enhance the features of the browser. From an 8-line command to insert a contact's email address in any text field, to a 50-line Twitter integration, this tutorial walks you through the process of being generative with Ubiquity.

The rest of this page documents the command developer API as it stands frozen in version 0.5.

If you want the bleeding-edge command developer API (which includes any new features that may have been added to the Ubiquity source code in HG but not yet included in a release), then that's here.

If you want the old command developer API, as it stood in Ubiquity 0.1 (not recommended), that's here.

Real Time Development

Ubiquity doesn't require you to restart Firefox as you develop. Restarting is a drastic measure, and we want none of it. Instead, Ubiquity reloads the commands every time it is summoned. When you are using the built-in editor then you don't even need to save!

To open the Ubiquity command editor, summon Ubiquity (control/alt + space) and use the "open command editor" command. Throughout this tutorial, when we want you to run a command in Ubiquity, we'll say Ubiq it. For instance, to open the editor, just Ubiq "open command editor".

In the following examples, just type in this editor. Updates happen the next time you summon Ubiquity.

Choices, Choices...

You may notice a drop-down menu mentioning something about a "feed type" just above the editing field in the command editor:

20090318121423.jpg

For now, just make sure this drop-down is set to "Regular". We'll tell you more about what this means later.

Hello World: The First Command

Just a Message: As Simple as it Gets

Let's start with the standard programing trope: printing "Hello, World!".

In the command editor type the following:

CmdUtils.CreateCommand({
  names: ["say hello"],
  execute: function hello_execute() {
    displayMessage("Hello, World!");
  }
});

Now try Ubiq-ing "say hello". You'll see that "Hello, World!" is immediately displayed on the screen. If you are on Mac OSX with Growl installed the message will appear as a Growl notification. If you are on Windows, then it will appears as a standard "toaster" notification in the bottom right-hand corner of the screen.

3651542339_022e4aa4c4.jpg

picture2vx2.png

In Ubuntu 8.04 (Hardy Heron) this appears thus:

ubiqubuntuhelloworldeq9.png

If you don't have Growl installed on OSX, or aren't on a Windows XP/Vista or Ubuntu Hardy, then you won't get any sort of notification. That's something to be worked on in future releases of Ubiquity.

CmdUtils.CreateCommand

CmdUtils is a namespace which contains all the functions you need to create commands. Commands are created by making an object and passing it to CmdUtils.CreateCommand. In Javascript, inline curly braces mean "new object", so this code:

{
  names: ["say hello"],
  execute: function () { //etc }
}

means "Make an object with two attributes, 'names' and 'execute'." This object is then passed as the argument to CmdUtils.CreateCommand.

names

'names' and 'execute' are the only two mandatory attributes for your command object. 'names' specifies what the command is called, and 'execute' specifies what it does. There are plenty of other attributes that you can specify, but they are all optional.

'names' is always an array (thus the square brackets). In the case of this command we provided only one name, "hello world". But we could have provided as many names as we wanted. For instance, if we had said:

 names: ["say hello", "greet"]

then "say hello" would be the normal name of the command, but Ubiquity would also recognize "greet" as a synonym or alias for the command.

execute

'execute' is always a function. When the user executes your command, this is the function that will be run. It can do pretty much anything you want -- or at least, anything you know how to write in JavaScript.

In the example above, we simply call displayMessage(), which displays the given message in whichever way the operating system can.


There are a number of other useful functions in the CmdUtils namespace. We don't yet have full documentation for these commands, but you'll get a sense of the useful ones in this tutorial. For more detailed information, take a look at cmdutils.js.

Making sure your command is localizable

Ubiquity now supports multiple languages. That means that hopefully someday someone will be translating your commands to the other languages that Ubiquity supports. Making your command localizable is easy, and a good habit to get into! You just have to locate all strings that appear in your preview() and execute() methods, that are intended for display to humans, and wrap them with:

_()

This may look odd, but what it does is quite important: it makes your strings appear in the template files that localizers will be using. So let's make our "Hello world!" command localizable:

CmdUtils.CreateCommand({
  names: ["say hello", "greet"],
  execute: function hello_execute() {
    displayMessage(_("Hello, World!"));
  }
});

This makes it so that when a localization template is generated from your command feed, "Hello, World!" will be listed among the strings to be translated.

Note that we don't need to wrap the names, or other strings that appear in the command metadata -- these are automatically wrapped for us. We only need to wrap strings that appear inside the functions.

Adding a Preview

picture3ex0.png

Let's add a preview to our new command. Previews give the user feedback about what a command does before it's executed. Previews are great for providing rich visual feedback like displaying a graphical representation of atmospheric conditions when using the weather command as shown above. Previews have the full expressive power of HTML, including animations, so there's a lot you can do with them.

One point of design: Preview code should never have side-effects. That is, a preview should never (without user interaction) change the state of the system.

For the "say hello" command, we don't need anything fancy: just some help text that is more descriptive than the default "Executes the say hello command."

CmdUtils.CreateCommand({
  names: ["say hello", "greet"],
  preview: "Displays a <i>salutary</i> greeting to the planet.",
  execute: function hello_execute() {
    displayMessage(_("Hello, World!"));
  }
})

Here the preview is an HTML-formatted string. The preview can also be a function. We'll get to that in the next section.

The Date Command: The Second Command

Setting the Selection

I often forget what day it is. That may be because I need to go outside more often, but, like any programmer, I generally solve my problem's symptoms with technology rather then addressing the root cause. My solution is to create a command that inserts the date at the location of the cursor.

CmdUtils.CreateCommand({
  names: ["insert date"],
  execute: function date_execute() {
    CmdUtils.setSelection(new Date().toLocaleDateString());
  }
})

The new function here is setSelection(). This inserts the passed-in text onto the page at the location of the cursor. If the cursor is in an editable text or rich-text fields, the text gets dumped there. If the cursor isn't in an editable area, setSelection() will still be able to insert the date. (Even when it isn't displayed, Firefox always keeps track of a cursor position. To see it, type F7.) Try going to a page, selecting some non-mutable text, and using the command. See, it works! This is particularly useful for commands like "translate", where you want to replace non-editable text with its translation.

The toLocaleDateString() function is native to Javascript, so if you're not familiar with it check out the documentation for the Javascript Date object.

A Better Preview

It's time to add a better preview to the date command. Let's have the preview show the date, so that the user will know what to expect when they execute the command. (As a side benefit the user doesn't even need to execute the command to do a quick check of the day.)

CmdUtils.CreateCommand({
  names: ["insert date"],
  
  _date: function date__date() {
    return new Date().toLocaleDateString();
  },
  
  preview: function date_preview(pblock) {
    var msg = _('Inserts today\'s date: "<i>${date}</i>"');
    pblock.innerHTML = CmdUtils.renderTemplate(msg, {date: this._date()});
  },
  
  execute: function date_execute() {
    CmdUtils.setSelection(this._date());
  }
})

We've done three things here. The first was to factor out the code for getting the date into the _date() function. This way we don't break DRY by repeating code across the preview and execute functions. Notice that to access the _date(), we use the this keyword.

The second thing we've done is to add a preview function. The first argument is the DOM element that gets displayed as the preview for your command. Modify pblock and you modify the preview. In this case, we set the innerHTML of the preview block to be the message we want.

The third thing we've done is to do some string formatting using the renderTemplate() function. This takes a template string and performs the appropriate substitution given the passed-in JSON object. Templates can handle a wide range of functionality, as we are currently using TrimPath's JavascriptTemplates. You should read their site for more documentation. Although JavascriptTemplates has some nice properties, we are contemplating moving to MJT sometime soon.

A shortcut for localization and rendering templates

Note how in the code above, we used the localization wrapper _() before passing the string to renderTemplate. Because this is such a very common combination when displaying strings, we have a shortcut for it. Calling _() with a JSON object as the second argument will automatically trigger CmdUtils.renderTemplate() on the post-localization string. So the above preview method could be rewritten more simply as:

  preview: function date_preview(pblock) {
    var msg = 'Inserts today's date: "<i>${date}</i>"';
    pblock.innerHTML = _(msg, {date: this._date()});
  },

Networking calls and placeholder previews

Previews display something meaningful to the user immediately. If you have a preview that requires an AJAX request—say, to fetch some search results—that call might take a while to return. In the meantime, your command should display a placeholder preview giving the user feedback.

  preview: function(pblock) {
    pblock.innerHTML = _("This will show until the AJAX request returns");
    CmdUtils.previewGet(pblock, "http://example.com", function (htm) {
      pblock.innerHTML = htm;
    });
  },

Documentation and Metadata

Before you share your command with the world, you should consider adding some attributions to the code:

CmdUtils.CreateCommand({
  names: ["insert date"],
  homepage: "http://azarask.in/",
  author: { name: "Aza Raskin", email: "aza@mozilla.com"},
  contributors: ["Atul Varma"],
  license: "MPL",
  
  /* THE REST OF THE CODE HERE */
})

And you should definitely add some documentation:

CmdUtils.CreateCommand({
  names: ["insert date"],
  homepage: "http://azarask.in/",
  author: { name: "Aza Raskin", email: "aza@mozilla.com"},
  contributors: ["Atul Varma"],
  license: "MPL",
  description: "Inserts today's date.",
  help: "If you're in an editable text area, inserts today's date, formatted for the current locale.",
  
  /* THE REST OF THE CODE HERE */
})

The .description and .help attributes are both automatically displayed alongside your command's name on the command-list page. (The user can get to this page at any time by issuing the "list ubiquity commands" command.) HTML tags can be used in both of these strings.

Description is a one-line summary of what the command does, while Help is a longer description that can include examples, caveats, and so on. If your command is simple enough that all you have to say about it fits in one line, it's OK to use a description alone and leave out the help.

Sharing it with the World

Now that we've got our awesome new "date" command, let's share it with the world. All you have to do is put the javascript file on the web somewhere, and make an html page linking to it with "link rel".

<link rel="commands" href="http://path-to-js" name="Title Goes Here" />

Anyone with Ubiquity who visits will get a message offering them the choice of subscribing to your command.

Subscribe.png

If the user chooses to subscribe to a Regular command feed from an untrusted source, they will get a security warning message before they can install the command. Because Regular Ubiquity command feeds can execute arbitrary javascript with chrome privileges, subscribing to a command from a website means allowing that site full access to do whatever it wants to your browser. We want to make sure people understand the dangers before subscribing to commands, so we made the warning page pretty scary.

Warning.PNG

Feed Types

Recall that back near the beginning of the tutorial, you set the "feed type" to "Regular". As you may have guessed, there's actually more than one way to write a command feed. The advantage of Regular feeds is that they let you do whatever you want, so it's really easy to innovate, but the disadvantages lie in subscribing to misbehaving or malicious code. You can alternatively write what's called a Locked-Down Feed, which is much safer and doesn't raise a warning page when a user subscribes to it—but the consequences are that you have less freedom of implementation as a command author. If you're interested in learning more about Locked-Down Feeds, check out the Locked-Down Feed Tutorial.

Trust Networks

In the future, we're going to have something set up that we call a "trust network". When you try out a Ubiquity command from a website, and determine that the command is safe (or unsafe), you'll be able to leave an approval (or a warning). When your friends with Ubiquity installed visit the same site, they'll see the approval or the warning that you left. In this way, users will be able to rely on the judgement of other people they already know and trust in order to help them make decisions about whether a command is safe to install or not.

By the way, the reason we call it "subscribing" to a command, rather than "installing" a command, is that if the javascript file changes -- if the site owner adds new commands, removes old commands, or updates existing commands -- all users subscribed to that URL will automatically get the updates. This will be very convenient for both users and developers, but it will also introduce another type of security risk: just because you decided a command was safe at one point in time doesn't mean that the command will always remain safe. For this reason, we'll need to make sure that the trust network keeps track of when commands have been modified, and notifies users of changes that may make a command unsafe.

Map Me! Location, Snapshots, and Inserting HTML

The "map" command that comes with Ubiquity is fairly powerful. It's also fairly complicated—well, comparatively. It's still only a couple hundred lines of code. The command, though, can get even more useful. Imagine being able to select some houses on Craigslist, or a list of restaurant names, and Ubiq "map these" to get just the map you want. The concept of "these" puts the power of mashups into the users hands. But I digress. Let's make a simple command that inserts a map of your current location.

In this command, we use the Google static map API and the Ubiquity function CmdUtils.getGeoLocation() to insert a map of your current location. Ubiquity currently uses the MaxMind API for trying to guess your location from your IP. That will probably change in the future.

We'll call this command "map me" so that it won't be confused with the standard "map" command.

CmdUtils.CreateCommand({
  names: ["map me"],
  
  _getMapUrl: function() {
    var loc = CmdUtils.getGeoLocation();
    var mapUrl = "http://maps.google.com/staticmap?";

    var params = {
      center: loc.lat + "," + loc.long,
      size: "500x400",
      zoom: 14,
      key: "ABQIAAAAGZ11mh1LzgQ8-8LRW3wEShQeSuJunOpTb3RsLsk00-MAdzxmXhQoiCd940lo0KlfQM5PeNYEPLW-3w"
    };

    return mapUrl + jQuery.param( params );
  },
  
  preview: function( pblock ) {
    var msg = "Inserts a map of your current location: <br/>" +
              "<img src='${url}'/>";
    pblock.innerHTML = _(msg, {url: this._getMapUrl()});
  },
  
  execute: function( ) {
    CmdUtils.getImageSnapshot( this._getMapUrl(), function(imgData) {
      CmdUtils.setSelection( "<img src='" + imgData +"'/>");
    })
  }
})

There are three new things here: CmdUtils.setSelection to set HTML (yep, it can do that); the use of CmdUtils.getGeoLocation(); and using CmdUtils.snapshotImage() to capture the bits for the image.

I find getting the location—as imprecise as IP-based location can be—useful for doing sensible defaults for location-based commands, like Yelp. CmdUtils.getGeoLocation() returns an object which has the following properties: city, state, country, lat, and long.

Why do we need to use CmdUtils.snapshotImage()? Because the Google Maps API requires a key that is tied to a particular URL. If we naively inject the image tag into a random web page, the image won't load because the key doesn't match that random web page's URL. Thus, we use the snapshotImage() function to convert the image into a data url.

There's also a CmdUtils.snapshotWindow function, which allows you to get the image data for any tab/window. The function takes a window as the first paramater, and a callback for the second.

Commands with Arguments

Echo

We'll be working towards making some fun and useful commands—commands that let you control the seething tendrils of the internet with your little pinky. But, let's start by making a simple command to echo back whatever you type.

CmdUtils.CreateCommand({
  names: ["echo"],
  arguments: [{role: "object",
               nountype: noun_arb_text,
               label: "your shout"}],
  preview: function previewEcho(pblock, args) {
    pblock.innerHTML = _("Will echo: ") + args.object.text;
  },
  execute: function executeEcho(args) {
    var msg = args.object.text + "... " + args.object.text + "......";
    displayMessage(msg);
  },
});

This says that the command "echo" takes one argument which is arbitrary text. Whatever text the user enters will get wrapped in an input object and passed into both the preview and execute function.

Try it! Ubiq "echo hellooooo" and watch what happens.

Ubiquity takes care of parsing the user's input, so you don't need to worry about handling prounoun substitution or any of the other natural-language-like features of the Ubiquity parser. Try selecting some text on a page, and Ubiq "echo this". Ubiquity should now echo the selected text.

Note that we gave three pieces of information when defining our argument: its role, its nountype, and its label. The label is the easiest part: It's just whatever text you want to have appear in the Ubiquity interface as a prompt for the user. E.g, if you ubiq "echo", you will see the label for the argument:

  echo (your shout)

The roles and the nountypes require some more explanation. We'll cover each of them in detail next.

Argument Roles

Your command can take multiple arguments. Each one is identified by a "role". To understand roles, it helps to think of your command name as a verb, and each argument as a noun. Remember that Ubiquity's command line is a pseudo-natural-language environment, so it attempts to be as close to natural language grammar as possible.

For example, if you've ever used the email command, you know that it takes up to two arguments: a message and a person.

  email message
  email to person
  email message to person
  email to person message

In grammatical terms, the message argument is the "direct object" of the verb "email". The person argument is an indirect object. We call it the "goal" of the verb. So if we were writing the email command, we'd define the arguments like this:

  arguments: [{role: "object",
               nountype: noun_arb_text,
               label: "message"},
              {role: "goal",
               nountype: noun_type_contact,
               label: "recipient"}]

Because we give the recipient argument the "goal" role, the Ubiquity parser knows to expect the user to type the word "to". When the user enters "email hello to Aza", the parser knows that the word following "to" -- that is, "Aza" -- should be assigned to the recipient argument.

In our simple "echo" command, we expect the user to type "echo hellooooo" or something like that. The "hellooooo" is the direct object of the verb "echo", so we give it the "object" role.

"Object" is the most common role. If a command takes only one argument, that argument is usually an "object".

What Roles Can I Use?

  • object (in "echo helloooo", hello is the object.)
  • goal (in "email this to brandon", brandon is the goal.)
  • source (in "translate this from spanish", spanish is the source.)
  • location (in "yelp pizza near boston", boston is the location.)
  • time (in "book a flight on thursday", thursday is the time.)
  • instrument (in "search monkeys with google", google is the instrument.)
  • format (in "check weather in celsius", celsius is the format.)
  • modifier (in "get email address for chris", chris is the modifier.)
  • alias (in "twitter this as jono", jono is the alias.)

More information about these roles.

The Arguments Object

When your execute method is called, it is passed a single object that encapsulates the values for all arguments.

When your preview method is called, it is passed this object, too.

The object has one attribute corresponding to each role. In our example above, the command accepts only an object-role argument, so the preview and execute methods get passed an argument with an args.object attribute.

If we made a command, like email, that takes an object-role argument and a goal-role argument, its preview and execute methods would get passed an argument with args.object and args.goal.

args.object (or args.goal) has several attributes of its own:

  args.object.text    // a string of the input in plain text, without formatting
  args.object.html    // a string of the input in formatted HTML, including tags
  args.object.data    // for non-text input types, an arbitrary data object
  args.object.summary // the HTML string displayed in the suggestion list, abbreviated if long

Our example command only cares about the .text attribute of the input, because it simply wants plain text. Often, when the user invokes your command by typing a few short words into the input box, .text, .html, and .summary will all have exactly the same value, and .data will be null. Many, if not most, commands that you write will only care about the text value. Nevertheless, the other versions of the input data are provided to you in case they differ from .text and in case your command has a use for them.

Introduction to Noun Types

Noun types specify what kind of input your command can accept for each one of its arguments.

For the "echo" command, we wanted the object-role argument to accept any text whatsoever, so for its nountype we passed in the predefined noun_arb_text object. This object accepts any arbitrary text as a valid argument and passes it to the command unchanged.

This is OK for very simple commands, like echoing back the user's input. But for commands that take structured data, you will want to use more specific nountypes.

For example, if a command can take a date (like the "check calendar" command), you would want to use noun_type_date as the nountype of the argument. noun_type_date provides several benefits to your command: it does all of the date parsing for you; it suggests dates that the user might want to enter (for instance, it defaults to today's date). And, it lets the parser know that your command takes a date. This is useful because when the user selects a date on a page and invokes Ubiquity, your command -- along with "check calendar" -- will be one of the top suggestions.

You can write your own noun types -- we'll get into that later. For now, let's take a look at the built-in nountypes that your command can use. These include:

  • noun_arb_text (Arbitrary text)
  • noun_type_language (Name of a human language, e.g. "English", "Japanese")
  • noun_type_url (A URL)
  • noun_type_contact (An email address from your address book)
  • noun_type_date (A date, in any format, or a word like "tomorrow")
  • noun_type_time (A time, in any format)
  • noun_type_percentage
  • noun_type_address
  • noun_type_tab
  • noun_type_searchengine
  • noun_type_tag
  • noun_type_geolocation


Once you are familiar with writing commands, you should check out the nountypes.js, which has the implementation for most of the noun-types. You can see what noun types are already available for your commands to use, what still needs to be written, and where the existing implementations could use improvement — and then come get involved to help us improve them.

Regexps as Noun Types

If none of the nountypes above is what you're looking for, there are several ways to define your own. The simplest is to use a regular expression. Suppose that (for whatever odd reason) you wanted your command to accept only arguments that begin with the letter N. The following regexp matches words that start with N:

  /^[nN]/

You could use it as a noun type, like so:

  arguments: [{role: "object",
               nountype: /^[nN]/,
               label: "word that starts with n"}]

(Note that you do not put quotes around the regexp.)

A regexp nountype will reject input that doesn't match, but it doesn't know how to help the user by suggesting appropriate input.

Lists as Noun Types

Suppose you're writing a command that takes a color as an argument (perhaps it outputs a hexadecimal RGB representation of that color.) To make a nountype that accepts colors, you can simply pass in an array of strings:

  names: ["convert color"],
  arguments: [{role: "object",
               nountype: ["red", "orange", "yellow", "green", 
                          "blue", "violet", "black", "white",
                          "grey", "brown", "beige", "magenta",
                          "cerulean", "puce"],
               label: "color to convert"}]


One benefit of using a list is that the parser can use it offer the user suggestions. If the user enters "get-color bl", for instance, Ubiquity will be able to suggest "black" and "blue" as the two valid completions based on the input. This makes list-based nountypes very useful for any command that can accept only a finite set of values.

Writing a Noun Type Object

Of course, not every type of noun you'd be interested in can be represented as a finite list or as a regexp. If you want to be able to accept or reject input based on some algorithmic test, you can do so by writing a custom JavaScript object that implements a suggest() method (and, optionally, a default() method.)

There is an example of how to do this in the section on the tab commands, below.

Insert Email: the Contact noun type

Let's take a look at one of the built-in noun-types: noun_type_contact. This lets Ubiquity know to expect a person (either by name or email address). By using the noun-type, Ubiquity will also autocomplete to known people while the user is entering the command. This is what the built-in Ubiquity command "email" uses.

At the moment, Ubiquity figures out what people you know through reading your Gmail contacts. In this prototyped version, you'll need to use Gmail and be logged in for for Ubiquity to know who you know. Eventually, we'd like to be able to interface with all major web-mail sites, as well as desktop software like Thunderbird.

Enough rambling. It's time for a command. I constantly find that I need to fetch someone's email address to paste into a text field because I don't know it off-hand. This command solves that by letting you insert someone's email address using autocomplete.

CmdUtils.CreateCommand({
  name: "insert email",
  arguments: {modifier: noun_type_contact},
  preview: "Inserts someone's email address by name.",
  execute: function(args) {
    CmdUtils.setSelection(args.modifier.html);
  },
});

To try this out, Ubiq "insert email for " and then the first few letters of someone you email often.

Shorter Argument Declarations

Notice that we used a shortcut for declaring the arguments. In the long form, we would have had to say:

  arguments: [{role: "modifier",
               nountype: noun_type_contact,
               label: "contact"}]

but if we don't care about specifying extra parameters for the argument, we can get away with using a single object for "arguments", with the roles as the property names, and the nountypes as the property values:

  arguments: {modifier: noun_type_contact}

or with label using "_" (or " ") as separator:

  arguments: {modifier_contact: noun_type_contact}

If we had several arguments, we could say:

  arguments: {object: noun_arb_text, modifier: noun_type_contact}

Finally, if you only have "object" and don't need to specify a label...:

  argument: noun_arb_text

Aza writes:

This one command sums up what I love about Ubiquity. In 8 lines of code, I can fundamentally enhance the browser's functionality. Doing the same thing using a normal Firefox extension methodology takes pages and pages of code—and the interface would take more thought still. Doing the same thing using a bookmarklet would require a server-side component (to get around cross-site Ajax request ban) as well as forcing the user to give up their email password. Ubiquity increases the surface area of innovation for the browser many-fold, by making anyone who can write simple Javascript into an agent for bettering the browser and the open Web.

TinyURL: Network Calls and jQuery (and the URL noun type)

Often while writing emails, I'll discover that I've pasted in a URL long enough to be used for unfortunate analogies. I'd like to be able to quickly turn that into a TinyURL—but the process of making a TinyURL involves lots of fiddly steps. Ubiquity to the rescue.

Because we include jQuery with Ubiquity, it is simple to perform Ajax calls as well as parse returning data. TinyUrl.com provides an easy to use RESTful API where you pass a URL and it returns its shortened form. We use that API in this command.

CmdUtils.CreateCommand({
  names: ["tinyurl"],
  description: "Replaces the selected URL with a TinyUrl.",
  arguments: [{role: "object",
               nountype: noun_type_url,
               label: "url to shorten"}],
  execute: function(args) {
    var baseUrl = "http://tinyurl.com/api-create.php";
    var params = {url: args.object.text};
    jQuery.get(baseUrl, params, function(tinyUrl) {
      CmdUtils.setSelection(tinyUrl);
    });
  },
})

noun_type_url is a useful nountype because it defaults to the URL of the page that you are currently on. If that is in fact the URL that the user wants, then they don't have to type it or even select it.

jQuery is a powerful tool. With it, you can fairly effortlessly cherry-pick the data you need from RSS feeds, XML, and all sorts of other data formats. It also makes doing in-preview animations a breeze.

Twitter: Putting It All Together

We've now covered everything we need to cover in order to write a command that allows us to Twitter from Ubiquity. The code below is the actual source code of the Twitter command as it appears in Ubiquity 0.5.

Many thanks to Blair McBride for writing this command. The source code is a bit more intricate than anything we've seen so far, but it's built using exactly the same basic pieces -- and demonstrates nearly all of those pieces in action.

const TWITTER_STATUS_MAXLEN = 140;

CmdUtils.CreateCommand({
  names: ["twitter", "tweet", "share using twitter"],
  arguments: [
    {role: "object", label: 'status', nountype: noun_arb_text},
    {role: "alias", nountype: noun_type_twitter_user}
  ],
  icon: "http://twitter.com/favicon.ico",
  description:
  "Sets your Twitter status to a message of at most 160 characters.",
  help: ("You'll need a <a href=\"http://twitter.com\">Twitter account</a>," +
         " obviously.  If you're not already logged in" +
         " you'll be asked to log in."),
  preview: function(previewBlock, args) {
    var statusText = (args.object ? args.object.text : '');
    var usernameText = "";
    if (args.alias) {
      usernameText = args.alias.text;
    } else if (args.as) {
      usernameText = args.as.text;
    }
    var previewTemplate = (
      "<div class='twitter'>"+_("Updates your Twitter status ${username} to:")+"<br/>" +
      "<b class='status'>${status}</b><br/><br/>" +
      _("Characters remaining: <b>${chars}</b>") +
      "<p><small>"+_("tip: tweet @mozillaubiquity for help")+"</small></p></div>");
    var truncateTemplate = (
      "<strong>"+_("The last <b>${truncate}</b> characters will be truncated!")+"</strong>");
    var previewData = {
      status: <>{statusText}</>.toXMLString(),
      username: usernameText && _("(For user <b>${usernameText}</b>)"),
      chars: TWITTER_STATUS_MAXLEN - statusText.length
    };

    var previewHTML = CmdUtils.renderTemplate(
                        CmdUtils.renderTemplate(previewTemplate, previewData),
                        {usernameText:usernameText});

    if (previewData.chars < 0) {
      var truncateData = {
        truncate: 0 - previewData.chars
      };

      previewHTML += CmdUtils.renderTemplate(truncateTemplate, truncateData);
    }

    previewBlock.innerHTML = previewHTML;
  },
  execute: function(args) {
    var statusText = args.object.text;
    if(statusText.length < 1) {
      this._show(_("requires a status to be entered"));
      return;
    }

    var updateUrl = "https://twitter.com/statuses/update.json";
    var updateParams = {
      source: "ubiquity",
      status: statusText
      //dont cut the input since sometimes, the user selects a big url,
      //and the total lenght is more than 140, but tinyurl takes care of that
    };
    var me = this;

    function sendMessage() {
      jQuery.ajax({
        type: "POST",
        url: updateUrl,
        data: updateParams,
        dataType: "json",
        error: function() {
          me._show(_("error - status not updated"));
        },
        success: function() {
          me._show(/^d /.test(statusText)
                   ? _("direct message sent")
                   : _("status updated"));
        },
        username: login.username,
        password: login.password
      });
    }

    var login;
    var alias = args.alias;
    if (alias && alias.text && alias.data) {
      login = alias.data;
      sendMessage();
    } else {
      login = {username: null,
               password: null};
      if (alias && alias.text)
        login.username = alias.text;
      sendMessage();
    }
  },
  _show: function(txt){
    displayMessage({icon: this.icon, title: this.name, text: txt});
  }
});

Switching Tabs

The final command in this tutorial is for switching between tabs. The end goal is this: type a few keys to that matches the title of an open tab (in any window), hit return, and you've switched to that tab.

We'll write this command in two steps. The first step is creating a tab noun-type. The second step is using that noun-type to create the tab-switching command.

Switching Tabs: Writing your own Noun-Types

A noun-type needs to only have two things: A label and a suggest() function. It can optionally also have a default() function.

The label is what shows up when the command prompts for input. Suggest returns a list of input objects, each one containing the name of a matching tab. We're using FUEL to interact with the browser, which is where the "Application" variable comes from.

var noun_type_tab = {
  label: "tab name",

  // Returns all tabs from all windows.
  getTabs: function nt_tab_getTabs() {
    return [tab
            for each (win in Application.windows)
            for each (tab in win.tabs)];
  },

  suggest: function nt_tab_suggest(text, html, callback, selectionIndices) {
    var textLC = text.toLowerCase();
    return [CmdUtils.makeSugg(tab.document.title, null, tab)
            for each (tab in this.getTabs())
            if (~tab.document.title.toLowerCase().indexOf(textLC))];
  }
};

The suggest method of a noun type always gets passed both text and html. If the input is coming from a part of a web page that the user has selected, these values can be different: they are both strings, but the html value contains markup tags while the text value does not. The Tab noun type only cares about the plain text of the tab name, so we ignore the value of html.

The callback argument is for use by nountypes that need to run asynchronously, i.e. because they need to do network calls to generate suggestions. The callback is a function; instead of returning a list of suggestions right away, an asynchronous noun type can call the callback with each suggestion it generates. More on this later; the tab noun type generates all its suggestions right away, so it just returns them instead of using the callback.

We use the convenience function CmdUtils.makeSugg() to generate an input object of the type that the Ubiquity parser expects. The full signature of this function is:

CmdUtils.makeSugg(text, html, data, score, selectionIndices);

It requires at least one of text, html, data. Use null if you want to skip text and/or html.

If the text or html input is very long, makeSugg() generates a summary for us, and puts it in the summary attribute of the input object.

We could have accomplished mostly the same thing without calling makeSugg() by returning a list of anonymous objects like these:

{ text: tabName,
  html: Utils.escapeHtml(tabName),
  data: tab,
  summary: Utils.escapeHtml(tabName) };

The input objects that our suggest() method generates are the same objects that will eventually get passed in to the execute() and preview() methods of any commands that use this noun type.

Switching Tabs: The Command

Now that we are armed with the tab noun-type, it is easy to make the tab-switching command. Again, we use FUEL to focus the selected tab.

CmdUtils.CreateCommand({
  names: ["switch tab"],
  arguments: {object: noun_type_tab},

  execute: function tab_execute(args) {
    // must be delayed because Ubiquity restores the focus on close
    Utils.setTimeout(function delayedFocus() {
      var tab = args.object.data;
      tab._window._window.focus();
      tab.focus();
    }, 99);
  },

  preview: function tab_preview(pblock, args) {
    var tabName = args.object.text;
    if (tabName) {
      var msg = "Changes to <strong>${tab}</strong> tab.";
      pblock.innerHTML = _(msg, {tab: tabName});
    }
    else pblock.innerHTML = _("Switch to a tab by name.");
  }
});

Development Hints

You now know all you need to know to get started developing useful Ubiquity commands of your own.

Here are some miscellaneous tips that didn't fit elsewhere on this page, that may make development easier for you.

The Source Code of Built-In Commands

Looking at the source code of built-in commands and built-in noun types can be a very useful aid to development. If you have the source checkout of Ubiquity (see [Labs/Ubiquity/Ubiquity_0.1_Development_Tutorial | the development tutorial] to find out how to get this), the source code can be found in the files:

ubiquity/standard-feeds/
ubiquity/builtin-feeds/builtincmds.js
ubiquity/modules/nountypes.js

If you don't have a checkout of the source code, you can view the latest version on the web here:

standard-feeds
builtincmds.js
nountypes.js

Interacting with Other Extensions

picture7cm5.png

There isn't much to say here besides that it's easy. For example, here's a command (thanks to Abimanyu Raja for writing this code) that finds the lyrics for a song. You can simply Ubiq something like "get lyrics for wild international" but the command will also interface with the FoxyTunes extension (if it is installed) and add the currently playing song to the suggestion list. Interfacing with other extensions, too, is easy because you can view the source code for every Firefox extension.

var noun_type_song = {
  label: "song name",
  suggest: function nt_song_suggest(text) {
    var suggestions = [CmdUtils.makeSugg(text)];
    var win = Utils.currentChromeWindow;
    if ("foxytunesGetCurrentTrackTitle" in win)
      suggestions.push(CmdUtils.makeSugg(win.foxytunesGetCurrentTrackTitle()));
    return suggestions;
  }
};

CmdUtils.CreateCommand({
  names: ["get lyrics"],
  arguments: {object: noun_type_song},
  description: "Searches for lyrics of the song.",
  preview: function gl_preview(pblock, {object: {text}}) {
    if (!text) {
      this.previewDefault(pblock);
      return;
    }
    pblock.innerHTML = _("Searches for the lyrics of <b>${query}</b>.",
                         {query: Utils.escapeHtml(text)});
  },
  execute: function gl_execute(args) {
    Utils.openUrlInBrowser(
      "http://www.google.com/search?q=" +
      encodeURIComponent(args.object.text + _(" lyrics")));
  }
});

Implementing Asynchronous Noun Suggestions

The noun types we've seen so far in this tutorial have all worked synchronously, returning their suggestions right away. However, Ubiquity also supports asynchronous noun suggestions. These are useful for when a noun type needs to do some potentially time-consuming work before it can make suggestions — most commonly when it needs to call an external service.

Implementing asynchronous suggestions is simple. Whenever the Ubiquity parser calls a noun type's suggest function, it includes a callback function that may be used to send suggestions back to the parser as they become available. In the most typical case, the noun type's suggest function makes an AJAX request, invoking the parser's callback function from within the callback function for the AJAX request.

Here's a simple example: a noun type that suggests Freebase topics based on the text the user has typed or selected, and a barebones freebase-lookup command that uses the noun type.

var noun_type_freebase_topic = {
  name: "Freebase topic",
  label: "topic",
  suggest: function freebase_topic_suggest(text, html, callback) {
    return [
      CmdUtils.makeSugg(text),
      $.ajax({
        url: "http://www.freebase.com/api/service/search",
        dataType: "json",
        data: {prefix: text, limit: 5},
        success: function suggestTopics(response) {
          if (response.status === "200 OK")
            callback([CmdUtils.makeSugg(result.name, result.name, result)
                      for each (result in response.result)]);
        },
      })];
  }
};

CmdUtils.CreateCommand({
  names: ["lookup on freebase"],
  arguments: {object: noun_type_freebase_topic},
  preview: function preview(pblock, args) {
    pblock.innerHTML = _(("Go to the Freebase topic page for " +
                          "{if text}${text}{else}any topic{/if}."),
                         args.object);
  },
  execute: function goToFreebase({object: {text, data}}) {
    Utils.openUrlInBrowser(
      text
      ? (data
         ? "http://www.freebase.com/view" + data.id
         : ("http://www.freebase.com/search?query=" +
            encodeURIComponent(text)))
      : "http://www.freebase.com/");
  }
});

A few notes:

  • A noun type's suggest function typically returns an empty array when it intends to make asynchronous suggestions, but it can return one or more suggestions synchronously if it has them available.
  • Because the work being done to generate asynchronous suggestions is generally somewhat expensive, and because a noun type's suggest function may be called for every keystroke the user makes, you should probably consider implementing a delay before starting the work and/or caching the work at some level. Ubiquity currently leaves this up to each noun type individually.
  • A much more robust implementation of Freebase-derived noun types can be found here.

Running on page load, Ubiquity load and startup

Commands aren't the only thing you can put in a command feed. You can also include functions you want to be run automatically at page-load time and at Firefox startup time.

In order to run some code on page load, you simply have to prefix your function with pageLoad_. For example, if you want to say "Hi" every time a page is loaded, your code would look like this:

function pageLoad_hi() {
  displayMessage("hi");
}

If you modify the function and want to see the changes, remember to first invoke Ubiquity. Although your function like above, might not be a Ubiquity command, this is necessary to refresh the cached code.

Similarly, functions prefixed by ubiquityLoad_ are called whenever Ubiquity is initialized for a main window of Firefox. This is useful when you want to interact with the Ubiquity interface itself, or run some code whenever a new window is opened.

function ubiquityLoad_googleAsDefaultEntry(gUbiquity, mainWindow) {
  gUbiquity.textBox.value = "google";
}

And if you want to run some code everytime Firefox starts up, you just have to prefix the function with startup_ .

The awesome thing about these functions is the ability to develop whole Firefox extensions (that require minimal UI) as Ubiquity plugins in lesser lines of code. You don't need to worry about chrome.manifest or install.rdf. Another added benefit is that you never have to restart your Firefox during development unless of course, you are running code on Firefox startup.

picture5eo9.png

Here's the code for Keyscape which is a Ubiquity command that makes use of pageLoad and startup to recreate the functionality of the Search Keys extension by Jesse Ruderman. In line with Ubiquity's aim to let you do things quicker using your keyboard, this command lets you go to search results on Google by just pressing a number. It'll add hints to show the number of each link.

//A lot of this code is borrowed from the Search Keys extension
//Many thanks to Jeese Ruderman

function startup_keyscape() {
  window.addEventListener("keydown", keyscape_down, true);
}

function pageLoad_keyscape(doc){

  var uri = Utils.url(doc.documentURI);
  //If we are on about: or chrome://, just return
  if(uri.scheme != "http")
    return;

  //Check if the page we are on is google
  if( keyscape_isGoogle(uri) ){
	      		    
    for(var num=1; num<=10; num++){

      var link = jQuery(doc.body).find('a.l')[num-1];
      
      if( link ){

        var hint = doc.createElementNS("http://www.w3.org/1999/xhtml", "span");
        hint.style.color = "blue";
        hint.style.background = "white";
        hint.style.padding = "1px 2px 1px 2px";
        hint.style.marginLeft = ".5em";
        hint.appendChild(doc.createTextNode(num == 10 ? 0 : num));
        link.parentNode.insertBefore(hint, link.nextSibling);
      }  		
    }
  }
}

function keyscape_isGoogle(uri){
  return uri.host.indexOf("google") != -1 
	 && (uri.path.substr(0,8) == "/search?" 
         || uri.path.substr(0,8) == "/custom?");
}

function keyscape_down(event){

  var doc =  Application.activeWindow.activeTab.document;	
  var uri = Utils.url(doc.documentURI);
 
  if( keyscape_isGoogle(uri) ){
	
   var key = parseInt(event.keyCode || event.charCode);	
   var num;
	
   if(48 <= key && key <= 57) //number keys
     num = key - 48;
   else if(96 <= key && key <= 105) //numeric keypad with numlock on
     num = key - 96;
   else
     return;

   //Don't do anything if we are in a textbox
   //or some other related elements
   var elt = window.document.commandDispatcher.focusedElement;
   
   if (elt) {
     var ln = new String(elt.localName).toLowerCase();
     if (ln == "input" || ln == "textarea" || ln == "select" || ln == "isindex")
        return;
   }
    
   //Get the link url from the search results page
   var url_dest = jQuery(doc.body).find('a.l').eq(num-1).attr('href');
   
   if(event.altKey){
     //Open in new tab
     Application.activeWindow.open(Utils.url(url_dest));
   }else{
     //Open in same tab
     doc.location.href = url_dest;
   }
  }
}

If Ubiquity does indeed become ubiquitous, a lot of extensions can be re-written as Ubiquity commands. This is much nicer for the end-user, as well, because the Ubiquity command installation process is a lot easier.

In the future, Ubiquity is also likely to have the ability to convert your Ubiquity commands into proper Firefox extensions. Look here to check on the progress of this functionality.

Persistent Storage

Some commands may want to store data that persist even after closing Firefox. Meet Bin:

var gId = 42; // used for avoiding needless preview updates when typing
CmdUtils.CreateCommand({
  name: "memo",
  description: "Lets you jot a memo for the page.",
  author: "satyr",
  argument: noun_arb_text,
  execute: function memo_execute({object: {html}}) {
    var {href} = CmdUtils.getDocument().location;
    var list = Bin[href]() || [];
    list.push(html);
    Bin[href](list);
    gId = +new Date;
  },
  preview: function memo_preview(pb) {
    var {href} = CmdUtils.getDocument().location;
    var list = Bin[href]();
    if (!list) {
      pb.innerHTML =
        <>No memos taken for: <small><code>{href}</code></small></>;
      return;
    }
    if (pb.ownerDocument.getElementById(gId)) return;
    var ol = CmdUtils.previewList(pb, list, function deleteMemo(i, ev) {
      $(ev.target).closest("li").slideUp();
      list.splice(i, 1);
      Bin[href](list.length ? list : null);
      gId = +new Date;
    });
    ol.id = gId;
  },
});

CmdUtils.CreateCommand({
  name: "list memos",
  description: "Opens all memos you've taken in a new tab.",
  author: "satyr",
  execute: function list_memos_execute() {
    var htm = +Bin + " page(s)<dl>", {escapeHtml} = Utils;
    for each (var [url, memos] in Bin) {
      htm += "<dt>" + escapeHtml(url) + "</dt><dd><ol>";
      for each (var memo in memos) htm += "<li>" + memo + "</li>";
      htm += "</ol></dd>";
    }
    Utils.openUrlInBrowser("data:text/html," + encodeURI(htm + "</dl>"));
  },
});

For details, see the bottom of FeedManager documentation.

Firebug

Ubiquity automatically enables Chrome Errors and Warnings on startup so that errors in your code appear in the Firebug console. Just use CmdUtils.log() rather than console.log().

Where To Go From Here

Take a look at Ubiquity's reference documentation for the Utils and CmdUtils namespaces.

If you have any questions or comments, feel free to join us on IRC at #ubiquity on irc.mozilla.org, as well as the Ubiquity Google Group.

Conclusion

To reiterate a point I made before: Ubiquity increases the surface area of innovation for the browser many-fold, by making anyone who can write simple Javascript into an agent for bettering the browser and the open Web. You are one of those agents.

Now, go forth and create.