Key Actions

Declarative keyboard action bindings supporting HTMX triggers and JS callbacks.

KeyAction

Declares a keyboard shortcut and its associated action. Actions can trigger HTMX requests, call JS functions, or switch modes.


KeyAction


def KeyAction(
    key:str, modifiers:frozenset[str]=<factory>, htmx_trigger:Optional[str]=None, js_callback:Optional[str]=None,
    mode_enter:Optional[str]=None, mode_exit:bool=False, prevent_default:bool=True, stop_propagation:bool=False,
    zone_ids:Optional[tuple[str, ...]]=None, mode_names:Optional[tuple[str, ...]]=None,
    not_modes:Optional[tuple[str, ...]]=None, custom_condition:Optional[str]=None, description:str='',
    hint_group:str='General', show_in_hints:bool=True
)->None:

A keyboard shortcut binding.

# Test basic KeyAction
action = KeyAction(
    key=" ",  # Space
    htmx_trigger="toggle-btn",
    description="Toggle selection",
    hint_group="Selection"
)

assert action.key == " "
assert action.htmx_trigger == "toggle-btn"
assert action.get_display_key() == "Space"
assert action.matches_context("any-zone", "any-mode") == True
# Test action with modifiers
shift_action = KeyAction(
    key="ArrowUp",
    modifiers=frozenset({"shift"}),
    htmx_trigger="reorder-up",
    zone_ids=("queue",),
    description="Move item up"
)

assert shift_action.get_display_key() == "Shift+↑"
assert shift_action.matches_context("queue", "navigation") == True
assert shift_action.matches_context("browser", "navigation") == False
# Test mode-specific action
split_action = KeyAction(
    key="Enter",
    htmx_trigger="execute-split",
    mode_names=("split",),
    description="Split at caret"
)

assert split_action.matches_context("any", "split") == True
assert split_action.matches_context("any", "navigation") == False
# Test action with not_modes
nav_only_action = KeyAction(
    key="Enter",
    mode_enter="split",
    not_modes=("split",),  # don't enter split if already in split
    description="Enter split mode"
)

assert nav_only_action.matches_context("zone", "navigation") == True
assert nav_only_action.matches_context("zone", "split") == False
# Test JS callback action
audition_action = KeyAction(
    key="ArrowDown",
    js_callback="auditionCurrent",
    zone_ids=("vad-timeline",),
    description="Navigate and audition"
)

config = audition_action.to_js_config()
assert config["jsCallback"] == "auditionCurrent"
assert config["htmxTrigger"] is None
assert config["zoneIds"] == ["vad-timeline"]

Common Action Patterns

# Example: Toggle selection (common for browser/list UIs)
toggle_space = KeyAction(
    key=" ",
    htmx_trigger="toggle-btn",
    description="Toggle selection",
    hint_group="Selection"
)

toggle_enter = KeyAction(
    key="Enter",
    htmx_trigger="toggle-btn",
    not_modes=("split", "edit"),  # don't toggle in edit modes
    description="Toggle selection",
    hint_group="Selection",
    show_in_hints=False  # don't duplicate in hints since Space is shown
)

# Example: Delete/Remove actions
delete_action = KeyAction(
    key="Delete",
    htmx_trigger="delete-btn",
    description="Delete item",
    hint_group="Actions"
)

backspace_delete = KeyAction(
    key="Backspace",
    htmx_trigger="delete-btn",
    description="Delete item",
    hint_group="Actions",
    show_in_hints=False  # alternative key
)
# Test documentation-only KeyAction factory + predicate
# Use case: documenting client-side-only keys (e.g., token-selector caret movement
# that's handled by a separate DOM event listener, not by the keyboard-nav library).

# Factory produces an action with all action paths unset and prevent_default disabled
doc_action = KeyAction.documentation_only(
    key="ArrowLeft",
    description="Move caret left",
    zone_ids=("text-cards",),
    mode_names=("split",),
    hint_group="Token Select",
)

# Action paths must all be unset — this is what makes it "documentation only"
assert doc_action.htmx_trigger is None
assert doc_action.js_callback is None
assert doc_action.mode_enter is None
assert doc_action.mode_exit is False

# Behavior flags must be False so the client-side handler is unblocked
assert doc_action.prevent_default is False, \
    "documentation_only must default prevent_default=False so client-side handlers receive the event unaltered"
assert doc_action.stop_propagation is False, \
    "documentation_only must default stop_propagation=False so client-side handlers receive the event unaltered"

# Documentation fields are populated from arguments
assert doc_action.description == "Move caret left"
assert doc_action.hint_group == "Token Select"
assert doc_action.show_in_hints is True  # appears in hints by default

# Zone/mode restrictions are preserved through the factory
assert doc_action.zone_ids == ("text-cards",)
assert doc_action.mode_names == ("split",)
assert doc_action.matches_context("text-cards", "split") is True
assert doc_action.matches_context("text-cards", "navigation") is False  # mode-restricted
assert doc_action.matches_context("vad-cards", "split") is False  # zone-restricted

# is_documentation_only predicate detects this action correctly
assert doc_action.is_documentation_only() is True

# Predicate correctly rejects actions with ANY action path set
htmx_action = KeyAction(key="x", htmx_trigger="btn", description="X")
assert htmx_action.is_documentation_only() is False

js_action = KeyAction(key="y", js_callback="cb", description="Y")
assert js_action.is_documentation_only() is False

mode_enter_action = KeyAction(key="z", mode_enter="edit", description="Z")
assert mode_enter_action.is_documentation_only() is False

mode_exit_action = KeyAction(key="Escape", mode_exit=True, description="Exit")
assert mode_exit_action.is_documentation_only() is False

# An action explicitly constructed with no action paths but prevent_default=True
# is technically "documentation-only" by the predicate but is misconfigured —
# this is exactly the misuse the factory exists to prevent.
naive_doc = KeyAction(key="ArrowLeft", description="caret left")
assert naive_doc.is_documentation_only() is True
assert naive_doc.prevent_default is True  # ⚠️ would suppress browser default behavior
# Lesson: always use KeyAction.documentation_only() instead of the bare constructor
# when you want documentation-only semantics. The factory bakes in the right defaults.

print("documentation_only factory + is_documentation_only predicate tests passed")