iShell: Tutorial

Contents

The iShell Command Tutorial

The great power of iShell - from a developer standpoint - is how easy it is to create commands. With only a couple of lines of Javascript, it enables even casual web developers to drastically enhance the features of the browser. This tutorial walks you through the process of being generative with iShell. The original Mozilla Ubiquity author tutorial, from which this one is adapted, could be found here.

The rest of this page documents the command development API as it is implemented in iShell. See the iShell API Reference for more details.

Note: iShell commands have access to the full browser WebExtension APIs and jQuery. If you need some features that are not covered by the existing add-on permissions, you need to add the necessary permissions to the add-on manifest and build/sign your own version of the add-on.

Real-Time Development

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

To open the iShell command editor, summon iShell (Control+Space) and use the "edit-shell-commands" command.

Hello World: The First Command

Just a Message: As Simple as it Gets

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

In the command editor type the following:

/** @command */
class SayHello {
    execute() {
        cmdAPI.notify("Hello, World!");
    }
}

Now try executing say-hello:

You'll see that "Hello, World!" is immediately displayed in the bottom-right corner of the screen in a standard Firefox notification.

cmdAPI

cmdAPI is a namespace that contains all the functions you need to create commands. In Mozilla Ubiquity commands were created by passing an object literal to the createCommand function, like the following:

cmdAPI.createCommand({
    names: ["say-hello"],
    execute() {
        cmdAPI.notify("Hello, World!");
    }
});

In iShell you just declare a class with the @command annotation. The command name will be generated automatically. A command class may derive from any other classes.

Only JavaDoc-style comments that start with /** may be used to annotate classes. The regular JavaScript comments are ignored. The content of these comments (except annotations) is displayed in the help section of the iShell command listing. It may contain HTML or Markdown-formatted text if the @markdown annotation is used.

execute is the only mandatory method for your commands. There are plenty of other attributes that you can specify as methods or in annotations, but they are all optional.

Command Names

The command name is automatically generated from the class name in the kebab case. It is possible to specify several names for a command through the @commnad annotation by separating them with a comma:

@command say-hello, greet

In this case, the class name is not used.

execute

This method is invoked when the user hits Enter in the iShell window. 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 cmdAPI.notify(), which displays the given message in whichever way the operating system can.

There are many other useful functions in the cmdAPI namespace. For more detailed information, take a look at the iShell API Reference.

Adding a Preview

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 search results when using the images 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."

/** @command say-hello, greet
    @preview Displays a salutary greeting to the planet.
 */
class HelloWorld {
    execute() {
        cmdAPI.notify("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

Let's create a command that inserts the current date at the location of the cursor.

/** @command */
class InsertDate {
    execute() {
        cmdAPI.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 a rich-text field, 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.

/** @command */
class InsertDate {
    _date() {
        return new Date().toLocaleDateString();
    }

    preview(_, display) {
        const date = this._date();
        display.text(`Inserts today's date: "<i>${date}</i>"`);
    }

    execute() {
        cmdAPI.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 second argument is the DOM element that gets displayed as the preview for your command. Modify the innerHTML property of display and you modify the preview. In iShell it has some additional methods. In this case, we use the text method of the display object to show the message we want.

The third thing we've done is the string formatting using JavaScript template literals. If some advanced formatting is required, there is a handy function for this: cmdAPI.reduceTemplate(), that is also called R. For example, the following code generates a nested HTML list from the provided array of items:

const html =
  `<ul>${
    R(items, item =>
      `<li>${item.text}:
        <ul>
          ${R(item.subitems, subitem => `<li>${subitem.text}</li>`)}
        </ul>
      </li>`)
  }</ul>`;

Networking calls and placeholder previews

Previews display something meaningful to the user immediately. If you have a preview that requires a network 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.

async preview(_, display) {
    display.text("This will show until the fetch request completes");
    const response = await cmdAPI.previewFetch(display, "http://example.com");
    if (response.ok) {
        const html = await response.text();
        display.set(html);
    }
    else
        display.error("HTTP request error.");
}

In the command preview handler, networking calls should be performed with the cmdAPI.preview* family of functions to make these calls automatically interruptable by the output of other commands. In the display object, there are also few shorthands for the cmdAPI.previewFetch function: fetch, fetchText, and fetchJSON. For example, the handler in the previous listing could be equivalently written as follows:

async preview(_, display) {
    display.text("This will show until the fetch request completes");
    const html = await display.fetchText("http://example.com");
    if (html)
        display.set(html);
    else
        display.error("HTTP request error.");
}

If a call to cmdAPI.previewFetch is interrupted by the preview of some other command, it throws an AbortError exception. If you need to perform some complex error processing and catch exceptions thrown by this function, do not change the preview if AbortError is thrown. You can determine this with cmdAPI.fetchAborted function:

async preview(_, display) {
    display.text("This will show until the fetch request completes");
    let html;
    try {
        html = await display.fetchText("http://example.com");
    } catch (e) {
        if (!cmdAPI.fetchAborted(e))
            display.error("Network error.");
        throw e; // AbortError may be rethrown, as any other exception
    }
    if (html)
        display.set(html);
    else
        display.error("HTTP request error.");
}

The code above could be written in a more concise way:

async preview(_, display) {
    display.text("This will show until the fetch request completes");
    const html = await display.fetchText("http://example.com", {_displayError: "Network error."});
    if (html)
        display.set(html);
    else
        display.error("HTTP request error.");
}

While fetch-based functions use response.ok/status and exceptions for error handling, old-fashioned jQuery-based functions, such as cmdAPI.previewAjax do not throw exceptions. They accept error codes through callbacks.

Commands with Arguments

Echo

Let's start by making a simple command to echo back whatever you type.

/** @command */
class Echo {
    constructor(args) {
        args[OBJECT] = {nountype: noun_arb_text, label: "your shout"};
    }

    preview(args, display) {
        display.text("Will echo: " + args.OBJECT.text);
    }

    execute(args) {
        const msg = args.OBJECT.text + "... " + args.OBJECT.text + "......";
        cmdAPI.notify(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 the function.

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

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

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

echo (your shout)

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

Argument Pronouns and Roles

Your command can take multiple arguments. Each one is identified by a pronoun. Each pronoun has the corresponding role. To understand roles, it helps to think of your command name as a verb, and each argument as a noun. Remember that iShell'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 contact.

email message
email to person@example.com
email message to person@example.com
email to person@example.com message

In grammatical terms, the message argument is the "direct object" of the verb "email". The person@example.com argument is an indirect object. We call it the "goal" of the verb.

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. If a command takes only one argument, that argument is usually an "object".

In the class-based syntax of iShell, if we were writing the email command, we'd define the arguments in the class constructor using pronouns (OBJECT is an exception):

class Email {
    constructor(args) {
        args[OBJECT] = {nountype: noun_arb_text, label: "message""}; // object
        args[TO]     = {nountype: noun_type_contact, label: "contact"}; // goal
    }

Please do not use the command constructor for any purposes other than argument definition and simple field initialization, since iShell may create the command object multiple times for various reasons. There are several other functions that are used to initialize commands.

In the object-based syntax of Mozilla Ubiquity, it is only possible to define command arguments using roles by setting the arguments command attribute:

names: ["email"],
arguments: [{role: "object", nountype: noun_arb_text, label: "message"}, // object
            {role: "goal",   nountype: noun_arb_contact, label: "contact"}], // to

What Argument Roles Can I Use?

The Arguments Dictionary

When your execute method is called, it is passed a dictionary that encapsulates the values for all arguments. Here we call JavaScript objects a dictionary to not confuse them with argument roles.

When your preview method is called, it is passed this dictionary, too. In the class-based syntax of iShell, the argument dictionary is always passed as the first parameter. In the preview method of object-based commands this dictionary comes at the second parameter.

This dictionary has one attribute corresponding to each pronoun. In our example above, the command accepts only an object-role argument, so the preview and execute methods get passed a dictionary with an OBJECT attribute (args.OBJECT in the code of the echo command above).

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 a dictionary with OBJECT and TO attributes (in the object-based syntax of Mozilla Ubiquity thease attributes are called object and goal respectively).

args.OBJECT (or args.TO) 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.

noun_arb_text 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, 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 iShell, your command 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:


Once you are familiar with writing commands, you should check out the nountypes.js at the addon source code, which has the implementation for most of the nountypes.

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:

args[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:

args[OBJECT] = {nountype: ["red", "orange", "yellow", "green",
                           "blue", "violet", "black", "white",
                           "grey", "brown", "beige", "magenta",
                           "cerulean", "puce"],
                label: "color to convert"}]

One benefit of specifying a list is that the parser can use it to offer suggestions. If the user enters "get-color bl", for instance, iShell 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.

Objects as Noun Types

Objects used as noun types allow to associate arbitrary data with the argument input value. Let's imagine that you want to create a command that offers a choice between some options represented by API parameters, that you do not want to show to the user.

args[OBJECT] = {nountype: {"option1": "api-parameter-1",
                           "option2": "api-parameter-2",
                           "option3": "api-parameter-3"}
                label: "option"}]

"option1", "option2"... will be shown in the UI and stored in args.OBJECT.text. In the command handler methods, you can access the actual values of the corresponding API parameters using the args.object.data property.

Switching Tabs: Creating Custom Noun Types

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.

For example, we can switch browser tabs with a command. The end goal is this: type a few characters that match the title of an open tab (in any window), hit return, and you've switched to that tab.

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

Switching Tabs: Writing your own Noun-Types

A nountype 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.

const noun_type_browser_tab = {
    label: "tab title or URL",

    // Suggestion methods declared as async should not use the callback argument.
    async suggest(text, html, callback, selectedIndices) {
        const tabs = await browser.tabs.query({});
        let suggs = tabs.map(tab => cmdAPI.makeSugg(tab.title || tab.url, null, tab, 1,
                             selectedIndices));

        // cmdAPI.grepSuggs filters suggestions by the user input and scores them.
        return cmdAPI.grepSuggs(text, suggs);
    }
};

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. Note that when the suggest function is declared as async, it should not call this function. Please, avoid the use of the callback entirely, as it is left only for backward compatibility. Return promises even if you use the old-fashioned callback-based APIs, such as XMLHttpRequest or jQuery.ajax.

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

cmdAPI.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 suggestion objects that our suggest() method generates are the same objects that will eventually get passed in argument dictionaries into the execute() and preview() methods of any commands that use this noun type.

Generating Noun Types with Annotations

If your noun type needs nothing more than the suggest function, you may apply @nountype annotation to a regular function to generate the noun type with the same name:

/** @nountype
    @label foo
*/
function noun_type_foo(text, html, callback, selectedIndices) {
    // ...
}

The annotated function accepts the same arguments as the suggest function of a full-fledged noun type.

Switching Tabs: The Command

Now that we are armed with the tab nountype, it is easy to make the tab-switching command.

/** @command
    @description Switches to the tab with the given title.
*/
class SwitchTab {
    constructor(args) {
        args[OBJECT] = {nountype: noun_type_browser_tab};
    }

    preview({OBJECT: {text: tabName}}, display) {
        if (tabName)
            display.text(`Changes to ${tabName} tab.`);
        else
            display.text(`Switch to a tab by name.`);
    }

    execute({OBJECT: {data: tab}}) {
        browser.tabs.update(tab?.id, {active: true});
    }
}

The @metaclass Annotation

Classes annotated with the @metaclass annotation do not automatically create commands. Instead, they could be instantiated and passed to cmdAPI.createCommand, or be inherited from. This is convenient when it is necessary to create a command-generating function like cmdAPI.createSearchCommand, which produces commands that contain the same logic but differ slightly in some aspects. In the example below, createSuffixCommand function uses a metaclass to generate a set of such commands.

/**
    This class does not create any commands automatically.

    @command
    @metaclass
    @description Supplies input with the "%s" suffix.
*/
class SuffixCommand {
    suffix = "";

    constructor(args) {
        args[OBJECT] = {nountype: noun_arb_text, label: "input"};
    }

    // a method named metaconstructor is used to initialize instances of the command
    metaconstructor(options) {
        // In metaconstructor it is possible to modify the values of the command
        // attributes as they are defined for cmdAPI.createCommand.
        // The following is not possible in a regular constructor:
        this.description = this.description.replace("%s", this.suffix);

        // the suffix and name properties are assigned here
        Object.assign(this, options);
    }

    preview({OBJECT}, display) {
        if (OBJECT?.text)
            display.text(`Processed input: ${OBJECT?.text}${this.suffix}`);
        else
            this.previewDefault(display);
    }

    execute() {}
}

function createSuffixCommand(options) {
    // Command constructor takes the same arguments as its metaconstructor method.
    cmdAPI.createCommand(new SuffixCommand(options));
}

createSuffixCommand({name: "add-um-suffix", suffix: "um"});
createSuffixCommand({name: "add-issa-suffix", suffix: "issa"});

Creating Search Commands

Because the Web is volatile, iShell provides search commands only for the essential sites and services. Nevertheless, you can easily make your own search command with the @search annotation (the equivalent of cmdAPI.createSearchCommand()). They can automatically parse HTML and JSON content, generate preview, and also provide custom filter functions to process DOM of the requested page with jQuery. Let's make a function that performs search in a dictionary:

/**
    @search
    @command
    @delay 1000
    @url https://mydictionary.com/define.php?term=%s
    @container .definition
    @title h1 a
    @href h1 a
    @description Find definitions in mydictionary.com
*/
class MyDictionary {
    // container, title, href, thumbnail, and body properties could be parsed
    // with the corresponding methods
    parseBody(container) {
        const meaning = container.find(".meaning");
        meaning.css("border-bottom", "1px solid white");
        meaning.css("margin-bottom", "5px");
        const example = container.find(".example");
        example.css("font-style", "italic");
        return $("<div>").append(meaning).append(example);
    }
}

An object-based equivalent looks like the following:

cmdAPI.createSearchCommand({
    name: "mydictionary",
    previewDelay: 1000,
    url: "https://mydictionary.com/define.php?term=%s",
    description: "Find definitions in mydictionary.com"
    parser: {
        type      : "html",
        container : ".definition",
        title     : "h1 a",
        href      : "h1 a",
        body      : container => {
            const meaning = container.find(".meaning");
            meaning.css("border-bottom", "1px solid white");
            meaning.css("margin-bottom", "5px");
            const example = container.find(".example");
            example.css("font-style", "italic");
            return $("<div>").append(meaning).append(example);
        }
    }
});

Executing Content Scripts

Content scripts allow modifying the live document loaded into a browser tab or retrieving its contents. Use the cmdAPI.executeScript() function to run JavaScript code in the context of the tab document. For example, let's create a command that changes the color of the heading tags (for example, H1 or H2) and prints the text of each heading in the preview area.

/**
    Executes content scripts in the active tab

    # Examples
    - change-color **of** *H2* **to** *red*

    @command
    @markdown
    @description Changes the color of the given heading tags.
*/
class ChangeColor {
    constructor(args) {
        args[OF] = {nountype: ["H1", "H2", "H3", "H4", "H5"], label: "selector"};
        args[TO] = {nountype: ["red", "green", "blue"], label: "color"};
    }

    async preview({OF}, display) {
        // A function with its arguments should be passed to executeScript.
        // This works both in MV2 and MV3; do not pass references to class methods.
        // The jQuery option allows the use of jQuery in content scripts.
        const options = {func: extractHeadings, args: [OF.text], jQuery: true};
        const [{result}] = await cmdAPI.executeScript(options);
        const headingList = `
    ${ R(result, h => (`
  • ${h}
  • `)) }
`; display.set(headingList) } async execute({OF, TO}) { const options = {func: colorHeadings, args: [OF.text, TO.text], jQuery: true}; await cmdAPI.executeScript(options); } } // These functions are executed as content scripts in the context of the tab document: function extractHeadings(selector) { const headings = $(selector).map((_, element) => element.textContent); return Array.from(headings); } function colorHeadings(selector, color) { $(selector).css("color", color); }

Persistent Storage

Some commands may want to store data that persist even after closing Firefox. Meet the Bin interface. It is available as the last argument of every command method called by iShell (except constructor). Note the use of display.htmlList() function to create a list with clickable items in the preview area.

/**
    @command
    @delay 1000
    @description Lets you jot a memo for the page.
*/
class Memo {
    constructor(args) {
        args[OBJECT] = {nountype: noun_arb_text, label: "text"};
    }

    preview(_, display, Bin) {
        const href = cmdAPI.getLocation();
        const list = Bin[href]();
        const clickHandler = (i, ev) => {
            list.splice(i, 1);
            $(ev.target).closest("li").slideUp();
            Bin[href](list.length ? list : null);
        };

        if (list)
            display.htmlList(list, clickHandler);
        else
            display.text(`No memos taken for: ${href}`);
    }

    execute({OBJECT: {html}}, Bin) {
        const href = cmdAPI.getLocation();
        const list = Bin[href]() || [];

        list.push(html);
        Bin[href](list);
    }
}

Putting It All Together: Creating Pins on Pinterest

The code below is the actual source code of the Pinterest command almost as it appears in iShell. It also demonstrates some tricks that you would not find in the official documentation. With the pinterest command, users can pin images found on the current page to a Pinterest board. One of the command arguments provides autocompletion for all user's boards. This argument also allows to create a board if it does not exist. A custom noun type (noun_type_board) obtains the list of user's boards. The noun type should generate suggestions only for those boards that match the current input.

To create a new board, we also need to append the current input as an additional suggestion to the generated suggestion list. We also need to score this additional suggestion with some low value to make it appear at the bottom of the board list. In the absence of the matching boards, we need to promote the creation of a new one by scoring the additional suggestion with the maximum possible value of 1. Note that generally the score value of 1 may conflict with the scoring of other arguments and produce unexpected results.

Due to the conceptual limitations of the iShell parser, the text of the additional suggestion as it is entered by the user can not contain spaces. It is because we already have one argument of noun_arb_text type (OBJECT). All text after the first space will automatically be assigned to the OBJECT argument. Board suggestions received from Pinterest can contain spaces, though.

/**
    This noun type provides autocompletion for the user's Pinterest boards.

    @label board
    @nountype
*/
function noun_type_board(text, html, _, selectionIndices) {
    let suggs = [], boards = this._command.deref().boards; // Obtain the list of all boards.

    if (boards) {
        // Create suggestions from all user's boards.
        // Created suggestions contain board API objects in their .data property.
        suggs = boards.map(b => cmdAPI.makeSugg(b.name, b.name, b, 1, selectionIndices));
        // Filter the suggestions by the current input.
        suggs = cmdAPI.grepSuggs(text, suggs);
    }

    // Add the current input as an additional suggestion. This suggestion does not contain
    // a board object.
    cmdAPI.addSugg(suggs, text, html, null, suggs.length? .001 : 1, selectionIndices);
    return suggs;
}

/**
    To create a pin, fill in the arguments and click on an image in the preview area
    or press the corresponding Ctrl+Alt+<key> combination. Execute the command to open
    the chosen board.

    # Syntax
    **pinterest** [*description*] **to** *board* [**of** *dimension*]

    # Arguments
    - *description* - a comment to the pin being created.
    - *board* - a name of the board to attach the pin to. Created if not exists.
    - *size* - minimal size of the images displayed in the command preview.
      500 pixels is the default.

    # Examples
    **pinterest** **to** *cats* **of** *1000** *Nice Kitty*

    @command
    @markdown
    @delay 1000
    @icon https://pinterest.com/favicon.ico
    @description Pin image to a board on Pinterest.
    @uuid 044941CF-A22B-45EA-B135-EFF0CA847DA7
 */
export class Pinterest {
    #pinterestAPI;
    #boards = [];

    constructor(args) {
        noun_type_board._command = new WeakRef(this);
        args[OBJECT] = {nountype: noun_arb_text, label: "description"};
        args[TO]     = {nountype: noun_type_board, label: "board"};
        args[OF]     = {nountype: noun_type_number, label: "size"};
    }

    // Executed when iShell is loaded.
    async load(storage) {
        // The actual command uses a more sophisticated initialization
        // which defers the retrieval of the boards until it is needed.
        this.#pinterestAPI = await new PinterestAPI(storage);
        this.#boards = await this.#pinterestAPI.getBoards() || [];
    }

    get boards() {
        return this.#boards;
    }

    async preview({OBJECT: {text: title}, OF: {text: dimension}, TO}, display, storage) {
        dimension = dimension || 500;
        const extractedImages = await this.#extractImagesFromPage(dimension);

        if (extractedImages?.length) {
            let imageList
            const imageHandler = i => {
                const board = TO?.data || TO?.text;
                const imageURL = extractedImages[i].url;
                this.#createPin(board, title, imageURL);
            };

            const imageURLs = extractedImages.map(i => i.url);
            imageList = display.imageList(imageURLs, imageHandler);
            this.#showImageDimensions(imageList, extractedImages);
        }
        else
            display.text(`No images larger than ${dimension}px found.`)
    }

    async #extractImagesFromPage(dimension) {
        const params = {func: extractImagesUserScript, args: [dimension], jQuery: true};
        try {
            const [{result}] = await cmdAPI.executeScript(params);
            return result;
        } catch (e) {
            console.error(e);
        }
    }

    #showImageDimensions(imageList, extractedImages) {
        $("img", imageList).each(function() {
            const image = extractedImages.find(i => i.url === this.src);
            const title = `${image.width}x${image.height}`;
            this.setAttribute("title", title);
        });
    }

    async #createPin(board, description, imageURL) {
        if (!board) {
            cmdAPI.notifyError("No board is selected.");
            return false;
        }

        if (typeof board === "string")
            board = await this.#createBoard(board);

        if (board) {
            const link = cmdAPI.getLocation();
            const success =
                await this.#pinterestAPI.createPin(board.id, description, link, imageURL);

            if (success) {
                cmdAPI.notify("Successfully pinned image.");
                return true;
            }
            else
                cmdAPI.notifyError("Error creating pin.");
        }
    }

    async #createBoard(name) {
        const board = await this.#pinterestAPI.createBoard(name);
        if (board) {
            this.#boards.push(board);
            return board;
        }
        else
            cmdAPI.notifyError("Error creating board.");
    }

    async execute(args, storage) {
        if (this.#pinterestAPI.isAuthorized) {
            if (args.TO?.data)
                cmdAPI.addTab(this.#pinterestAPI.getBoardURL(args.TO.data));
            else
                cmdAPI.addTab(this.#pinterestAPI.userProfileURL);
            this.#boards = await this.#pinterestAPI.getBoards();
        }
        else
            cmdAPI.addTab(this.#pinterestAPI.PINTEREST_URL);
    }
}

function extractImagesUserScript(dimension) {
    dimension = parseInt(dimension);

    let images = $("img").filter(function () {
        return this.naturalWidth >= dimension || this.naturalHeight >= dimension;
    });

    const bySizeDesc = (a, b) => Math.max(b.naturalWidth, b.naturalHeight)
                               - Math.max(a.naturalWidth, a.naturalHeight)

    images = images.toArray().sort(bySizeDesc);

    return images.map(i => ({url: i.src, width: i.naturalWidth, height: i.naturalHeight}));
}

class PinterestAPI {
    // The details are not important...
}

The Backend Application

iShell native backend application is a local web server that may be used to transcend the limits of WebExtensions. There you may develop your own Flask handlers callable from user commands. In these handlers, you could do anything that Python can (see also: Enso). Although technically you may make requests to any local web server, the lifetime of the backend application is managed automatically by the browser. Use the cmdAPI.backendFetch() function to call backend application handlers. For example, this is how you implement close-browser command for the Windows version of Firefox:

/** @command
    @description Closes the browser.
 */
class CloseBrowser {
    async execute() {
        await cmdAPI.backendFetch("/close_browser")
    }
}
# The Flask handler.
# Requires psutil and pywin32 Python libraries.
import os
import psutil
import win32api
import win32con
import win32gui
import win32process

@app.route("/close_browser", methods=['GET'])
def close_browser():
    pid = os.getpid()
    firefox = psutil.Process(pid).parent().parent()
    firefox_tree = firefox.children(recursive=True)
    firefox_pids = [p.pid for p in firefox_tree]
    firefox_pids.append(firefox.pid)

    def enumHandler(hwnd, lParam):
        [_, pid] = win32process.GetWindowThreadProcessId(hwnd)
        if pid in firefox_pids:
            win32api.SendMessage(hwnd, win32con.WM_CLOSE)

    win32gui.EnumWindows(enumHandler, None)

    return "", 204

Development Hints

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

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

Running Code on iShell load and when iShell popup is shown

There are two additional command properties that may be set in cmdAPI.createCommand for that purposes:

Command UUID

Although any command could be run without an 'uuid' attribute, it is recommended to add it to every command you create. If a command is renamed, this will help to preserve its context menu entries, performance data, and the data the command stores through its Bin interface.

'uuid' may be an arbitrary unique string, for example, the homepage URL of a command. It is not necessary to specify 'uuid' attribute if the proper 'homepage' attribute is specified and will never change. An RFC 4122 v4 UUID is generated automatically for the commands inserted through command editor templates. In the examples above we omit this attribute for brevity.

Anaphora

If there is an active selection, and the value of an arbitrary text argument is equal to one of the predefined anaphoric pronouns, the command handler methods will receive the content of the selection through this argument. In the following example, the pronoun this will be substituted for the content of the active selection at the current page:

  send this to user@example.com with subject text

This may be useful if the command contains more than one arbitrary text argument, so it is possible to specify any value of the second such argument in the command line (the with argument in the example above). Without anaphoric pronouns, all text after the first space in the value of the second arbitrary text argument will be assigned to the first such one, which is always the object. If there is no active selection, anaphoric pronouns are treated as literal text values.

iShell parser recognizes the following anaphoric pronouns:

Ace Code Editor

iShell provides the Ace code editor to develop user-defined commands. Its list of default keyboard shortcuts could be found here. An additional non-standard shortcut: Ctrl-S will immediately evaluate and save the edited code.