Skip to main content

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

1

Capture

Terminal input (keyboard, mouse, resize) is captured by the Native Core via the terminal backend.
2

Classification

Raw input is classified into typed events (Key, Mouse, Resize, FocusChange, Change, Submit).
3

Buffering

Classified events are stored in an internal event buffer.
4

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:
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)

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

Mouse Buttons

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:
1

Tab Key

Advances focus to the next focusable widget in tree order.
2

Shift+Tab

Moves focus to the previous focusable widget in reverse tree order.
3

Mouse Click

Focuses the clicked widget (if focusable).
4

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

Focusable Widgets

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

Widget-Specific Handlers

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

Performance Considerations

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

1

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();
}
2

Use Appropriate Timeout

readInput(timeout) blocks for up to timeout ms. Use 16ms for 60fps responsiveness, or 0ms for non-blocking.
3

Minimize Focus Indicators

Update focus indicators (border color, etc.) in the focus event handler, not by polling focus state every frame.
4

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

Build docs developers (and LLMs) love