Skip to main content
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 TypeDescriptionCommon Use Cases
keyKeyboard inputShortcuts, navigation, quit commands
mouseMouse clicks and movementScroll wheel, button clicks
focusFocus changed to a widgetUpdate UI state, show/hide indicators
changeWidget value changedLive preview, validation
submitEnter key or selection confirmedForm submission, option selection
accessibilityAccessibility eventScreen 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;
}

Handling Mouse Input

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}`);
    }
  }
}

Input Validation

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;
}

Handling Form Submission

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;
}

Form Validator

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) > 0n;

  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

1
Always Drain Events
2
Call app.drainEvents() every frame to process all buffered input.
3
Use Non-Blocking Input During Animations
4
Set readInput(0) when animations are active to maintain 60fps.
5
Handle Escape for Quit
6
Provide a consistent exit path:
7
if (event.type === "key" && event.keyCode === KeyCode.Escape) {
  running = false;
}
8
Validate Before Submit
9
Use change events for live validation and submit for final processing.
10
Store Widget Handles
11
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

Build docs developers (and LLMs) love