Appearance
Handling Events
Events are the lightweight extension points in Klix. They let you hook into the app lifecycle without mixing startup, teardown, and error handling into command bodies.
For the event system itself, see Events. For the broader runtime flow, see Architecture.
The Events Klix Uses Today
Current runtime code actively emits:
startinputcommandinterrupterrorexit
The EventBus can support any event name, but those are the ones the shipped App loop emits out of the box.
Startup Work
start is the right place for initial UI setup.
python
@app.on("start")
def on_start(session: klix.Session) -> None:
session.ui.clear()
session.ui.layout.header.set("OpsTool", color="accent")
session.ui.layout.status.set("Ready", "Use /help", color="muted")
session.ui.layout.redraw_ui()
session.ui.print("Connected to local session.", color="muted")Why not do this in module scope:
- you need a live
Session - renderers and input engines are attached at runtime
- startup should happen per session, not once at import time
Reacting To Raw Input
input fires after the prompt returns but before command handling is finished.
python
@app.on("input")
def count_input(text: str, session: klix.Session) -> None:
if text.startswith("/"):
session.state.command_count += 1This is a good hook for:
- telemetry
- input logging
- lightweight counters
It is not a replacement for routing. If you want free-form chat behavior, you still need a custom loop like the one in Examples: Gemini-Style CLI.
Watching Commands
command fires when a known command is about to execute.
python
@app.on("command")
def on_command(cmd: klix.ParsedCommand, session: klix.Session) -> None:
session.ui.print(f"Executing {cmd.name}", color="muted")Use this when you want observation without wrapping execution the way middleware does.
Handling Interrupts
The app loop catches KeyboardInterrupt and emits interrupt.
python
@app.on("interrupt")
def on_interrupt(session: klix.Session) -> None:
session.ui.print("Interrupted. Type /exit to quit cleanly.", color="warning")This gives you a way to keep the app alive while still acknowledging the interruption.
Handling Errors
If command execution raises, Klix emits error and prints an error line.
python
@app.on("error")
def on_error(error: Exception, session: klix.Session) -> None:
session.ui.output.panel(
str(error),
title="Command Error",
border_color="error",
title_color="error",
)Keep the handler lightweight. The runtime already caught the exception; your event handler should focus on visibility or cleanup.
Shutdown Work
exit runs at the end of the session loop.
python
@app.on("exit")
def on_exit(session: klix.Session) -> None:
session.ui.print("Goodbye.", color="muted")This is the right place for:
- final logging
- summary output
- flushing in-memory results to state
Sync vs Async Event Listeners
Both are supported:
python
@app.on("start")
async def async_start(session: klix.Session) -> None:
await asyncio.sleep(0.1)
session.ui.print("Async startup completed.", color="success")The event bus preserves listener order and awaits async listeners in sequence.
Custom Event Names
The EventBus can emit arbitrary names. If you want that pattern, do it inside your own app code:
python
await app._event_bus.emit("sync:finished", result, session)That is useful in advanced apps, but it is not part of the main App convenience API. Most projects can stay with the built-in lifecycle events.
Common Mistakes
- Putting command-specific logic into
startorinputhandlers. - Assuming
resize,focus, orblurfire automatically. They are named in the event model, but the current runtime does not emit them. - Doing large blocking work in a sync event listener.
- Forgetting that event listeners run in registration order.