Nl:Ubiquity 0.1.2 Auteur Tutorial

From MozillaWiki
Jump to: navigation, search

Terug naar Labs/Ubiquity.

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

In andere talen

Als Nederlands niet je moedertaal is lees dan de tutorial in je eigen taal. Als je eigen taal er niet bij staat, voel je dan vrij om te vertalen.

De Ubiquity 0.1 Commando Tutorial

De grote kracht van Ubiquity—vanuit het perspectief van een ontwikkelaar—is het gemak waarmee commando's gemaakt kunnen worden. Met slechts een paar regels Javascript stelt Ubiquity zelfs de minder geoefende web developers in staat om de mogelijkheden van de browser drastisch te verbeteren.

Van een commando van 8 regels om een e-mail adres van een contactpesoon in een willekeurig tekst veld in te voegen tot een commando van 50 regels voor twitter integratie, deze tutorial loopt je door het process van ontwikkelen met Ubiquity.

WAARSCHUWING: Ubiquity is nog veranderlijk. Dit is een 0.1 release. De API zal waarschijnlijk nog drastisch wijzigen in latere revisies. Hoewel dit betekent dat de code die je vandaag schrijft volgende week misschien niet meer werkt, betekent het ook dat je door commando's te schrijven en ons je feedback te geven de richting waarin Ubiquity zich beweegt zult kunnen beïnvloeden.

De rest van deze pagina beschrijft de commando ontwikkel API zoals deze werkt in de laatst verschenen versie van Ubiquity, 0.1.1. Echter, als je de laatste source versie hebt beschik je over een nieuwere API met enkele aanvullende mogelijkheden die niet beschikbaar zijn in 0.1.1. Je kunt lezen over de nieuwste-van-het-nieuwste source-tip API op Ubiquity Source Tip Author Tutorial (nog niet vertaald).

Real Time Ontwikkelen

Ubiquity verlangt geen herstart van Firefox tijdens het ontwikkelen. Dat zou een barbaarse actie zijn en daar willen we niets van weten. In plaats daarvan herlaadt Ubiquity de commando's elke keer dat ze worden aangeroepen. Als je de ingebouwde editor gebruikt hoef je niet eens op te slaan!

Om de Ubiquity commando editor te openen, roep je Ubiquity op (control/alt + spatie) en gebruik je het "command-editor" commando. In deze tutorial, als we willen dat je een commando uitvoert in Ubiquity, zullen we zeggen Ubiq het. Bijvoorbeeld, om de editor te openen Ubiq je "command-editor".

In de volgende voorbeelden typ je gewoon in deze editor. Het commando wordt bijgewerkt de volgende keer dat je Ubiquity oproept.

Hello World: Het Eerste Commando

Gewoon een Functie: Zo Makkelijk kan het Zijn

Laten we beginnen met het standaard programmeer voorbeeld: Het afdrukken van "Hello, World!". In Ubiquity zijn commando's gewoon functies met diverse eigenschappen er aan geplakt. We zullen beginnen met het handmatig aanmaken van een commando, maar daarna gaan we snel verder met een elegantere methode.

In de commando editor type je het volgende:

function cmd_hello_world() {
  displayMessage( "Hello, World!");
}

Probeer nu "hello-world" te Ubiq-en. Je zult zien dat "Hello, World!" meteen wordt weergegeven op het scherm. Als je op een Mac OSX werkt met Growl geïnstalleerd dan verschijnt het bericht als een Growl mededeling. Als je op Windows werkt verschijnt het als een standaard mededeling in de hoek rechts-onderin van het scherm.

picture1ui2.png

picture2vx2.png

In Ubuntu 8.04 (Hardy Heron) verschijnt dit zo:

ubiqubuntuhelloworldeq9.png

Als je geen Growl hebt geïnstalleerd op OSX, of je zit niet op Windows XP/Vista of Ubuntu Hardy/Intrepid, dan krijg je geen enkele mededeling. Dat is iets waar nog aan gewerkt moet worden in toekomstige versies van Ubiquity.

Dit commando bevat weinig code, dus laten we er meteen induiken. Elke functie die begint met cmd_ wordt automatsch een Ubiquity commando. Het is een beetje namespace magie die het ontwikkelen super simpel maakt.

Er zijn andere voorvoegsels met andere effecten, zoals code uitvoeren bij het laden van de pagina (pageLoad_), en startup code (startup_), maar dat zijn dingen voor een andere tutorial.

Terug naar het voorbeeld. De bulk van het commando zit in de functie displayMessage, die het bericht toont op de manier die het best geschikt is voor het huidige besturingssysteem.

Je zult je misschien afvragen waarom er een streepje in de naam staat, in plaats van een spatie. Dat is zo omdat de Ubiquity natuurlijke taal parser nog niet slim genoeg is om commando's te verwerken die uit meerdere woorden bestaan, en argumenten bestaande uit meerdere woorden. Dat is iets waar we in de toekomst aan gaan werken.

CreateCommand gebruiken

Voor command's die ingewikkelder zijn dan ons simpele "hello-world" commando kun je de hulpfunctie CmdUtils.CreateCommand() gebruiken, welke een opties dictionary accepteert. Om het "hello-world" commando opnieuw te implementeren gebruik makend van de hulpfunctie zouden we schrijven:

CmdUtils.CreateCommand({
  name: "hello-world",
  execute: function() {
    displayMessage( "Hello, World!" );
  }
})

Dit lijkt misschien niet veel voordeel op te leveren (en dat doet het ook niet voor kleine commando's), maar naarmate de commando's ingewikkelder worden zul je zien dat het toch veel helpt. Al is het maar omdat je vrijer kunt zijn in de naamgeving van de commando's—unicode niet-romaanse karakters kunnen nu vrijelijk gebruikt worden.

Er zijn een aantal andere nuttige functies in de CmdUtils namespace. We hebben nog geen volledige documentatie van deze commando's, maar je kunt een gevoel krijgen voor het nut van deze commando's in deze tutorial. Voor meer gedetailleerde informatie moet je maar eens kijken naar de automatisch gegenereerde documentatie of cmdutils.js.

Een Preview Toevoegen

picture3ex0.png

Laten we eens een preview toevoegen aan ons nieuwe commando. Previews geven de gebruiker feedback over wat een commando doet voordat het wordt uitgevoerd. Previews zijn goed voor het geven van rijke visuele feedback zoals het weergeven van een grafische representatie van de atmospherische condities bij het gebruik van het weather commando zoals boven getoond. Previews hebben de volledige expressieve kracht van HTML, inclusief animaties, dus je kunt er een heleboel mee doen.

Een design puntje: Preview code zou nooit bijwerkingen moeten hebben. Dat wil zeggen, een preview zou nooit (zonder tussenkomst van de gebruiker) de staat van het systeem mogen veranderen.

Voor het "hello-world" commando hebben we niks bijzonders nodig: alleen wat help tekst die wat meer zegt dan het standaard tekstje "Executes the hello-world command."

CmdUtils.CreateCommand({
  name: "hello-world",
  preview: "Displays a <i>salutary</i> greeting to the planet.",
  execute: function() {
    displayMessage( "Hello, World!" );
  }
})

Here is de preview een string opgemaakt met HTML. De preview kan ook een functie zijn. Daar komen we later nog op terug.

Het Date Commando: Het Tweede Commando

De Selectie Instellen

Ik vergeet vaak welke dag het is. Misschien komt dat omdat ik er meer uit zou moeten gaan, maar zoals elke programmeur los ik de symptomen van mijn problemen meestal op met technologie in plaats van het echt te verhelpen. Mijn oplossing is het maken van een commando die de datum invoegt op de locatie van de cursor.

CmdUtils.CreateCommand({
  name: "date",
  execute: function() {
    var date = new Date();
    CmdUtils.setSelection( date.toLocaleDateString() );
  }
})

De nieuwe functie hier is setSelection(). Deze voegt de meegegeven tekst toe aan de pagina op de positie van de cursor. Als de cursor in een wijzigbaar tekstvak staat wordt de tekst daar neergezet. Zelfs als de cursor niet in een wijzigbaar deel staat zal setSelection() toch in staat zijn de datum in te voegen. (Zelfs als deze niet wordt weergegeven houdt Firefox toch de locatie van de cursor bij. Om hem te zien druk je op F7). Probeer maar eens naar een pagina te gaan, wat niet-wijzigbare tekst te selecteren en het commando te gebruiken. Zie je, het werkt! Dit is vooral handig voor commando's zoals "translate", waarbij je niet-wijzigbare tekst wil vervangen door zijn vartaling.

De toLocalDateString() functie is onderdeel van Javascript, dus als je er niet bekend mee bent moet je maar eens kijken naar de documentatie van het Javascript Date object.

Een Betere Preview

Het wordt tijd om een betere preview aan het date commando toe te voegen. Laten we de preview de huidige datum laten tonen, zodat de gebruiker weet wat hij kan verwachten als hij het commando uit gaat voeren. (Een bijkomend voordeel is dat de gebruiker het commando niet eens hoeft uit te voeren om snel te zien welke dag het is.)

CmdUtils.CreateCommand({
  name: "date",
  
  _date: function(){
    var date = new Date();
    return date.toLocaleDateString();
  },
  
  preview: function( pblock ) {
    var msg = 'Inserts todays date: "<i>${date}</i>"';
    pblock.innerHTML = CmdUtils.renderTemplate( msg, {date: this._date()} );
  },
  
  execute: function() {
    CmdUtils.setSelection( this._date() );
  }
})

We hebben hier twee dingen gedaan. Het eerste is het verplaatsen van de code die de datum ophaalt naar de _date() functie. Zo hoeven we de regel DRY niet te breken door code te herhalen in de preview en execute functies. Merk op dat we het this keyword gebruiken om de _date() te benaderen.

Het tweede dat we hebben gedaan is het toevoegen van een preview functie. Het eerste argument is het DOM element dat wordt weergegeven als preview voor je commando. Pas pblock aan en je past de preview aan. In dit geval zetten we in de innerHTML van het preview blok de tekst die we willen weergeven.

Een ander ding dat ik heb gedaan is wat string formattering toegepast door gebruik te maken van de renderTemplate() functie. Die accepteert een template string en vervangt dan de parameters met de waarden in het meegegeven JSON object. Templates bieden een grote hoeveelheid functionaliteit, omdat we op het moment gebruik maken van TrimPath's JavascriptTemplates. Je zou hun website eens moeten doorlezen voor meer documentatie. Hoewel JavascriptTemplates een aantal aardige eigenschappen heeft overwegen we om binnenkort over te stappen op MJT.

Previews laten meteen iets zinnigs zien aan de gebruiker. Als je preview een AJAX request moet afvuren—zeg om wat zoekresultaten op te halen—dan kan het even duren voordat er een antwoord komt. In de tussentijd zou je commando een vervangende preview moeten tonen om de gebruiker feedback te geven.

  preview: function( pblock ) {
    pblock.innerHTML = "This will show until the AJAX request returns";
    // AJAX request
    pblock.innerHTML = getFromServer();
  },

In de toekomst gaan we wellicht werken aan het verbeteren van dit proces..

Documentatie en Metadata

Voordat je je nieuwe commando deelt met de wereld zou je moeten overwegen wat attributies aan de code toe te voegen:

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

En je zou zeker wat documentatie moeten toevoegen:

CmdUtils.CreateCommand({
  name: "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 */
})

De .description en .help attributen worden beide automatisch getoond naast de naam van je commando op de command-list pagina. (De gebruiker kan deze pagina op elk moment openen door het commando "command-list" uit te voeren.) HTML tags kunnen in allebei deze strings worden gebruikt.

Description is één regel die samenvat wat het commando doet, terwijl Help eem langere beschrijving is die voorbeelden en opmerkingen en dergelijke kan bevatten. Als je commando zo simpel dat één regel genoeg is om het toe te lichten is het OK om te volstaan met een description en de help weg te laten.

Het met de Wereld Delen

Nu we ons geweldige nieuwe "date" commando hebben gemaakt kunen we het delen met de wereld. Het enige dat je hoeft te doen is het javascript bestand ergens op het web plaatsen en een html pagina te maken die er naar linkt met "link rel".

<link rel="commands" href="http://path-to-js" name="Titel komt hier" />

Opmerking: Je webserver moet .js files serveren als 'application/x-javascript'. Het mime-type 'text/javascript' wordt stilletjes genegeerd.

Iedereen met Ubiquity die je pagina bezoekt zal een bericht krijgen dat hen aanbiedt zich te abboneren op je commando.

Subscribe.png

If the user chooses to subscribe to a command from an untrusted source, they will get a security warning message before they can install the command. (And in Ubiquity 0.1, ALL sources are considered untrusted, so don't take it personally!) Because Ubiquity commands 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

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.

CmdUtils.CreateCommand({
  name: "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/>";
    msg += "<img src='%s'/>".replace( /%s/, this._getMapUrl() );
    pblock.innerHTML = msg;
  },
  
  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.getImageSnapshot() 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.getImageSnapshot()? 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.getWindowSnapshot() 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({
  name: "echo",
  takes: {"your shout": noun_arb_text},
  preview: function( pblock, theShout ) {
    pblock.innerHTML = "Will echo: " + theShout.text;
  },
  execute: function( theShout ) {
    var msg = theShout.text + "... " + theShout.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.

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.

The Input Object

The input object that your execute and preview functions receive has the following attributes:

  inputObject.text  // a string of the input in plain text, without formatting
  inputObject.html  // a string of the input in formatted html, including tags
  inputObject.data  // for non-text input types, an arbitrary data object
  inputObject.summary // for very long inputs, an abbreviated display string

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

Notice that we specified the type of argument to expect by passing in an object — in this case, the predefined noun_arb_text object, which accepts any arbitrary text as a valid argument. If we had wanted to restrict the inputs that our command could take, we could have used a more specific noun-type object to restrict the scope of the argument: for instance, noun_type_date to accept only dates (like the "check-calendar" command) or noun_type_language to accept only names of languages (like the optional modifiers for the "translate" command).

The benefit of specifying a more restrictive noun-type is that it helps the Ubiquity parser generate better suggestions and auto-completions based on user-input. For instance, if the user has a date selected, commands that operate specifically on dates are more likely to be apropos than commands that take arbitrary text, so Ubiquity can suggest the date-specific commands first.

There are many types of nouns that a command could conceivably take: people, dates, places, tabs, etc. Many of these noun-types aren't implemented yet, and most of the them currently have a lack-luster implementation. This is one of the areas where Ubiquity could use the greatest help. Noun-types enable creating compelling user experiences, with minimal amounts of code. It also allows for code-reuse across numerous commands.

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.

Insert Email: Commands with Specific Argument Types

Let's take a look at one of the special 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",
  takes: {"person": noun_type_contact},
  preview: "Inserts someone's email address by name.",
  execute: function( email ) {
    CmdUtils.setSelection( email.text );
  }
})

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

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({
  name: "tinyurl",
  takes: {"url to shorten": noun_arb_text},
  preview: "Replaces the selected URL with a TinyUrl.",
  execute: function( urlToShorten ) {
    var baseUrl = "http://tinyurl.com/api-create.php";
    var params = {url: urlToShorten.text};
    jQuery.get( baseUrl, params, function( tinyUrl ) {
      CmdUtils.setSelection( tinyUrl );
    })
  }
})

Although I used the noun_arb_text command noun-type, I should have used the noun_type_url—if such a thing existed. It doesn't yet.

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.

Color: Creating Bounded Noun Types

Suppose you're writing a set of commands for artists and web designers, and you know that several of the commands will operate on colors. You'd like to be able to specify that certain commands expect names of colors as arguments. Since there are a finite number of named colors, you can define a custom noun type for them based on a list of strings, like so:

noun_type_color = new CmdUtils.NounType( "Color",
  ["red", "orange", "yellow", "green", "blue", "violet", "black", "white",
   "grey", "brown", "beige", "magenta", "cerulean", "puce"] // etc...
  );

Note that we gave the new object a name starting with "noun_". The Ubiquity command loader automatically detects objects starting with "noun_" as custom noun-types, in the same way as it auto-detects functions starting with "cmd_" as custom commands.

Once you've defined a custom noun-type, you can use it in as many commands as you like, thus:

CmdUtils.CreateCommand({
  name: "get-color-code",
  takes: {"color": noun_type_color},
  preview: "Inserts the HTML hex-code for the given color.",
  // etc...

One benefit of creating the custom color noun-type is that if the user has entered "get-color bl", for instance, Ubiquity will be able to suggest "black" and "blue" as the two valid completions based on the input.

Of course, not every type of noun you'd be interested in can be represented as a finite list. If you want to be able to accept or reject input based on some algorithmic test, you can do so by creating your own noun-type implementation instead of instantiating CmdUtils.NounType. There is an example of this in the section on the tab commands, below.

Replace: Commands With Multiple Arguments

Commands, like the translate command, can take multiple (and possibly optional) arguments. Ubiquity takes care of the parsing—you don't have to worry about what order the user types them in, you'll just get passed a dictionary with the appropriate entries.

To illustrate that, let's make a simple regular-expression-based "replace" command. It will take three arguments: the thing to replace, the replacement, and the scope-text to do the replacing in. Here's the command:

CmdUtils.CreateCommand({
  name: "replace",
  takes: {"what": noun_arb_text},
  modifiers: {"with": noun_arb_text, "in": noun_arb_text},
  
  preview: function( pblock, what, mods ) {
    // args contains .with and .in, both of which are input objects.
    var msg = 'Replaces "${whatText}" with ${withText} in ${inText}.';
    var subs = {whatText: what.text, withText: mods["with"].text, inText: mods["in"].text};
    
    pblock.innerHTML = CmdUtils.renderTemplate( msg, subs );
  },
  
  execute: function( what, mods ) {
    // If the scope text isn't specified, use the current selection.
    var text = mods["in"].text || CmdUtils.getSelection();
    var newText = text.replace( what.text, mods["with"].text, "i");
    CmdUtils.setSelection( newText );
  }
});

(In earlier prototypes, modifier arguments could only accept a single-word value, but this has now been fixed.)

The modifiers argument takes a dictionary, where the key is the name of the argument and the value is the noun-type of the argument. In later releases we may include further options, like the ability to specify an argument as required/optional, etc.

The translate command is a good place to learn more about modifiers and the noun_type_language.

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. Many thanks to Blair McBride for writing this command. This is a fully functioning command: the browser takes care of the odd edge cases, like not being logged in.

// max of 140 chars is recommended, but it really allows 160
const TWITTER_STATUS_MAXLEN = 160;

CmdUtils.CreateCommand({
  name: "twitter",
  takes: {status: noun_arb_text},
  
  homepage: "http://theunfocused.net/moz/ubiquity/verbs/",
  author: {name: "Blair McBride", homepage: "http://theunfocused.net/"},
  license: "MPL",
  
  preview: function(previewBlock, statusText) {
    var previewTemplate = "Updates your Twitter status to: <br/>" +       
                          "<b>${status}</b><br /><br />" + 
                          "Characters remaining: <b>${chars}</b>";
    var truncateTemplate = "<br />The last <b>${truncate}</b> " + 
                           "characters will be truncated!";
    var previewData = {
      status: statusText.text,
      chars: TWITTER_STATUS_MAXLEN - statusText.text.length
    };
      
    var previewHTML = CmdUtils.renderTemplate(previewTemplate,
                                                    previewData);
    
    if(previewData.chars < 0) {
      var truncateData = {
        truncate: 0 - previewData.chars
      };
      
      previewHTML += CmdUtils.renderTemplate(truncateTemplate,
                                                   truncateData);
    }
    
    previewBlock.innerHTML = previewHTML;
  },
  
  execute: function(statusText) {
    if(statusText.text.length < 1) {
      displayMessage("Twitter requires a status to be entered");
      return;
    }
    
    var updateUrl = "https://twitter.com/statuses/update.json";
    var updateParams = {
      source: "ubiquity",
      status: statusText.text
    };
    
    jQuery.ajax({
      type: "POST",
      url: updateUrl,
      data: updateParams,
      dataType: "json",
      error: function() {
        displayMessage("Twitter error - status not updated");
      },
      success: function() {
        displayMessage("Twitter status updated");
      }
    });
  }
});

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: Writing your own Noun-Types

A noun-type needs to only have two things: A name and a suggest function. Soon, we'll probably move to having a convenience CmdUtils.CreateNounType(), which will simplify things even more.

The name 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 = {
  _name: "tab name",

  // Returns all tabs from all windows.
  getTabs: function(){
    var tabs = {};

    for( var j=0; j < Application.windows.length; j++ ) {
      var window = Application.windows[j];
      for (var i = 0; i < window.tabs.length; i++) {
        var tab = window.tabs[i];
        tabs[tab.document.title] = tab;
      }
    }

    return tabs;
  },

  suggest: function( text, html ) {
    
    var suggestions  = [];
    var tabs = noun_type_tab.getTabs();

    //TODO: implement a better match algorithm
    for ( var tabName in tabs ) {
      if (tabName.match(text, "i"))
	 suggestions.push( CmdUtils.makeSugg(tabName) );
    }

    // Return a list of input objects, limited to at most five:
    return suggestions.splice(0, 5);
  }
}

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.

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 );

but html and data are optional and need be provided only if they differ from text.

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: tabName,
  data: null,
  summary: 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({
  name: "tab",
  takes: {"tab name": noun_type_tab},

  execute: function( directObj ) {
    var tabName = directObj.text;
    var tabs = noun_type_tab.getTabs();
    tabs[tabName]._window.focus();
    tabs[tabName].focus();
  },

  preview: function( pblock, directObj ) {
    var tabName = directObj.text;
    if( tabName.length > 1 ){
        var msg = "Changes to <b style=\"color:yellow\">%s</b> tab.";
        pblock.innerHTML = msg.replace(/%s/, 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 the development tutorial to find out how to get this), the source code can be found in the files and directories:

ubiquity/standard-feeds/
ubiquity/builtin-feeds/en/builtincmds.js
ubiquity/feed-parts/header/en/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 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 = {
  _name: "song name",
  suggest: function( text, html ) {
    var suggestions  = [CmdUtils.makeSugg(text)];
    if(window.foxytunesGetCurrentTrackTitle){
   suggestions.push(CmdUtils.makeSugg(window.foxytunesGetCurrentTrackTitle()));
  	}
    return suggestions;
  }
}


CmdUtils.CreateCommand({
  name: "get-lyrics",
  takes: {song: noun_type_song},
  preview: function(pblock, directObject) {
    
    searchText = jQuery.trim(directObject.text);
    if(searchText.length < 1) {
      pblock.innerHTML = "Searches for lyrics of the song";
      return;
    }

    var previewTemplate = "Searches for the lyrics of <b>${query}</b>";
    var previewData = {query: searchText};
    pblock.innerHTML = CmdUtils.renderTemplate(previewTemplate, previewData);

  },
  execute: function(directObject) {
    var url = "http://www.google.com/search?q={QUERY}"
    var query = directObject.text + " lyrics";
    var urlString = url.replace("{QUERY}", query);
    Utils.openUrlInBrowser(urlString);
  }
});

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",
  
  suggest: function suggest( text, html, callback ) {
    jQuery.ajax( {
      url: "http://www.freebase.com/api/service/search",
      dataType: "json",
      data: { prefix: text, limit: 5 },
      success: function suggestTopics( response ) {
        var i, results, result;
        results = response.result;
        for ( i = 0; i < results.length; i++ ) {
          result = results[ i ];
          callback( CmdUtils.makeSugg( result.name, result.name, result ) );
        }
      }
    } );
    return [];
  }
}
   
CmdUtils.CreateCommand( {
  name: "freebase-lookup",
  takes: { topic: noun_type_freebase_topic },
  preview: function preview( container, topic ) {
    var text = topic.text || "any topic";
    container.innerHTML = "Go to the Freebase topic page for " + text + ".";
  },
  execute: function goToFreebase( topic ) {
    if ( topic ) {
      Utils.openUrlInBrowser( "http://www.freebase.com/view" + topic.data.id );
    }
  }
} );

A few notes:

  • The parser's callback function expects only one suggestion (not an array of suggestions), so it must be called one time for each suggestion, even if the noun type has multiple suggestions available at the same time (as in the Freebase example above). This is a bit different from the synchronous case, in which the suggest function is expected to return an array.
  • 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 and startup

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, 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.

Firebug

You should enable Chrome Errors and Warnings if you want the errors in your code to appear in the Firebug console. Use CmdUtils.log() rather than console.log() Note: For now, you can only pass one argument to log()


Adding Commands Programmatically

Here is a snippet of code that shows how a developer can programmatically register a command included in a Firefox extension.


// Helper function used to determine the local directory where the
// extension which contains the command is installed. This is used
// to create the URL of the js file which includes the implementation
// of the list of commands you want to add.
function getExtensionDir() {
    var extMgr = Components.classes["@mozilla.org/extensions/manager;1"]
                 .getService(Components.interfaces.nsIExtensionManager);
    return extMgr.getInstallLocation( "feedly@devhd" ).getItemLocation( "feedly@devhd" );
}

function getBaseUri() {
    var ioSvc = Components.classes["@mozilla.org/network/io-service;1"]
	                  .getService(Components.interfaces.nsIIOService);
    var extDir = getExtensionDir();
    var baseUri = ioSvc.newFileURI(extDir).spec;
    return baseUri;
}

// your extension needs to call the addUbiquity command each time a new browser
// session is create and your extension is loaded.
function addUbiquityCommands(){
     // url of the file which contains the implementation of the commands
     // we want to add. 
     var url = getBaseUri() + "content/app/ubiquity/commands.js"
	
     // link to the ubiquity setup module.
     var jsm = {}; 
     Components.utils.import("resource://ubiquity/modules/setup.js", jsm); 
		
     // look up the feed manager and add a new command. Note: we are using the
     // isBuiltIn=true option so that the command is only added for the duration
     // of the browser session. This simplifies the overall lifecycle: if the
     // extension is disabled or un-installed, it will be automatically
     // "removed" from ubiquity.
     jsm.UbiquitySetup.createServices().feedManager.addSubscribedFeed( {
	url: url,
        sourceUrl: url,
        canAutoUpdate: true,
        isBuiltIn: true
     });
}

Inside your command implementation, you can use importing modules or looking up a singleton XPCOM component to link your command back to the functionality encapsulated by your extension. Here is a sample command which does that:

var StreetsUtils = function(){
    var that = {};
    that.lookupCore = function(){
        return Components.classes["@devhd.com/feedly-boot;1"]
				.getService(Components.interfaces.nsIFeedlyBoot)
				.wrappedJSObject
				.lookupCore();
    };
    return that;
}();

CmdUtils.CreateCommand({
  name: "my-extension-test",
  takes: {"message": noun_arb_text},
  icon: "chrome://my-extension-test/skin/icon-16x16.png",
  modifiers: {to: noun_type_contact},
  description:"Testing the feedly+ubiquity integration",
  help:"This is a test help message",
  preview: function(pblock, directObj, modifiers) {
    var html = "Testing my-extension-test ";
    if (modifiers.to) {
      html += "to " + modifiers.to.text + " ";
    }
    if (directObj.html) {
      html += "with these contents:" + directObj.html;
    } else {
      html += "with a link to the current page.";
    }
    pblock.innerHTML = html;
  },

  execute: function(directObj, headers) {
      CmdUtils.log( ">>> my-extension core? " + ( StreetsUtils.lookupCore() != null ) );
  }
});

Note: If you command file needs to include/load other JS files included in your extension, you can use the following piece of code at the top of the command js file you are adding to ubiquity.

function include( partialURI )
{
    // Load JS libraries
    var u = "chrome://feedly/content/app/" + partialURI;
    var jsl = Cc["@mozilla.org/moz/jssubscript-loader;1"]
			.getService(Ci.mozIJSSubScriptLoader);  
		
    jsl.loadSubScript( u );
}

include( "ubiquity/templates.ubiquity.js" );

Closing Ubiquity Programmatically

Here is the line of code that a developer can use in order to programmatically close Ubiquity (from the preview for example):

context.chromeWindow.gUbiquity.closeWindow();

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.