Skip to content

Actions

An event is what the host observed. An Action is a named trigger your app cares about — and something you can perform as synthetic input.

Define a binding once; reuse it across hooks, tests, and hosts.

Naming a Trigger
from xnano import Action

SAVE = Action.keyboard("ctrl+s")

SAVE is an immutable value, not a callback.

Binding an action

Binding an Action
from xnano import Action, on_action

SAVE = Action.keyboard("ctrl+s") # (1)!

@on_action(SAVE) # (2)!
def save(self) -> None:
    self.dirty = False
    self.status = "saved"
  1. Same binding grammar as @on_keyboard: "enter", "ctrl+s", "alt+left", …
  2. Changing SAVE updates every hook that references it.

Specialized decorators are fine for one-off bindings:

Actions and Specialized Hooks
@on_action(Action.keyboard("escape"))
def close(self) -> None: ...

@on_keyboard("escape")
def close(self) -> None: ...

The same action can be bound on more than one grid:

Shared Trigger
from xnano import Action, BaseGrid, on_action

SAVE = Action.keyboard("ctrl+s")

class Editor(BaseGrid):
    @on_action(SAVE)
    def save_document(self) -> None:
        self.status = "saved"

class Settings(BaseGrid):
    @on_action(SAVE)
    def save_preferences(self) -> None:
        self.status = "preferences saved"

Performing an action

The host turns an action into an event on the same path as real input.

Performing an Action
from xnano import Context, on_keyboard

terminal.actions.perform(SAVE) # (1)!

@on_keyboard("f2")
def save_from_shortcut(self, ctx: Context) -> None:
    ctx.actions.perform(SAVE) # (2)!
  1. Useful in tests and host code that holds a Terminal.
  2. Inside a hook, ctx.actions is bound to the active runtime.

The performer (terminal.actions / ctx.actions) has four entry points:

Method Role
perform(action) Queue any Action instance
keyboard(*bindings, kind="press") Shortcut for Action.keyboard
click(field=None, button="left") Shortcut for Action.click
request(method, path="/") Shortcut for Action.request

Other families (focus, clipboard, tick, resize, mouse) are built with Action.* and passed to perform:

ctx.actions.perform(Action.focus("search", kind="gained"))
ctx.actions.perform(Action.tick(16))
ctx.actions.perform(Action.clipboard("pasted text"))

Interactive

This code block runs in the browser via Pyodide.

Try editing the code!
  • Change the initial count default.
  • Perform INCREMENT a third time before the final render.

Editor (session: default)Run
from xnano import Action, BaseGrid, Field, Terminal, on_action

INCREMENT = Action.keyboard("right")

class Counter(BaseGrid, border="rounded", title=" action ", padding=1):
    label: str = Field(default="count: 0", horizontal_align="center")
    count: int = Field(default=0, state=True)

    @on_action(INCREMENT)
    def increment(self) -> None:
        self.count += 1
        self.label = f"count: {self.count}"

counter = Counter()
terminal = Terminal()
terminal.render(counter)
terminal.actions.perform(INCREMENT)
terminal.actions.perform(INCREMENT)
terminal.render(counter)
OutputClear

Avoid action loops

A performed action can trigger a hook that performs another action. Queueing is ordered, but a hook that always re-performs its own trigger is still a loop.

Action families

Builder Matches Specialized hook
Action.keyboard(*bindings, kind=None) key press / release / repeat @on_keyboard
Action.mouse(*buttons, kind=None) mouse button or movement @on_mouse
Action.click(field=None, button="left") click @on_click
Action.focus(field=None, kind=None) focus change @on_focus
Action.clipboard(text=None) paste @on_clipboard
Action.tick(interval_ms=0) clock tick @on_tick
Action.resize(width=None, height=None) resize @on_resize
Action.request(method, path) HTTP route request hooks

matches(event) tests an event; to_event() builds the synthetic payload for perform().

API

Action · Context