Events
Kraken TUI’s Event Module provides comprehensive input handling for keyboard, mouse, and terminal resize events. Events are captured in the Native Core and delivered through a buffer-poll model.
Event Architecture
The Event Module uses a hybrid buffer-poll delivery model. The Native Core captures and buffers events; the Host Layer drains the buffer each tick through explicit calls.
How Events Work
Capture
Terminal input (keyboard, mouse, resize) is captured by the Native Core via the terminal backend.
Classification
Raw input is classified into typed events (Key, Mouse, Resize, FocusChange, Change, Submit).
Buffering
Classified events are stored in an internal event buffer.
Drain
The Host Layer calls app.readInput(timeout) to trigger capture, then app.drainEvents() to pop events from the buffer.
This is not a callback model. The Host Layer controls the event loop cadence. No callbacks cross the FFI boundary.
Event Types
Kraken TUI supports seven event types:
key
mouse
resize
focus
change
submit
accessibility
interface KeyEvent {
type : "key" ;
target : number ; // Focused widget handle
keyCode : number ; // KeyCode enum value
modifiers : number ; // Ctrl/Shift/Alt flags
codepoint : number ; // Unicode codepoint (text input)
}
Fired when: User presses a keyTarget: Currently focused widget (0 if no focus)interface MouseEvent {
type : "mouse" ;
target : number ; // Widget handle at (x, y)
x : number ; // Column (0-based)
y : number ; // Row (0-based)
button : number ; // 0=left, 1=right, 2=middle
modifiers : number ; // Ctrl/Shift/Alt flags
}
Fired when: User clicks or drags mouseTarget: Deepest widget containing (x, y) via hit-testinginterface ResizeEvent {
type : "resize" ;
target : number ; // Always 0 (no specific widget)
width : number ; // New terminal width (columns)
height : number ; // New terminal height (rows)
}
Fired when: Terminal size changesTarget: Always 0 (global event)interface FocusChangeEvent {
type : "focus" ;
target : number ; // New focused widget handle
fromHandle : number ; // Previous focused widget (0 if none)
toHandle : number ; // New focused widget (0 if none)
}
Fired when: Focus moves between widgets (Tab, Shift+Tab, click, widget.focus())interface ChangeEvent {
type : "change" ;
target : number ; // Changed widget handle
selectedIndex ?: number ; // For Select widgets
}
Fired when: Widget value changes (Input text, Select option, TextArea content)interface SubmitEvent {
type : "submit" ;
target : number ; // Submitted widget handle
}
Fired when: User presses Enter in Input or TextAreainterface AccessibilityEvent {
type : "accessibility" ;
target : number ; // Focused widget handle
roleCode : number ; // AccessibilityRole enum value
}
Fired when: Screen reader queries widget role/label/description
Event Loop Patterns
Basic Loop (Manual)
import { Kraken , Box , Text , KeyCode } from "kraken-tui" ;
const app = Kraken . init ();
const root = new Box ();
app . setRoot ( root );
let running = true ;
while ( running ) {
// Read input with 16ms timeout (≈60fps)
app . readInput ( 16 );
// Drain all buffered events
for ( const event of app . drainEvents ()) {
if ( event . type === "key" ) {
// Ctrl+C or Escape to quit
if ( event . keyCode === KeyCode . Esc ||
( event . keyCode === KeyCode . Char_c && event . modifiers & 0x02 )) {
running = false ;
}
}
if ( event . type === "resize" ) {
console . log ( `Terminal resized: ${ event . width } x ${ event . height } ` );
}
}
// Render frame
app . render ();
}
app . shutdown ();
Async Loop (Animation-Aware)
import { Kraken } from "kraken-tui" ;
const app = Kraken . init ();
await app . run ({
mode: "onChange" , // Render on work, idle sleep
idleTimeout: 100 , // Max 100ms idle sleep
onEvent : ( event ) => {
if ( event . type === "key" && event . keyCode === KeyCode . Esc ) {
app . stop ();
}
}
});
app . shutdown ();
The app.run() method (v3) is a host-level convenience that wraps the manual loop pattern. It automatically adjusts sleep duration when animations are active.
Keyboard Events
Key Codes
Common key codes are exposed as constants:
import { KeyCode } from "kraken-tui" ;
KeyCode . Enter // 0x0100
KeyCode . Backspace // 0x0101
KeyCode . Tab // 0x0102
KeyCode . Esc // 0x010E
KeyCode . Up // 0x0103
KeyCode . Down // 0x0104
KeyCode . Left // 0x0105
KeyCode . Right // 0x0106
KeyCode . Home // 0x0107
KeyCode . End // 0x0108
KeyCode . PageUp // 0x0109
KeyCode . PageDown // 0x010A
KeyCode . Delete // 0x010B
KeyCode . Insert // 0x010C
// Function keys
KeyCode . F1 // 0x0110
KeyCode . F2 // 0x0111
// ... F3-F12
// Character keys: codepoint in lower 16 bits
KeyCode . Char_a // 0x0061 (lowercase 'a')
KeyCode . Char_A // 0x0041 (uppercase 'A')
Modifiers
Modifier flags are bitwise OR’d:
const SHIFT = 0x01 ;
const CTRL = 0x02 ;
const ALT = 0x04 ;
if ( event . modifiers & CTRL ) {
// Ctrl is pressed
}
if (( event . modifiers & ( CTRL | SHIFT )) === ( CTRL | SHIFT )) {
// Both Ctrl and Shift pressed
}
Text Input
For character keys, use the codepoint field:
if ( event . type === "key" && event . codepoint > 0 ) {
const char = String . fromCodePoint ( event . codepoint );
console . log ( `User typed: ${ char } ` );
}
Keyboard Example: Command Palette
const commands = new Map ([
[ "Ctrl+S" , "Save" ],
[ "Ctrl+O" , "Open" ],
[ "Ctrl+Q" , "Quit" ],
]);
app . on ( "key" , ( event ) => {
if ( event . modifiers & CTRL ) {
if ( event . keyCode === KeyCode . Char_s ) {
console . log ( "Save command" );
}
if ( event . keyCode === KeyCode . Char_o ) {
console . log ( "Open command" );
}
if ( event . keyCode === KeyCode . Char_q ) {
app . stop ();
}
}
});
Mouse Events
Hit-Testing
The Event Module performs back-to-front hit-testing using layout geometry from the Layout Module. The deepest widget containing (x, y) receives the event.
app . on ( "mouse" , ( event ) => {
console . log ( `Clicked widget ${ event . target } at ( ${ event . x } , ${ event . y } )` );
console . log ( `Button: ${ event . button === 0 ? "left" : "right" } ` );
});
const LEFT_BUTTON = 0 ;
const RIGHT_BUTTON = 1 ;
const MIDDLE_BUTTON = 2 ;
if ( event . button === LEFT_BUTTON ) {
// Left click
}
if ( event . button === RIGHT_BUTTON ) {
// Right click (context menu)
}
Click Handler Example
const button = new Box ();
button . setBorderStyle ( "rounded" );
button . setPadding ( 0 , 2 , 0 , 2 );
const label = new Text ({ content: "Click Me" });
button . append ( label );
app . on ( "mouse" , ( event ) => {
if ( event . target === button . handle && event . button === 0 ) {
console . log ( "Button clicked!" );
// Visual feedback
button . setBackground ( "#00FF00" );
app . render ();
setTimeout (() => {
button . setBackground ( "default" );
app . render ();
}, 100 );
}
});
Mouse support is optional. The Event Module remains operational in keyboard-only mode if the terminal doesn’t support mouse events.
Focus System
Focus Traversal
Kraken TUI implements depth-first, DOM-order focus traversal :
Tab Key
Advances focus to the next focusable widget in tree order.
Shift+Tab
Moves focus to the previous focusable widget in reverse tree order.
Mouse Click
Focuses the clicked widget (if focusable).
Programmatic
Call widget.focus() to move focus explicitly.
Focus Order Example
const form = new Box ({ direction: "column" , gap: 1 });
const input1 = new Input ({ placeholder: "Name" });
const input2 = new Input ({ placeholder: "Email" });
const input3 = new Input ({ placeholder: "Password" });
input1 . setFocusable ( true );
input2 . setFocusable ( true );
input3 . setFocusable ( true );
form . append ( input1 ); // Focus order: 1
form . append ( input2 ); // Focus order: 2
form . append ( input3 ); // Focus order: 3
// Tab cycles: input1 → input2 → input3 → input1
Focus Events
app . on ( "focus" , ( event ) => {
console . log ( `Focus moved from ${ event . fromHandle } to ${ event . toHandle } ` );
if ( event . toHandle === input1 . handle ) {
input1 . setBorderColor ( "cyan" );
}
if ( event . fromHandle === input1 . handle ) {
input1 . setBorderColor ( "default" );
}
app . render ();
});
By default, only interactive widgets are focusable:
Input (single-line text entry)
TextArea (multi-line text editor)
Select (option list)
You can make any widget focusable:
const button = new Box ();
button . setFocusable ( true ); // Now receives focus
app . on ( "focus" , ( event ) => {
if ( event . toHandle === button . handle ) {
button . setBorderColor ( "cyan" );
app . render ();
}
});
app . on ( "key" , ( event ) => {
if ( event . target === button . handle && event . keyCode === KeyCode . Enter ) {
console . log ( "Button activated!" );
}
});
Resize Events
Resize events are captured and buffered. The current render pass completes against previous dimensions. The next render() recomputes layout with new surface dimensions.
app . on ( "resize" , ( event ) => {
console . log ( `Terminal resized to ${ event . width } x ${ event . height } ` );
// Adjust layout for new size
if ( event . width < 80 ) {
sidebar . setVisible ( false ); // Hide sidebar on narrow terminals
} else {
sidebar . setVisible ( true );
}
app . render ();
});
Change and Submit Events
Change Events
Fired when widget value changes:
const input = new Input ();
app . on ( "change" , ( event ) => {
if ( event . target === input . handle ) {
const value = input . getValue ();
console . log ( `Input changed: ${ value } ` );
}
});
Submit Events
Fired when user presses Enter:
const input = new Input ();
app . on ( "submit" , ( event ) => {
if ( event . target === input . handle ) {
const value = input . getValue ();
console . log ( `Form submitted: ${ value } ` );
// Clear input
input . setValue ( "" );
app . render ();
}
});
Event Handler Patterns
Centralized Dispatcher
function handleEvents ( app : Kraken ) {
for ( const event of app . drainEvents ()) {
switch ( event . type ) {
case "key" :
handleKeyPress ( event );
break ;
case "mouse" :
handleMouseClick ( event );
break ;
case "resize" :
handleResize ( event );
break ;
case "focus" :
handleFocusChange ( event );
break ;
case "change" :
handleValueChange ( event );
break ;
case "submit" :
handleSubmit ( event );
break ;
}
}
}
const handlers = new Map < number , ( event : KrakenEvent ) => void >();
function registerHandler ( widget : Widget , handler : ( event : KrakenEvent ) => void ) {
handlers . set ( widget . handle , handler );
}
for ( const event of app . drainEvents ()) {
const handler = handlers . get ( event . target );
if ( handler ) {
handler ( event );
}
}
Command Pattern
interface Command {
execute () : void ;
}
const keyBindings = new Map < number , Command >([
[ KeyCode . F1 , { execute : () => showHelp () }],
[ KeyCode . F5 , { execute : () => refresh () }],
[ KeyCode . Esc , { execute : () => app . stop () }],
]);
app . on ( "key" , ( event ) => {
const command = keyBindings . get ( event . keyCode );
if ( command ) {
command . execute ();
}
});
High-frequency events (mouse movement) can flood the buffer. The Native Core buffers all events until drained. Mitigation: Call app.drainEvents() every frame (60fps = every 16ms). The buffer-poll model prevents individual events from crossing FFI in isolation.
Mouse events trigger O(n) hit-testing traversal (back-to-front through all widgets). Impact: Acceptable for discrete, infrequent mouse events. Not a concern for typical TUI applications.
Tab key triggers O(focusable nodes) traversal to find next/previous focusable widget. Impact: Negligible for typical focus counts (< 100 focusable widgets).
Event Best Practices
Drain Events Every Frame
Always call app.drainEvents() in your event loop, even if you don’t handle every event type. while ( running ) {
app . readInput ( 16 );
for ( const event of app . drainEvents ()) {
// Handle events
}
app . render ();
}
Use Appropriate Timeout
readInput(timeout) blocks for up to timeout ms. Use 16ms for 60fps responsiveness, or 0ms for non-blocking.
Minimize Focus Indicators
Update focus indicators (border color, etc.) in the focus event handler, not by polling focus state every frame.
Batch Renders
Handle all events, then call render() once. Don’t render after every event. // Good: One render per frame
for ( const event of app . drainEvents ()) {
handleEvent ( event );
}
app . render ();
// Avoid: Multiple renders per frame
for ( const event of app . drainEvents ()) {
handleEvent ( event );
app . render (); // Wasteful
}
Next Steps
Widgets Learn about focusable widgets
Animation Trigger animations from events