Event Handling
Wijjit applications are event driven. Keyboard input, mouse activity, and widget actions are converted into strongly-typed events defined in wijjit.core.events and dispatched through wijjit.core.events.HandlerRegistry. This chapter explains how to hook those events, manage scope, and keep handlers responsive.
Event types
EventType.KEYLow-level key presses. Wrapped by
wijjit.core.events.KeyEvent(event.key,event.modifiers,event.key_obj).EventType.ACTIONEmitted by widgets such as buttons, menus, dialogs, and inputs configured with
action="…". Delivered aswijjit.core.events.ActionEventcontainingaction_id,source_element_id, and optionaldata.EventType.CHANGETriggered when a bound input modifies its value.
wijjit.core.events.ChangeEventsurfaceselement_id,old_value, andnew_value.EventType.FOCUS/EventType.BLURSent whenever focus moves between elements. Helpful for validation and inline help.
EventType.MOUSERaised for clicks, drags, scrolls, and hover updates. Wraps
wijjit.terminal.mouse.MouseEventwith coordinates and modifiers.
Registering handlers
Wijjit exposes decorator helpers on the app instance:
@app.on_key("ctrl+s")Decorator that registers a KEY handler with hotkey parsing (
ctrl,alt,shift). The key is matched exactly (case-insensitively) againstevent.key; there is no"*"wildcard.@app.on_action("save")Decorator that subscribes to a specific action id. Handy for buttons and dialogs.
app.on(EventType.CHANGE, handler, scope=HandlerScope.VIEW)Lowest-level API, and the only one that reaches every
EventTypeand anyHandlerScope. It works both ways: pass acallbackto register immediately, or omit it to use@app.on(...)as a decorator (matching the@app.on_key/@app.on_actionstyle):@app.on(EventType.CHANGE, scope=HandlerScope.VIEW, view_name="signup") def on_change(event): ...
Handlers receive one argument (the event object). They can be synchronous or async; Wijjit detects coroutines automatically. Example:
from wijjit.core.events import EventType, HandlerScope
async def handle_keys(event):
if event.key == "escape":
app.quit()
app.on(
EventType.KEY,
handle_keys,
scope=HandlerScope.VIEW,
view_name="home",
priority=10,
)
Handler scopes & lifetimes
HandlerScope.GLOBALAlways active. Use for cross-view shortcuts (
Ctrl+Cto quit). Keep global handlers minimal to avoid unexpected interactions.HandlerScope.VIEWActive only while the named view is visible. When navigation occurs, Wijjit automatically unregisters view-scoped handlers and re-registers those belonging to the new view. To get this scope, pass
scope=HandlerScope.VIEW(and aview_name) toapp.on(...). Note that@app.on_keyalways registers atHandlerScope.GLOBAL, and@app.on_actiondoes not useHandlerScopeat all – its handlers are stored in a separate action-handler map.HandlerScope.ELEMENTTied to a specific widget id. Wijjit uses this scope to route a widget’s own focus, mouse, and change events; custom elements can register element-scoped handlers to capture events for their id.
Priorities default to 0; higher values run earlier. For example, Wijjit registers Tab/Shift+Tab navigation with priority 100 so it executes before user code can intercept the key.
Actions & change events
Any widget can emit an action:
Buttons –
{% button action="save" %}Text inputs –
{% textinput action="login" %}(fires on Enter)Dialogs –
{% confirmdialog action_ok="confirm_delete" %}Menus – menu items specify
actionon each entry.
Handle them using @app.on_action("save") or app.on(EventType.ACTION, handler) if you need a catch-all logger. ActionEvent.data carries widget-specific payloads (e.g., selected option).
ChangeEvent is emitted by bound inputs (text, textarea, checkbox, select). It is useful for real-time validation:
def validate(event):
if event.element_id == "password" and len(event.new_value) < 8:
app.state.password_error = "Too short"
app.on(EventType.CHANGE, validate, scope=HandlerScope.VIEW, view_name="signup")
Keyboard shortcuts
prompt-toolkit normalizes keys to strings ("ctrl+c", "f5", "enter"). @app.on_key registers one key per decorator; apply it multiple times (or call it directly) to bind several keys:
@app.on_key("ctrl+s")
def save_handler(event):
save()
@app.on_key("ctrl+w")
def close_handler(event):
close_tab()
To stop further handlers from running, call event.cancel().
Mouse handling
Mouse events surface through wijjit.core.events.MouseEvent, which wraps the terminal-layer mouse event in event.mouse_event and exposes convenience properties directly on the event: event.x, event.y, event.button, event.mouse_type, event.shift/event.alt/event.ctrl, and event.click_count. Scroll direction is read from event.button (there is no scroll_amount):
from wijjit.terminal.mouse import MouseButton
def on_mouse(event):
if event.button == MouseButton.SCROLL_UP and event.ctrl:
zoom_in()
elif event.button == MouseButton.SCROLL_DOWN and event.ctrl:
zoom_out()
app.on(EventType.MOUSE, on_mouse, scope=HandlerScope.VIEW, view_name="canvas")
For widget-level interactions (click a specific element), implement handle_mouse on the element or rely on built-in widgets (buttons, menus, scroll containers) which already handle mouse input.
Element event callbacks
In addition to app-level handlers, elements expose callback attributes for direct event handling:
Base Element Callbacks (all elements):
on_double_click(event: MouseEvent)– Called on double-clickon_context_menu(event: MouseEvent) -> list | None– Called on right-click, return menu items
Drag-and-Drop (all elements, set draggable=True or drop_target=True to enable):
on_drag_start(event) -> data | None– Start drag, return data or None to cancelon_drag(event, data)– Called during dragon_drag_end(event, data, dropped)– Called when drag endson_drag_over(event, data) -> bool– Check if drop allowedon_drop(event, data, source_element) -> bool– Handle drop
Table Callbacks:
on_row_click(row_index, row_data)– Row clickedon_row_double_click(row_index, row_data)– Row double-clickedon_cell_click(row_index, column_key, value)– Cell clickedon_header_click(column_key)– Column header clickedon_sort(column_key, direction)– Sort changed
TextInput/TextArea Callbacks:
on_change(old_value, new_value)– Value changedon_submit(value)– Enter pressed (TextInput) or Ctrl+Enter (TextArea)on_paste(text) -> str | None– Modify pasted text, return None to use originalon_file_path_paste(paths) -> bool– File paths detected in paste, return True to prevent
Checkbox / Radio / Toggle Callbacks:
These boolean inputs share one callback vocabulary — on_change = the value
changed, on_action = the user activated the control:
on_change(old, new)– Checked state changed (old/neware booleans). Fires whenever the value changes, whether the user clicked or you set it in code:checkbox.checked = Truetriggerson_changethe same as a click. (CheckboxGrouppasses selection lists:on_change(old_values, new_values).)on_action()– The control was toggled/activated (no arguments). OnTogglethis is the activation hook to use.
Example:
from wijjit.elements.input.text import TextInput
from wijjit.elements.display.table import Table
# TextInput with submit handling
search = TextInput(id="search")
search.on_submit = lambda value: print(f"Search: {value}")
# Table with row selection
table = Table(data=data, columns=columns)
table.on_row_click = lambda idx, row: print(f"Selected: {row}")
Asynchronous handlers
Mark a handler async def to perform network calls or I/O. Wijjit awaits the coroutine. If you need to run CPU-intensive synchronous handlers off the main loop, set the config keys RUN_SYNC_IN_EXECUTOR = True (and optionally EXECUTOR_MAX_WORKERS); synchronous handlers then execute on a thread pool and the event loop remains responsive.
Error handling & debugging
An exception inside a handler is caught and logged with a full stack trace, and the app keeps running unless the error is fatal - a bad handler will not take down your whole UI.
Use
logger = get_logger(__name__)and log inside handlers to confirm they fire in the expected order.
Best practices
Keep handlers small. Delegate business logic to separate functions/services.
Prefer specific scopes. Global handlers are powerful but can cause conflicts as your app grows.
Use actions for semantic events (“save”, “submit”) and key handlers for physical keystrokes. That way you can trigger the same action from buttons, menus, or keyboard shortcuts without duplicating code.
Cancel events when you consume them to prevent downstream handlers from running unnecessarily.
Remove long-lived handlers you register manually when they’re no longer needed to avoid memory leaks —
app.unregister_key(key)removes a handler bound via@app.on_key, andhandler_registry.unregistercovers the general case.
Next, read Layout System and Components to see how these events translate into UI building blocks.