Custom Commands

With the Serenade API, you can write your own voice commands. Custom commands are written via JavaScript files in your ~/.serenade/scripts directory. Each script in that directory will have access to a global object called serenade that serves as the entry point for the Serenade API. If you have any syntax errors in your scripts, they'll be displayed in the Serenade app.

Snippets

Snippets are a powerful way to create shortcuts for code you type regularly.

Here's an example of a snippet that creates a new Python method and prefixes it with the string test_ when you say something like "test method foo bar". Create a file called ~/.serenade/scripts/snippets.js, and add:

serenade.language("python").snippet(
    "test method <identifier>",
    "def test_<identifier:underscores>(self):\n<indent>pass",
    "method"
);

As you can see, the snippet method takes three parameters:

  • A string that specifies the trigger for the voice command. Surrounding text in <> creates a matching slot that matches any text. You can then reference the matched text in the generated snippets, much like regular expression capture groups.

  • A snippet to generate. You can use <> to reference matching slots. You can also define the default formatting for any matching slot by putting a colon after the slot's name; to specify multiple styles, separate them with commands. In the above example, we're specifying that the text should have underscores between words. By default, each slot will have the lowercase style. Possible values include:

    • caps: All capital letters.
    • capital: The first letter of the first word capitalized.
    • camel: Camel case.
    • condition: The condition of an if, for, while, etc.—symbols like "equals" will automatically become "==". condition implies expression.
    • dashes: Dashes between words.
    • expression: Any expression; symbols will not be escaped, so dash will become -.
    • identifier: The name of a function, class, variable, etc.; symbols will be escaped automatically, so dash will become dash.
    • lowercase: Spaces between words.
    • pascal: Pascal case.
    • underscores: Underscores between words.
  • How to add the snippet to your code. In the above example, we're specifying that this block should be added as a method, so if your cursor is outside of a class, it will move to the nearest class before inserting anything, just as it would if you said "add method". The default value for this argument is statement. Possible values include:

    • argument
    • attribute
    • catch
    • decorator
    • else
    • else_if
    • extends
    • finally
    • method
    • parameter
    • return_value
    • statement
    • tag

Here's a snippet to add a new React class in a JavaScript file:

serenade.language("javascript").snippet(
    "new component <identifier>",
    "class <identifier:pascal><cursor> extends React.Component {\n}"
);

In both of these custom commands, we called our slot <identifier>. <identifier> is actually a special slot that will automatically use the identifier text style, since it's so common. We could have equivalently written:

serenade.language("javascript").snippet(
    "new component <component_name>",
    "class <component_name:pascal,identifier><cursor> extends React.Component {\n}"
);

Other special slots include:

  • <body>: An optional body for a block starting with for, else, while, etc.
  • <condition>: Shorthand for <foo:condition>.
  • <cursor>: Where the cursor will be after the snippet is added.
  • <expression>: Shorthand for <foo:expression>.
  • <indent>: Add one level of indentation
  • <terminator>: The statement terminator for the current language (i.e., ; in JavaScript).

And one more example, to create a Java class with an extends and implements in one command:

serenade.language("java").snippet(
    "new class <name> extends <extends> implements <implements>",
    "public class <name:pascal,identifier><cursor> extends <extends:pascal,identifier> implements <implements:pascal,indentifier>"
);

For a full list of snippet options, see the API Reference.

Workflow Automation

You can also use the Serenade API to automate workflows. For instance, create a new file called ~/.serenade/scripts/make.js with the contents:

serenade.global().command("make", async api => {
    await api.focus("terminal");
    await api.typeText("make clean && make");
    await api.pressKey("return");
});

With this script, saying make from any application will open up a terminal app, type the bash command make clean && make, and then execute it.

The global method specifies that we'd like this command to be triggerable from any application. The command method takes two arguments: the voice trigger for the command, followed by an asynchronous function to execute. An api instance is passed to your async function, which enables you to automate a workflow. Inside of that function, we're using the await keyword to make sure that each operation completes before moving onto the next. Don't forget to mark your function with the async keyword!

Here's another example. Create a new file called ~/.serenade/scripts/find.js with the contents:

serenade.app("chrome").command("find <text>", async (api, text) => {
    await api.pressKey("f", ["command"]);
    await api.typeText(text);
});

With this script, saying find hello when Google Chrome is in the foreground will open a find dialog (via pressing ⌘-f) and then type the text hello into that dialog.

This time, rather than specifying global(), we used .app("chrome") to make this command valid only when Google Chrome is focused. As with snippets, you can use slots to capture text. In this example, we used a slot called <text> to determine what to type into Chrome.

Here's one last example, using multiple slots this time:

serenade.global().command("yarn <first> then <second>", async (api, matches) => {
    await api.focus("terminal");
    await api.typeText(`yarn ${matches.first}`);
    await api.pressKey("return");
    await api.wait(3000);
    await api.typeText(`yarn ${matches.second}`);
    await api.pressKey("return");
});

As you can see, when you have multiple slots, the second argument to your callback will be a map, where the keys are the names of your slots, and the values are the matched text.

Check out the API Reference for a full list of API options.