This is an alternate way to define your noises, parrot, foot pedals, face gestures, or other input sources in a way that supports:
- combos
- mode switching
- throttling
- debounce
- variable inputs
- greater than or less than for
power,f0,f1,f2,x,y, orvalue - cross-input modifiers
Formerly known as
parrot_config.
Clone this repo into your Talon user directory:
# mac and linux
cd ~/.talon/user
# windows
cd ~/AppData/Roaming/talon/user
git clone https://github.com/rokubop/talon-input-map/"pop": ("click", lambda: actions.mouse_click(0)) # basic
"pop cluck": ("combo", lambda: actions.mouse_click(2)) # combo
"hiss:th_90": ("scroll", lambda: actions.user.scroll_down()) # throttle 90ms
"hiss_stop:db_100": ("stop", lambda: None) # debounce 100ms
"tut $noise": ("reverse", lambda noise: reverse(noise)) # variable
"pop:power>10": ("loud", lambda: actions.user.strong_click()) # condition
"pop:else": ("soft", lambda: actions.mouse_click(0)) # fallback
"pop:power>10:th_100":("burst", lambda: actions.user.strong_click()) # compose
"pedal + pop": ("R click", lambda: actions.mouse_click(1)) # cross-input modifierModes | Single | Options | Cross-input modifier | Edge debounce | Legend | Events | Channels
-
Call
user.input_map_handlefrom a talon file.parrot(pop): user.input_map_handle("pop")
-
Define your input map in a python file and return it in a context action.
input_map = { "pop": ("click", lambda: actions.mouse_click(0)), "tut": ("cancel", lambda: actions.key("escape")), "tut tut": ("close window", lambda: actions.key("alt+f4")), }
-
Pass that input map to the context action:
@ctx.action_class("user") class Actions: def input_map(): return input_map
Instead of a flat input map, use a dict of modes where keys are mode names:
input_map = {
"default": {
"pop": ("click", lambda: actions.mouse_click(0)),
"tut": ("cancel", lambda: actions.key("escape")),
},
"combat": {
"pop": ("attack", lambda: actions.mouse_click(1)),
"tut": ("block", lambda: actions.user.game_key("q")),
},
}Switch modes:
actions.user.input_map_mode_set("combat")The "default" key is required - it's how input map detects that modes are being used, and it's the initial mode on startup. Use {**base, ...} to inherit from a base mode and override specific inputs. See all mode actions.
If you don't need a full input map and just want mode switching for one or two inputs:
pop_map = {
"click": ("left click", lambda: actions.mouse_click(0)),
"repeat": ("repeat", lambda: actions.core.repeat_command(1)),
}
@mod.action_class
class Actions:
def my_pop():
"""handle pop"""
actions.user.input_map_single("pop", pop_map)parrot(pop): user.my_pop()actions.user.input_map_single_mode_set("pop", "repeat")Each name has independent state. See all single actions.
Basic
parrot(pop): user.input_map_handle("pop")"pop": ("click", lambda: actions.mouse_click(0)),Combo
parrot(pop): user.input_map_handle("pop")
parrot(cluck): user.input_map_handle("cluck")"pop": ("click", lambda: actions.mouse_click(0)),
"pop cluck": ("combo", lambda: actions.mouse_click(2)), # pop delayed 300ms waiting for cluckThrottle / Debounce
parrot(hiss): user.input_map_handle("hiss")
parrot(hiss:stop): user.input_map_handle("hiss_stop")"hiss:th_90": ("scroll", lambda: actions.user.scroll_down()), # at most once per 90ms
"hiss_stop:db_100": ("stop", lambda: None), # wait 100ms before stoppingUse ":th" or ":db" for defaults.
Variable pattern
parrot(tut): user.input_map_handle("tut")"tut $noise": ("reverse", lambda noise: actions.user.reverse(noise)), # captures next inputCondition (power, f0, f1, f2)
parrot(pop): user.input_map_handle_parrot("pop", power, f0, f1, f2)"pop:power>10": ("loud click", lambda: actions.user.strong_click()),
"pop:else": ("soft click", lambda: actions.mouse_click(0)),Requires input_map_handle_parrot to access power, f0, f1, f2. Operators: >, <, >=, <=, ==, !=.
Condition (gaze)
face(gaze_xy): user.input_map_handle_xy("gaze", gaze_x, gaze_y)"gaze:x<-0.5": ("look left", lambda x, y: actions.user.aim_left(x, y)),
"gaze:x>0.5": ("look right", lambda x, y: actions.user.aim_right(x, y)),
"gaze:else": ("neutral", lambda: None),Requires input_map_handle_xy for x, y. Adding else makes it edge-triggered - fires once per region transition instead of every event.
Condition (face value)
face(dimple_left:change): user.input_map_handle_value("dimple_left", value)"dimple_left:value>0.5": ("ability on", lambda: actions.user.activate()),
"dimple_left:else": ("ability off", lambda: actions.user.deactivate()),Requires input_map_handle_value for value.
Bool (noise start/stop)
noise.register("hiss", lambda active: actions.user.input_map_handle_bool("hiss", active))"hiss": ("scroll", lambda: actions.user.scroll_down()),
"hiss_stop": ("stop", lambda: None),Maps True to "hiss", False to "hiss_stop".
Cross-input modifier
"pedal_left": ("hold", lambda: actions.user.hold_action()),
"pedal_left_stop": ("release", lambda: actions.user.release_action()),
"pop": ("click", lambda: actions.mouse_click(0)),
"pedal_left + pop": ("R click", lambda: actions.mouse_click(1)), # pop while pedal heldThe left side of + is the modifier - must be stateful (has a _stop pair or if/else edge-triggered conditions). The right side is the activator - the discrete event. When the modifier is active, the modifier action fires instead of the normal action. When not active, the normal action fires.
Works with edge-triggered modifiers too:
"gaze:x<-0.5": ("look left", lambda: ...),
"gaze:else": ("neutral", lambda: ...),
"gaze + pop": ("gaze click", lambda: actions.mouse_click(1)), # pop while gaze active (non-else)Conditions on the modifier side target specific regions:
"gaze:x<500 + pop": ("left click", lambda: actions.mouse_click(0)), # pop while gaze x<500
"gaze:x>=500 + pop": ("right click", lambda: actions.mouse_click(1)), # pop while gaze x>=500Edge debounce
Stabilize edge-triggered region transitions to prevent flicker:
settings():
user.input_map_edge_debounce_ms = 50When set, region transitions are delayed by the specified ms. Rapid flicker within the debounce window settles to the final state. _active_region retains the old value during the window. Default is 0 (off, identical to current behavior).
Composing modifiers
Conditions, throttle, and debounce can be combined:
"pop:power>10:th_100": ("throttled loud click", lambda: actions.user.strong_click()),Get a {input: label} dict for the current mode - useful for building HUDs or debug displays:
legend = actions.user.input_map_get_legend()
# {"pop": "click", "tut": "cancel"}Modifiers are stripped and empty labels are filtered out.
Listen to every input that fires through input map:
def on_input(event):
print(event.input, event.label, event.mode)
actions.user.input_map_event_register(on_input)
actions.user.input_map_event_unregister(on_input)Works globally across input map, channels, and singles.
actions.user.input_map_mode_set("combat")
actions.user.input_map_mode_cycle()
actions.user.input_map_mode_revert()
actions.user.input_map_mode_get()Instead of the context approach, you can use channels to have multiple input maps active at the same time. Each channel is registered by name and processes inputs independently.
-
Register channels from a python file:
navigation_map = { "pop": ("select", lambda: actions.mouse_click(0)), "hiss:th_100": ("scroll", lambda: actions.user.scroll_down()), } combat_map = { "cluck": ("attack", lambda: actions.mouse_click(0)), "cluck cluck": ("heavy attack", lambda: actions.mouse_click(1)), } actions.user.input_map_channel_register("navigation", navigation_map) actions.user.input_map_channel_register("combat", combat_map)
-
Route inputs to channels from a talon file:
parrot(pop): user.input_map_channel_handle("navigation", "pop") parrot(hiss): user.input_map_channel_handle("navigation", "hiss") parrot(cluck): user.input_map_channel_handle("combat", "cluck")
-
Channels support modes, events, bool handlers, and all the same features:
actions.user.input_map_channel_mode_set("combat", "defensive") actions.user.input_map_channel_mode_cycle("combat") actions.user.input_map_channel_mode_revert("combat") actions.user.input_map_channel_unregister("combat")
actions.user.input_map_single_mode_set("pop", "repeat")
actions.user.input_map_single_mode_cycle("pop")
actions.user.input_map_single_mode_revert("pop")
actions.user.input_map_single_mode_get("pop")
actions.user.input_map_single_get_legend("pop", pop_map)Map formats - just callables, with labels, or expanded for combos/modifiers:
# Just callables
pop_map = {
"click": lambda: actions.mouse_click(0),
"repeat": lambda: actions.core.repeat_command(1),
}
# With labels
pop_map = {
"click": ("left click", lambda: actions.mouse_click(0)),
"repeat": ("repeat", lambda: actions.core.repeat_command(1)),
}
# Expanded - for combos/modifiers
pop_map = {
"click": {
"pop": ("click", lambda: actions.mouse_click(0)),
"pop pop": ("double click", lambda: actions.mouse_click(0, 2)),
},
}To run the test suite, open the Talon REPL and run:
actions.user.input_map_tests()none
Check out my other Talon packages for UI, mouse control, parrot, and more at talon-hub-roku.