Core Concepts
This chapter introduces the moving pieces that make Wijjit feel like “Flask for the console”. Once you understand how the application lifecycle, views, state, rendering, and events cooperate, the rest of the docs become much easier to navigate.
Application lifecycle
The wijjit.core.app.Wijjit class orchestrates everything:
Construction – provide
template_dir(if you keep templates on disk) and optionalinitial_state. Creating the app sets up everything it needs to render and respond to input: the rendering pipeline, layout engine, focus and mouse handling, the overlay system, and the terminal connection.Configuration – register views via
@app.view, hook actions/keys/mouse handlers, and set global options (refresh interval, custom themes, etc.). Nothing is rendered yet.Run –
app.run()starts the event loop, which enters the alternate terminal buffer, enables mouse tracking, and renders the default view. Wijjit is async internally, soapp.run()is a thin wrapper aroundasyncio.run(...); if you are already inside an event loop, await the loop directly withawait app.event_loop.run_async()instead.Main loop – every tick collects input, dispatches events, applies state changes, and re-renders whenever something changed. Focus, hover, overlays, and notifications are updated along the way.
Shutdown –
app.quit()orCtrl+Ccauses the event loop to unwind, restore the cursor and terminal state, and stop any background work.on_exitview hooks and overlayon_closecallbacks are guaranteed to run.
If a handler raises, Wijjit logs the traceback and the app keeps running — one bad handler does not take down your UI. Tracebacks are buffered while the alternate screen is active and flushed to stderr after the terminal is restored, so they never corrupt the frame. The exception is the initial render: a template that fails on first paint is fatal, so the error surfaces on a clean screen instead of leaving your terminal wedged.
Views and routing
Views describe what should be rendered for a given route. @app.view("name", default=True) registers the function with wijjit.core.view_router.ViewRouter. A view returns what to render via the Flask-style helpers:
wijjit.render_template_string()– an inline Jinja template plus its context (return render_template_string(SOURCE, title="Home")).wijjit.render_template()– a template file from the template directory plus its context (return render_template("dashboard.wij.j2", stats=stats)).
Lifecycle hooks go on the decorator – @app.view("name", default=True, on_enter=setup, on_exit=teardown) – not in the return value. Context is passed as keyword arguments and flattened into top-level template variables; state is auto-injected.
ViewRouter lazily evaluates the function the first time the view is used, then re-invokes a synchronous view on every render so any derived context stays live. Navigation is performed with app.navigate("settings", tab="profile"); parameters are passed as keyword arguments (Wijjit.navigate(view_name, **params)). navigate auto-detects async view callables, so the same call works for sync and async views. During navigation Wijjit:
Initializes the target view if necessary.
Executes the previous view’s
on_exithook and clears view-scoped handlers.Switches
handler_registry.current_viewand storescurrent_view_params.Runs the new view’s
on_enterhook.Flags
needs_renderso the event loop paints the new layout.
You can keep arbitrary navigation state (breadcrumb stacks, modal routes, etc.) inside the State object or your own controller classes. The examples/advanced/navigation_demo.py script showcases multiple named views and hotkeys for moving between them.
State & reactivity
Wijjit ships with wijjit.core.state.State, a dict-like container with change detection. Every mutation notifies global on_change callbacks and per-key watchers registered through state.watch("key", callback). The app subscribes to state itself, so while it is running any change schedules a re-render automatically.
Key behaviors:
Any key name – including ones that collide with a
Statemethod (items,keys, …). Templates resolve{{ state.items }}to your value; in Python, reach those keys withstate["items"]. See Keys that share a name with a State method.Attribute access –
state.greetingworks in Python and templates, butstate["greeting"]remains available for non-identifier keys.Async callbacks – watchers can be
async def; Wijjit tracks pending tasks and awaits them safely.Immutability optional – the class does not enforce immutability; you can mutate nested lists/dicts, but prefer assigning new objects to keep renders predictable.
See State Management for in-depth recipes, including validation, derived data, and background workers.
Rendering pipeline
Rendering is a multi-step process:
Template rendering – Wijjit configures a Jinja environment with custom extensions from
wijjit.tags({% frame %},{% vstack %},{% button %}, etc.). Your view’s template (inline or file) is rendered withstateinjected as a named object, plus every context keyword argument flattened into a top-level template variable. Passingtitle="Home"is therefore referenced as{{ title }}, not{{ data.title }}.Layout tree – tags such as
{% vstack %}emit layout nodes (wijjit.layout.engine.VStack,wijjit.layout.engine.HStack,wijjit.layout.engine.FrameNode) that describe sizing, spacing, and alignment.Layout pass –
wijjit.layout.engineperforms a bottom-up intrinsic size calculation followed by a top-down assignment of absolute bounds (wijjit.layout.bounds.Bounds). Scroll containers report overflow towijjit.layout.scroll.Element painting – Each
wijjit.elements.base.Elementpaints itself into awijjit.rendering.paint_context.PaintContext, which tracks ANSI styling and dirty regions. Elements only redraw when their bounds intersect the dirty set.Terminal flush – The
wijjit.terminal.screen.ScreenManagerwrites the diffed buffer to the alternate screen, preserving performance even when complex components are present.
Because layouts are recomputed every render, you can safely change widths/heights in response to state. The renderer also wires up the ElementWiringManager so bound inputs ({% textinput id="name" %}) stay synced with state keys.
Event flow
Events originate from wijjit.terminal.input.InputHandler and wijjit.terminal.mouse.MouseEvent. The loop wraps them in classes from wijjit.core.events (KeyEvent, ActionEvent, ChangeEvent, MouseEvent) and dispatches them through wijjit.core.events.HandlerRegistry. Handlers can be scoped to:
GLOBAL– always run (use sparingly).VIEW– automatically registered/unregistered when the active view changes.ELEMENT– tied to a specific element via wiring (e.g., a button’s action).
Handlers may be synchronous or async. By default they run on the main loop, but you can offload blocking synchronous handlers to a thread pool by setting the config keys RUN_SYNC_IN_EXECUTOR = True (and optionally EXECUTOR_MAX_WORKERS); see Configuration. The registry also supports priorities so critical behavior (e.g., Tab navigation) runs before user code.
Mouse events are routed via wijjit.core.mouse_router.MouseEventRouter, which performs hit testing against overlay layers first, then base elements. Hover state is managed by wijjit.core.hover.HoverManager.
Working asynchronously
Wijjit is async internally – app.run() calls asyncio.run(...) to drive an async event loop, and both sync and async handlers/views are supported. Long-running operations should:
Spawn a
asyncio.create_taskor schedule work onThreadPoolExecutor.Update
app.statewhen results arrive (loop-safe thanks toStatecallbacks).Use
app.refresh()to request a render if you mutate data outside the normal event pipeline.
Putting it together
Every frame follows the same shape. In pseudocode:
while the app is running:
read the next input event (key or mouse)
dispatch it to the matching handlers
expire any timed-out notifications
if anything changed, re-render the view
Understanding where your feature plugs into this loop (state mutation, handler, layout node, overlay, etc.) will help you design predictable TUIs. Continue to State Management, Templates & Tags, and Event Handling for deeper dives into each subsystem.