Extending OSUI
OSUI's architecture is designed for extensibility, allowing you to add custom behaviors, event listeners, and rendering logic through the Extension
trait. Extensions are global listeners and modifiers that hook into the OSUI application lifecycle.
The Extension
Trait
The core of OSUI's extensibility is the Extension
trait:
pub trait Extension {
fn init(&mut self, _ctx: &Context) {}
fn event(&mut self, _ctx: &Context, _event: &dyn Event) {}
fn on_close(&mut self) {}
fn render(&mut self, _ctx: &Context, _scope: &mut RenderScope) {}
fn render_widget(&mut self, _ctx: &Context, _scope: &mut RenderScope, _widget: &Arc<Widget>) {}
fn after_render_widget(
&mut self,
_ctx: &Context,
_scope: &mut RenderScope,
_widget: &Arc<Widget>,
) {}
}
Each method provides a hook into a specific part of the OSUI application lifecycle:
init(&mut self, ctx: &Context)
: Called once when the extension is registered with theScreen
and before the main rendering loop starts. Use this for setup tasks, like spawning background threads or initializing global resources.event(&mut self, ctx: &Context, event: &dyn Event)
: Called whenever anyEvent
is dispatched across the OSUI system. This is where extensions can listen for and react to keyboard input, custom events, or events from other extensions.on_close(&mut self)
: Called whenscreen.close()
is invoked. Use this for cleanup, like disabling raw terminal mode or saving state.render(&mut self, ctx: &Context, scope: &mut RenderScope)
: Called before any individual widgets are rendered for a given frame. ThisRenderScope
represents the entire screen. Useful for drawing global elements or backgrounds that span the whole terminal.render_widget(&mut self, ctx: &Context, scope: &mut RenderScope, widget: &Arc<Widget>)
: Called before a specificwidget
'sElement::render
method is called. Extensions can inject drawing commands into thescope
or modify thewidget
at this stage.after_render_widget(&mut self, ctx: &Context, scope: &mut RenderScope, widget: &Arc<Widget>)
: Called after a specificwidget
'sElement::after_render
method and its drawing commands have been processed. Useful for post-processing, debugging, or recording widget positions (e.g.,RelativeFocusExtension
).
The Context
Object
The Context
struct (src/extensions/mod.rs
) is passed to most Extension
hooks and provides access to core OSUI functionalities:
Context::new(screen: Arc<Screen>)
: Creates a newContext
linked to the mainScreen
.context.event<E: Event + Clone + 'static>(&self, e: &E)
: Dispatches an evente
to all widgets (via theirevent
method andHandler<E>
components) and all registered extensions. This is how extensions can inject new events into the system.context.get_widgets()
: Provides aMutexGuard
to theVec<Arc<Widget>>
currently managed by theScreen
. This allows extensions to iterate over and potentially modify widgets.context.iter_components<C, F>()
/context.get_components<C>()
: Helper methods to easily iterate over or collect specific components attached to widgets.
Creating a Custom Extension
Let's create a simple extension that logs every keyboard key pressed to standard output.
// src/my_extension.rs
use std::io::{self, Write};
use osui::prelude::*; // Import OSUI prelude
// Define our custom extension struct
pub struct KeyLoggerExtension;
// Implement the Extension trait for our struct
impl Extension for KeyLoggerExtension {
// Implement the event hook to listen for events
fn event(&mut self, _ctx: &Context, event: &dyn Event) {
// Attempt to downcast the generic `event` to a `crossterm::event::Event`.
// The `InputExtension` dispatches these events.
if let Some(crossterm_event) = event.get::<crossterm::event::Event>() {
match crossterm_event {
crossterm::event::Event::Key(key_event) => {
// Log the key event to stdout.
let _ = writeln!(io::stdout(), "Key Pressed: {:?}", key_event);
let _ = io::stdout().flush();
}
_ => {} // Ignore other crossterm events (mouse, resize)
}
}
}
}
Registering Your Extension
To make your extension active, you must register it with the Screen
instance in your main.rs
or application setup:
// src/main.rs
mod my_extension; // Declare your new module
mod demos; // Assuming you have a demos module as in guides
use osui::prelude::*;
use my_extension::KeyLoggerExtension; // Import your extension
fn main() -> std::io::Result<()> {
let screen = Screen::new();
// Register OSUI's built-in InputExtension (essential for keyboard input)
screen.extension(InputExtension);
// Register OSUI's built-in RelativeFocusExtension (for Tab/Shift+Tab navigation)
screen.extension(RelativeFocusExtension::new());
// Register your custom KeyLoggerExtension
screen.extension(KeyLoggerExtension);
demos::app(screen.clone()).draw(&screen);
screen.run()
}
Now, when you run your application and press keys, you will see Key Pressed: ...
messages in your terminal's output stream (though they might be mixed with the TUI output due to how crossterm
operates).
Built-in Extensions
OSUI includes several powerful built-in extensions that you've already encountered in examples:
InputExtension
: Handles raw terminal input fromcrossterm
.RelativeFocusExtension
: Manages focus between widgets based on their rendered positions, enabling navigation via keyboard (e.g., arrow keys). It introducesFocused
andAlwaysFocused
components.IdExtension
: Provides a way to assign unique IDs to widgets and retrieve them later by ID. It introduces theId
component.TickExtension
: DispatchesTickEvent
s at a configurable interval, useful for animations or periodic updates.VelocityExtension
: Provides simple animation by automatically moving widgets with aVelocity
component.
By implementing custom extensions, you can integrate external libraries, create complex behaviors not covered by simple components, or debug your UI in powerful ways.