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.
SAVE is an immutable value, not a callback.
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"
- Same binding grammar as
@on_keyboard:"enter","ctrl+s","alt+left", … - Changing
SAVEupdates every hook that references it.
Specialized decorators are fine for one-off bindings:
@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:
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.
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)!
- Useful in tests and host code that holds a
Terminal. - Inside a hook,
ctx.actionsis 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
countdefault. - Perform
INCREMENTa third time before the final render.
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)
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().