Skip to main content

.talon Files

The primary way to extend talon is using .talon files placed anywherein the user directory. A talon file comes in two parts

An example .talon file might look like this:

# Comments start with a # sign, and they must always be on their own line.
#
# This part, the context header, defines under which circumstances this file applies.
os: windows
os: linux
app: Slack
app: Teams
# Anything above this (single!) dash is part of the context header.
-
# Anything below the dash is part of the body.
# If there is no dash, then the body starts immediately.

# These define voice commands.
([channel] unread next | goneck): key(alt-shift-down)
insert code fragment:
# A single command can perform a sequence of actions.
insert("``````")
key(left left left)
# the number of times the key should be pressed can be specified after a colon
key(shift-enter:2)
key(up)

# This activates the tag 'user.tabs'.
tag(): user.tabs

# This adjusts settings (within this file's context).
settings():
key_wait = 1.5

Context header

The context header specifies when the body of the file will be activated. That is, only when the requirements of the header are met will the settings, tags, and commands in the body be available. This enables you to specify commands and behaviour that are only available for specific windows, applications, etc.

The following requirements can be set:

MatcherDescription
osrequire specific operating systems; currently either linux, mac, or windows
tagrequire a specific tag
modeonly active for specific talon modes (like command, dictation, sleep et al.)
appmatch applications by explicitly declared, well-known name
app.namematch applications by name (TODO where does Talon read this out?)
app.exematch applications by executable, like /usr/lib/firefox/firefox or firefox.exe
app.bundlematch applications by their MacOS bundle, like com.mozilla.Firefox
titlematch a window title
code.languagespecify a currently active programming language
languagespecify the particular human language (e.g. pt_BR, en) for the file. Defaults to en if not specified. Currently only needed for multilingual webspeech.
hostnamematch the 'hostname' of your machine (from the hostname CLI command on Linux/Mac). Useful if you want to have a single set of custom config but have some machine-specific parts.

Additionally, you can create user scopes. scopes allow matching on additional arbitrary string information supplied by user scripts. For example you might write a scope called slack_workspace_name. You'd then be able to make .talon files that only matched a particular Slack workspace by putting a line like 'user.slack_workspace_name: Talon' in the header. See the scope concept section below for more information.

Each individual header line has the format [and] [not] <requirement or scope name>: (<literal match value> | /<regex match value>/<python regex flags>) where [] indicates an optional token, (|) indicates exclusive options, and <> a special segment. Some examples of valid lines are title: foo, title: /foo/i, and tag: user.bar, not tag: /foo/, and and not tag: user.foo.

We've already indicated what requirements and scopes are, so lets move on to the matcher part (on the right of the ':'). This can either be a literal string match like title: foo (matching a window whose entire title is 'foo'), or a regular expression. The regular expression engine essentially uses the Python re.search() function to see if the value of the requirement or scope matches. So for the title: /foo/i example we'd match any window whose title had the string 'foo' in it in a case insensitive manner (due to the 'i' flag). For requirement types that have multiple values (tag and mode), Talon iterates through each active tag or mode and matches the header line if any of those match the regex or string literal.

Talon lets you combine multiple lines in the context header. This acts as a composite matcher following specific rules. In the following examples the comment contains an expression describing what the rule will match, e.g. paint_app or (windows and not notepad_app). In this case the expression would match the when the app paint_app is active or the operating system is windows and the app notepad_app is not active.

# paint_app or notepad_app
app: paint_app
app: notepad_app
# (paint_app or notepad_app) and windows
app: paint_app
os: windows
app: notepad_app
# (paint_app and windows) or notepad_app
app: paint_app
and os: windows
app: notepad_app
# paint_app and not windows
app: paint_app
not os: windows

So without modifiers, requirements of the same type (e.g. two apps) are OR-ed together. Requirements of different types (e.g. 'app' and 'os') are AND-ed together. The 'and' modifier looks at the previous requirement and merges with it to make a compound expession. The 'not' modifier just negates the condition.

Voice commands

A voice command has the format RULE: BODY, where RULE determines what words activate the command, and BODY defines what the command does when activated:

# -------- RULE ----------------  ------ BODY -------
([channel] unread next | goneck): key(alt-shift-down)

This command, for example, will press the shortcut alt-shift-down whenever you say either “channel unread next”, “unread next”, or “goneck”.

Rules

Rules have a versatile syntax that is like a word based regex:

SyntaxDescriptionMatches
fooWords“foo”
[foo]Optional“foo” or null (nothing)
foo*Zero or more“”, “foo”, “foo foo”, ...
foo+One or more“foo”, “foo foo”, ...
foo | barChoice“foo”, “bar”
(foo)Precedence/grouping“foo”
{some_list}ListDepends on the list.
<some_capture>CaptureDepends on the capture.
^fooStart anchorSee below
foo$End anchorSee below

Rules can be anchored or unanchored. Talon has a system that detects when a user is and isn't speaking which it uses to break up microphone input into a sequence of 'utterance blocks'. So if you said "first bit ... other ... bits" ('...' means a sufficiently long pause), then Talon might turn this into three utterance blocks: ["first bit", "other", "bits"]. Anchoring a rule requires that it occur at the start or end (or both) of an utterance block.

For example if the following command were added to the Talon Community user file set ^my command: "first" and you said "my command air bat cap" then Talon would insert "firstabc". "air bat cap my command" on the other hand would only produce "abc" (and maybe a misrecognition) because 'my command' was not at the start of your utterance. If other command$: "second" were defined and you said "air bat cap other command" you'd get "abcsecond". If you said "other command air bat cap" you'd just get "second". Because the command matched and had the $ suffix, the rest of your utterance was thrown away.

In general you shouldn't anchor rules since it prevents the user from chaining them together (like we were doing with our examples and the air bat cap commands). Aside from special circumstances you really only consider anchoring when you have a command you wouldn't chain (e.g. switching from command to dictation mode), or you really want to prevent the command from being called by accident.

Talonscript Body

The BODY part of a command is implemented in Talonscript, a simple statically typed language. We'll discuss Talonscript and how it interracts with the RULE part of the command with reference to the following .talon file:

# The following captures are implemented in the https://github.com/talonhub/community user file set:
#
# <user.letter> is a list mapping words like 'plex' or 'gust' to latin letters like 'x' or 'g'
# <user.number_string> is a capture mapping words like 'five' to number strings like '5'
# <digits> is a capture that maps a variable length phrase like
# "one two three" onto an integer 123
#
# The following list does not exist (it's made up for this example):
#
# {user.address_book} maps the names 'sally', 'frank' etc. to their email addresses (sally@example.com, frank@example.com)

# Saying "double letter plex" presses ctrl+a then inserts "x." then "x"
double letter <user.letter>:
modified = letter + "."
key(ctrl-a)
insert(modified)
insert(letter)

# Saying "defaultable plex" inserts "x", saying "defaultable" inserts "default"
defaultable [<user.letter>]:
insert(letter or "default")

# Saying "choose plex" inserts "x", saying "choose five" inserts "5"
choose (<user.letter>|<user.number_string>):
insert(letter or number_string)

# Saying "join plex and gust" or "join plex gust" inserts "xg"
join <user.letter> [and] <user.letter>:
insert(letter_1 + letter_2)

# Saying "add one two three and four five six" inserts "579"
add <digits> and <digits>:
insert(digits_1 + digits_2)

# Saying "insert lots plex gust plex" inserts "['x', 'g', 'x']"
insert lots <user.letter>+:
insert(letter_list)

# Saying "type email sally" inserts "sally@example.com"
# Lists can be used in exactly the same way as captures
type email {user.address_book}:
insert(address_book)

In the above we can see that the lists and captures in the rule part are bound to variables in the Talonscript based on the name of the list/capture. If we use the same lists/capture in a rule multiple times then each use gets a corresponding _1, _2 suffix. If we make a list/capture optional then we have to handle the case where it isn't included using "or". Similarly if we have a choice of matches we have to handle the cases where the alternative was picked. Finally, if we match multiple captures/lists (e.g. with '+'), then we can refer to the lot of them with the _list suffix. Individual items from the multiple match can be referred to with the _1, _2 suffix as well.

In terms of the Talonscript itself, the syntax can be thought of as a very limited subset of Python. Consider the following file which (as of writing) demonstrates all available syntax. See the inline comments for what everything does:

# Comments must be on their own line (optionally preceeded by whitespace)
some [<user.letter>] command:
# or operator is used to deal with optional or alternative command parts. It works as the null
# coalescing operator, not like boolean or.
letter_defaulted = letter or "default"

# Local variable assignment
a = 2.2
b = "foo"
c = "interpolate the {letter_defaulted} and {b} variables into the string"
c = """
multiline string
"""
# Only a single mathematical operation per line
d = 2
a = a + d
a = a - d
a = a * d
a = a / d
a = a % d

# Sleep is a built in function and takes arguments of the (<float>|<integer><suffix>) form.
# Float allows specifying (fractions) of a second. The <integer><suffix> form can be '1m', '5s', '500ms', '1000000us' etc.
# Be aware sleeping in this way will prevent Talon from processing voice commands until the
# sleep is over
sleep(2s)

# Repeats the previous line the given number of times, so in this case we'd sleep for a further 4 seconds
repeat(2)

# The key() action. Allows pressing, holding, and repeating individual key presses.
# See the "key() action" wiki page for more details
key(ctrl-f)

insert("type in this literal string")
auto_insert("process this string with the auto_format action, then type it in with insert()")

# Stylistically we only recommend the following shorthand if it is the only action being
# performed by the command.
"type in this string using auto_insert()"
"""
type in this
multiline
string using auto_insert()
"""

# Call built in or user defined actions
app.notify("show this in a notification balloon")
user.grid_activate()

Tags, settings, and other capabilities

.talon files can do a few other things aside from defining voice commands.

The most common usage after voice commands is to adjust settings. The following changes the given setting values when the context header matches:

title: /my app/
-
settings():
some.setting = 123

another.setting = 432

You can also activate tags. This snippet activates the user.my_tag tag when the context header matches. This is used reasonably often to enable extra sets of voice commands for the given context.

title: /my app/
-
tag(): user.my_tag

Keyboard Shortcuts

Another feature is the ability to bind keyboard shortcuts.

title: /my app/
-
# Show notification saying the key was pressed and prevent other apps from receiving the key event
key(f8): app.notify("f8 key pressed")

# One or more modifiers can be used with the matcher
key(ctrl-shift-alt-super-f8): app.notify("Lots of modifiers and the f8 key pressed. Note that alt is option on Mac. Use cmd modifier on Mac to use the apple key in a shortcut.")

key(f9:passive): app.notify("f9 pressed, and we won't stop any other apps from receiving the key")
key(f9:up): app.notify("show this balloon when the f9 key is released")

The list of available keys you can listen to isn't well defined, but it is likely a subset of the names on the [key() action](Talon Library Reference/key_action.md) wiki page.

Aside from these, additional extra capabilities may be added from time to time. For example in the beta version you can currently define rules for matching facial expressions on OSX and user supplied noises (e.g. a whistle sound) via integration with parrot.py.