Skip to content

rokubop/talon-input-map

Repository files navigation

Version Status License

Talon Input Map

Preview

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, or value
  • cross-input modifiers

Formerly known as parrot_config.

Installation

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/

Features

"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 modifier

Modes | Single | Options | Cross-input modifier | Edge debounce | Legend | Events | Channels

Table of Contents

Usage - simple

  1. Call user.input_map_handle from a talon file.

    parrot(pop): user.input_map_handle("pop")
  2. 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")),
    }
  3. Pass that input map to the context action:

    @ctx.action_class("user")
    class Actions:
        def input_map():
            return input_map

Modes

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.

Single

Single

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.

Options

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 cluck

Throttle / 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 stopping

Use ":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 input

Condition (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 held

The 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>=500

Edge debounce

Stabilize edge-triggered region transitions to prevent flicker:

settings():
    user.input_map_edge_debounce_ms = 50

When 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()),

Legend

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.

Events

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.

Mode actions

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

Channels - multiple input maps at the same time

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.

  1. 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)
  2. 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")
  3. 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")

Single actions

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)),
    },
}

Testing

To run the test suite, open the Talon REPL and run:

actions.user.input_map_tests()

Dependencies

none

More Talon packages

Check out my other Talon packages for UI, mouse control, parrot, and more at talon-hub-roku.

About

Combos, modes, throttling, debounce and conditions for Talon input sources.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages