Kraken TUI provides a comprehensive event system for handling user input. Events are buffered during input polling and drained in your event loop.
Event Types
Kraken TUI supports five core event types:
Event Type Description Common Use Cases keyKeyboard input Shortcuts, navigation, quit commands mouseMouse clicks and movement Scroll wheel, button clicks focusFocus changed to a widget Update UI state, show/hide indicators changeWidget value changed Live preview, validation submitEnter key or selection confirmed Form submission, option selection accessibilityAccessibility event Screen reader integration
Event Structure
All events share a common base structure:
interface KrakenEvent {
type : "key" | "mouse" | "focus" | "change" | "submit" | "accessibility" ;
target : number ; // Widget handle that received the event
// Type-specific fields...
}
Reading Events (Imperative)
In the imperative API, you manually read input and drain events:
import { Kraken , KeyCode } from "kraken-tui" ;
import type { KrakenEvent } from "kraken-tui" ;
const app = Kraken . init ();
// ... build UI ...
let running = true ;
while ( running ) {
// Poll for input (timeout in milliseconds)
app . readInput ( 16 );
// Drain all buffered events
const events : KrakenEvent [] = app . drainEvents ();
for ( const event of events ) {
// Handle each event
if ( event . type === "key" && event . keyCode === KeyCode . Escape ) {
running = false ;
}
}
app . render ();
}
app . shutdown ();
readInput(timeout) blocks for up to timeout milliseconds waiting for input. Use 0 for non-blocking, or 16 (~60fps) for smooth updates.
Keyboard Events
Keyboard events include key codes, modifiers, and character data.
Event Structure
interface KeyEvent {
type : "key" ;
target : number ;
keyCode : number ; // KeyCode enum value
char : string ; // Printable character (if any)
ctrl : boolean ;
alt : boolean ;
shift : boolean ;
}
Handling Key Presses
import { KeyCode } from "kraken-tui" ;
for ( const event of app . drainEvents ()) {
if ( event . type === "key" ) {
// Exit on Escape
if ( event . keyCode === KeyCode . Escape ) {
running = false ;
}
// Tab navigation
if ( event . keyCode === KeyCode . Tab ) {
if ( event . shift ) {
app . focusPrev ();
} else {
app . focusNext ();
}
}
// Ctrl+S shortcut
if ( event . char === "s" && event . ctrl ) {
save ();
}
// Arrow keys
if ( event . keyCode === KeyCode . Up ) {
scrollUp ();
}
if ( event . keyCode === KeyCode . Down ) {
scrollDown ();
}
}
}
Common Key Codes
KeyCode . Enter
KeyCode . Escape
KeyCode . Tab
KeyCode . Backspace
KeyCode . Delete
KeyCode . Up
KeyCode . Down
KeyCode . Left
KeyCode . Right
KeyCode . PageUp
KeyCode . PageDown
KeyCode . Home
KeyCode . End
KeyCode . F1 through KeyCode . F12
Printable Characters
if ( event . type === "key" && event . char ) {
// Printable character was typed
console . log ( "Character:" , event . char );
}
Mouse Events
Mouse events include coordinates, button state, and modifiers.
Event Structure
interface MouseEvent {
type : "mouse" ;
target : number ;
x : number ; // Terminal column (0-based)
y : number ; // Terminal row (0-based)
button : number ; // 0=left, 1=middle, 2=right
pressed : boolean ; // Button down or up
ctrl : boolean ;
alt : boolean ;
shift : boolean ;
}
for ( const event of app . drainEvents ()) {
if ( event . type === "mouse" ) {
console . log ( `Mouse at ( ${ event . x } , ${ event . y } )` );
// Left click
if ( event . button === 0 && event . pressed ) {
handleClick ( event . x , event . y );
}
// Right click
if ( event . button === 2 && event . pressed ) {
showContextMenu ( event . x , event . y );
}
// Drag (button held down)
if ( event . button === 0 && event . pressed ) {
handleDrag ( event . x , event . y );
}
}
}
ScrollBox widgets automatically handle scroll wheel events. Mouse events for scrolling are consumed by the widget.
Focus Events
Focus events fire when a widget gains focus.
Event Structure
interface FocusEvent {
type : "focus" ;
target : number ; // Widget that gained focus
}
Handling Focus Changes
const input = new Input ({ width: 30 , height: 3 , border: "rounded" });
const select = new Select ({ options: [ "A" , "B" ], width: 20 , height: 5 });
app . setId ( "input" , input );
app . setId ( "select" , select );
for ( const event of app . drainEvents ()) {
if ( event . type === "focus" ) {
if ( event . target === input . handle ) {
statusBar . setContent ( "Editing name..." );
inputLabel . setForeground ( "#a6e3a1" );
} else if ( event . target === select . handle ) {
statusBar . setContent ( "Select an option..." );
selectLabel . setForeground ( "#f9e2af" );
}
}
}
Change Events
Change events fire when a widget’s value changes (Input, Select, TextArea).
Event Structure
interface ChangeEvent {
type : "change" ;
target : number ;
// For Select widgets:
selectedIndex ?: number ;
}
Live Preview with Select
const select = new Select ({
options: [ "Dark Mode" , "Light Mode" , "Solarized" ],
width: 25 ,
height: 7 ,
});
for ( const event of app . drainEvents ()) {
if ( event . type === "change" ) {
if ( event . target === select . handle && event . selectedIndex != null ) {
const option = select . getOption ( event . selectedIndex );
applyTheme ( option ); // Live preview as user browses
statusBar . setContent ( `Preview: ${ option } ` );
}
}
}
const input = new Input ({ width: 30 , height: 3 , maxLength: 50 });
for ( const event of app . drainEvents ()) {
if ( event . type === "change" && event . target === input . handle ) {
const value = input . getValue ();
if ( value . length < 3 ) {
errorText . setContent ( "Name must be at least 3 characters" );
errorText . setForeground ( "#f38ba8" );
} else {
errorText . setContent ( "" );
}
}
}
Submit Events
Submit events fire when the user presses Enter (Input, TextArea) or selects an option (Select).
Event Structure
interface SubmitEvent {
type : "submit" ;
target : number ;
}
const input = new Input ({ width: 30 , height: 3 , border: "rounded" });
const select = new Select ({ options: [ "Option 1" , "Option 2" ], width: 25 , height: 5 });
for ( const event of app . drainEvents ()) {
if ( event . type === "submit" ) {
if ( event . target === input . handle ) {
const value = input . getValue ();
statusBar . setContent ( `Submitted: " ${ value } "` );
processInput ( value );
} else if ( event . target === select . handle ) {
const idx = select . getSelected ();
const option = select . getOption ( idx );
statusBar . setContent ( `Selected: ${ option } ` );
applyOption ( option );
}
}
}
Accessibility Events
Accessibility events fire when focus changes on widgets with accessibility annotations. These are useful for screen reader integration.
Event Structure
interface AccessibilityEvent {
type : "accessibility" ;
target : number ;
roleCode ?: number ; // AccessibilityRole enum
}
Handling Accessibility Events
import { AccessibilityRole } from "kraken-tui" ;
// Set accessibility properties
input . setRole ( AccessibilityRole . Input );
input . setLabel ( "Full name" );
input . setDescription ( "Enter your full name" );
for ( const event of app . drainEvents ()) {
if ( event . type === "accessibility" ) {
const roleCode = event . roleCode ?? 0 ;
const roleName = getRoleName ( roleCode );
console . log ( `Focus -> handle= ${ event . target } , role= ${ roleName } ` );
}
}
JSX Event Handlers
In JSX, attach event handlers directly as props:
Handler Props
< Input
width = { 30 }
height = { 3 }
border = "rounded"
focusable = { true }
onKey = { ( event ) => {
if ( event . keyCode === KeyCode . Escape ) {
loop . stop ();
}
} }
onChange = { ( event ) => {
validateInput ();
} }
onSubmit = { ( event ) => {
processForm ();
} }
onFocus = { ( event ) => {
statusText . value = "Editing..." ;
} }
/>
Select Events in JSX
let selectWidget = null ;
< Select
options = { [ "Dark Mode" , "Light Mode" , "Solarized" ] }
width = { 25 }
height = { 7 }
border = "rounded"
focusable = { true }
ref = { ( w ) => ( selectWidget = w ) }
onChange = { ( event ) => {
if ( event . selectedIndex != null ) {
const option = selectWidget . getOption ( event . selectedIndex );
applyTheme ( option );
statusText . value = `Theme: ${ option } ` ;
}
} }
onSubmit = { ( event ) => {
const idx = selectWidget . getSelected ();
const option = selectWidget . getOption ( idx );
confirmTheme ( option );
} }
/>
Global Event Handling with createLoop
import { createLoop , KeyCode } from "kraken-tui" ;
const loop = createLoop ({
app ,
onEvent ( event ) {
// Global event handler (fires before widget handlers)
if ( event . type === "key" ) {
if ( event . keyCode === KeyCode . Escape ) {
loop . stop ();
}
if ( event . char === "?" && event . shift ) {
showHelp ();
}
}
},
onTick () {
// Called each frame after events
updateMetrics ();
},
});
await loop . start ();
With createLoop(), both the global onEvent callback and per-widget JSX handlers fire. Global handlers run first.
Focus Management
Control which widget receives keyboard input:
Programmatic Focus
// Make widget focusable
input . setFocusable ( true );
select . setFocusable ( true );
// Set focus
input . focus ();
// Cycle focus
app . focusNext (); // Tab
app . focusPrev (); // Shift+Tab
// Get current focus
const focused = app . getFocused ();
if ( focused === input . handle ) {
console . log ( "Input is focused" );
}
Focus in JSX
let inputRef = null ;
const tree = (
< Box width = "100%" height = "100%" padding = { 1 } >
< Input
width = { 30 }
height = { 3 }
focusable = { true }
ref = { ( w ) => ( inputRef = w ) }
/>
</ Box >
);
const rootInstance = render ( tree , app );
// Set initial focus after render
if ( inputRef ) inputRef . focus ();
Tab Navigation
By default, Tab and Shift+Tab cycle focus through all focusable widgets in tree order. Override this behavior:
for ( const event of app . drainEvents ()) {
if ( event . type === "key" && event . keyCode === KeyCode . Tab ) {
if ( event . shift ) {
app . focusPrev ();
} else {
app . focusNext ();
}
}
}
Custom Event Handlers
Create reusable event handler functions:
Quit Handler
function handleQuit ( event : KrakenEvent , loop : { stop : () => void }) {
if ( event . type === "key" && event . keyCode === KeyCode . Escape ) {
loop . stop ();
return true ; // Handled
}
return false ;
}
for ( const event of app . drainEvents ()) {
if ( handleQuit ( event , { stop : () => ( running = false ) })) {
continue ;
}
// Handle other events...
}
Keyboard Shortcuts
function handleShortcuts ( event : KrakenEvent ) {
if ( event . type !== "key" ) return false ;
// Ctrl+S: Save
if ( event . char === "s" && event . ctrl ) {
save ();
return true ;
}
// Ctrl+Q: Quit
if ( event . char === "q" && event . ctrl ) {
quit ();
return true ;
}
// F1: Help
if ( event . keyCode === KeyCode . F1 ) {
showHelp ();
return true ;
}
return false ;
}
function createValidator ( inputHandle : number , errorText : Text ) {
return ( event : KrakenEvent ) => {
if ( event . type === "change" && event . target === inputHandle ) {
const value = input . getValue ();
if ( value . length < 3 ) {
errorText . setContent ( "Minimum 3 characters" );
errorText . setForeground ( "#f38ba8" );
return false ;
} else {
errorText . setContent ( "" );
return true ;
}
}
return true ;
};
}
const validator = createValidator ( input . handle , errorText );
Event Loop Patterns
Blocking vs Non-Blocking
// Blocking: Wait for input (saves CPU when idle)
app . readInput ( 100 ); // Block up to 100ms
// Non-blocking: Return immediately
app . readInput ( 0 );
Animation-Aware Loop
import { PERF_ACTIVE_ANIMATIONS } from "kraken-tui/loop" ;
while ( running ) {
const animating = app . getPerfCounter ( PERF_ACTIVE_ANIMATIONS ) > 0 n ;
if ( animating ) {
// Render at 60fps when animating
app . readInput ( 0 );
await Bun . sleep ( 16 );
} else {
// Block on input when idle
app . readInput ( 100 );
}
for ( const event of app . drainEvents ()) {
// Handle events...
}
app . render ();
}
Event Throttling
let lastUpdate = 0 ;
const throttleMs = 100 ;
for ( const event of app . drainEvents ()) {
if ( event . type === "change" ) {
const now = Date . now ();
if ( now - lastUpdate > throttleMs ) {
updateUI ();
lastUpdate = now ;
}
}
}
Best Practices
Call app.drainEvents() every frame to process all buffered input.
Set readInput(0) when animations are active to maintain 60fps.
Provide a consistent exit path:
if ( event . type === "key" && event . keyCode === KeyCode . Escape ) {
running = false ;
}
Use change events for live validation and submit for final processing.
Use app.setId() or JSX ref to identify event targets.
Next Steps
Advanced Patterns Animation choreography, custom themes, and performance tips
Animations Create smooth property transitions
Widgets Complete widget API reference
Events Concepts Learn about event system design