Skip to content

Event Handling

maya's event system gives you keyboard, mouse, paste, focus, and resize events through a unified Event variant type and a set of ergonomic predicate/helper functions.

Program Apps: Declarative Event Handling

In the Program architecture (run<P>(RunConfig)), event handling is declarative. You define a subscribe() function that returns a Sub<Msg> describing which events map to which messages. The framework dispatches matched messages to your update() function.

key_map: Simple Key-to-Message Mapping

For straightforward key bindings, use key_map<Msg>():

static auto subscribe(const Model&) -> Sub<Msg> {
    return key_map<Msg>({
        {'q', Quit{}}, {'+', Increment{}},
        {SpecialKey::Up, MoveUp{}},
    });
}

Sub::on_key: Complex Key Matching

For more complex matching logic, use Sub<Msg>::on_key():

Sub<Msg>::on_key([](const KeyEvent& k) -> std::optional<Msg> {
    if (key_is(k, 'q')) return Quit{};
    if (ctrl_is(k, 'c')) return Quit{};
    return std::nullopt;
})

The predicates key_is(), ctrl_is(), and alt_is() work on KeyEvent& (not Event&) and are designed for use inside Sub<Msg>::on_key() filters.

The Event Type

using Event = std::variant<KeyEvent, MouseEvent, PasteEvent, FocusEvent, ResizeEvent>;

You never inspect the variant directly — instead, use the predicate functions.

Keyboard Events

Key Predicates

key(ev, 'q')                   // Was 'q' pressed?
key(ev, '+')                   // Was '+' pressed?
key(ev, SpecialKey::Escape)    // Was Escape pressed?
key(ev, SpecialKey::Enter)     // Was Enter pressed?
key(ev, SpecialKey::Up)        // Was arrow up pressed?

Modifier Keys

ctrl(ev, 'c')    // Ctrl+C
ctrl(ev, 's')    // Ctrl+S
alt(ev, 'x')     // Alt+X
shift(ev, SpecialKey::Tab)  // Shift+Tab (BackTab)

Any Key

any_key(ev)      // Was any key pressed?

Special Keys

SpecialKey::Up, Down, Left, Right
SpecialKey::Home, End
SpecialKey::PageUp, PageDown
SpecialKey::Tab, BackTab
SpecialKey::Backspace, Delete, Insert
SpecialKey::Enter, Escape
SpecialKey::F1 ... F12

Raw Key Access

if (auto* ke = as_key(ev)) {
    // ke->key is the Key variant (CharKey or SpecialKey)
    // ke->mods has .ctrl, .alt, .shift, .super_
    // ke->raw_sequence is the raw ANSI bytes
}

Fire-and-Forget Handlers (canvas_run / legacy callbacks)

The on() helper combines a predicate with an action — returns bool (true if matched). This is used in canvas_run() event callbacks:

// Single key
on(ev, 'q', [&] { should_quit = true; });

// Two keys (either matches)
on(ev, '+', '=', [&] { count++; });
on(ev, '-', '_', [&] { count--; });

// Special key
on(ev, SpecialKey::Enter, [&] { submit(); });

on() in Practice

In canvas_run(), the event handler is a function that receives events. Use on() for each action:

[&](const Event& ev) {
    on(ev, '+', '=', [&] { count.update([](int& n) { ++n; }); });
    on(ev, '-', '_', [&] { count.update([](int& n) { --n; }); });
    on(ev, 'r',      [&] { count.set(0); });
    on(ev, 't',      [&] { theme = (theme + 1) % kThemeCount; });
    return !key(ev, 'q');  // false = quit
}

Mouse Events

Enable mouse events in the config:

run<P>({.mouse = true});
// or
canvas_run({.mouse = true}, ...);

Mouse Predicates

mouse_clicked(ev)                        // Left button clicked
mouse_clicked(ev, MouseButton::Right)    // Right button clicked
mouse_clicked(ev, MouseButton::Middle)   // Middle button clicked
mouse_released(ev)                       // Left button released
mouse_moved(ev)                          // Mouse moved
scrolled_up(ev)                          // Scroll wheel up
scrolled_down(ev)                        // Scroll wheel down

Mouse Position

if (auto pos = mouse_pos(ev)) {
    int col = pos->col;   // 1-based column
    int row = pos->row;   // 1-based row
}

Coordinates are frame-relative, not absolute. In inline mode (Mode::Inline) your UI is drawn partway down the terminal, but the runtime translates the raw SGR position into your frame's coordinate space (it learns the frame's top row via a one-time cursor-position query when mouse is enabled). So a click on the top-left cell reports (1, 1) whether the app is at the top of the screen or 20 rows down — identical to fullscreen mode. Mouse events that land outside the frame (in the surrounding scrollback) are dropped before reaching your handler.

Raw Mouse Access

if (auto* me = as_mouse(ev)) {
    // me->button: MouseButton enum
    // me->kind: MouseEventKind (Press, Release, Move)
    // me->x: Columns, me->y: Rows
    // me->mods: Modifiers (.ctrl, .alt, .shift)
}

Mouse Example (canvas_run callback)

canvas_run(
    {.mouse = true},
    on_resize,
    [&](const Event& ev) {
        if (mouse_clicked(ev)) {
            auto pos = mouse_pos(ev);
            if (pos) {
                click_x = pos->col;
                click_y = pos->row;
            }
        }
        if (scrolled_up(ev))   zoom_in();
        if (scrolled_down(ev)) zoom_out();
        return !key(ev, 'q');
    },
    on_paint
);

For Program apps, mouse events are handled via Sub<Msg>::on_mouse() in subscribe().

Mouse capture vs. native terminal scroll — set_mouse()

While mouse reporting is on, the terminal delivers the scroll wheel to your app (as mouse buttons), so the terminal's own scrollback stops scrolling until the app exits. This is the terminal mouse protocol, not maya — no app can have in-app clicks and native scrollback at the same time. Capture is always released on exit.

When you need to switch between the two at runtime, call maya::set_mouse() (declared in maya/app/quit.hpp, alongside maya::quit()):

#include "maya/app/quit.hpp"

run({.mouse = true}, [&](const Event& ev) {
    if (key(ev, 'm')) maya::set_mouse(false);  // release wheel → terminal scrolls
    if (key(ev, 'M')) maya::set_mouse(true);   // recapture clicks/drag/wheel
    if (key(ev, 'q')) maya::quit();
    return true;
}, render);

set_mouse(bool) sets a pending request that the run() / Program loop applies on its next iteration (mirroring quit()), emitting the enable/disable sequence and keeping the runtime's mouse state in sync so the terminal is correctly restored on exit. It also works from a Program's update() / subscribe(). If an app doesn't need the mouse, simply leave mouse = false (the default) and native terminal scroll works untouched.

Resize Events

if (resized(ev)) {
    // Terminal was resized
}

int w, h;
if (resized(ev, &w, &h)) {
    // w and h now hold the new terminal size
}

In run() with Ctx, the context's size field is automatically updated on resize, so you usually don't need to handle this yourself.

Paste Events

If the terminal supports bracketed paste:

std::string pasted_text;
if (pasted(ev, &pasted_text)) {
    // pasted_text contains the clipboard content
}

Focus Events

if (focused(ev))   { /* Terminal gained focus */ }
if (unfocused(ev)) { /* Terminal lost focus */ }

Event Handler Patterns

Program Pattern: Declarative subscribe()

For run<P>() apps, define a subscribe() function:

static auto subscribe(const Model&) -> Sub<Msg> {
    return key_map<Msg>({
        {'q', Quit{}}, {'+', Increment{}}, {'-', Decrement{}},
        {'r', Reset{}}, {'t', CycleTheme{}},
    });
}

static auto update(Model model, Msg msg) -> std::pair<Model, Cmd<Msg>> {
    return std::visit(overload{
        [&](Quit)       { return std::pair{model, Cmd<Msg>::quit()}; },
        [&](Increment)  { model.count++; return std::pair{model, Cmd<Msg>::none()}; },
        // ...
    }, msg);
}

canvas_run() Callback Patterns

These patterns apply to canvas_run() event callbacks.

Pattern 1: Bool Return (Quit Control)

// Return false to quit, true to continue
[&](const Event& ev) -> bool {
    on(ev, '+', [&] { count++; });
    on(ev, '-', [&] { count--; });
    if (key(ev, 'q') || key(ev, SpecialKey::Escape)) return false;
    return true;
}

Pattern 2: Void Return (Use quit())

// Call maya::quit() to exit
[&](const Event& ev) {
    on(ev, '+', [&] { count++; });
    on(ev, '-', [&] { count--; });
    on(ev, 'q', [] { quit(); });
}

Pattern 3: Complex Event Dispatch

[&](const Event& ev) {
    // Key shortcuts
    on(ev, 't', [&] { theme = (theme + 1) % kThemeCount; });
    on(ev, 'T', [&] { theme = (theme + 1) % kThemeCount; });
    on(ev, 'p', [&] { paused = !paused; });
    on(ev, ' ', [&] { trigger_action(); });

    // Speed control
    on(ev, '+', '=', [&] { speed = std::min(5.0f, speed + 0.25f); });
    on(ev, '-', '_', [&] { speed = std::max(0.1f, speed - 0.25f); });

    // Mouse
    if (auto pos = mouse_pos(ev)) {
        hover_col = pos->col - 1;
    }
    if (mouse_clicked(ev)) {
        auto pos = mouse_pos(ev);
        if (pos) do_click(pos->col, pos->row);
    }
    if (scrolled_up(ev))   speed *= 1.25f;
    if (scrolled_down(ev)) speed *= 0.8f;

    // Quit
    return !(key(ev, 'q') || key(ev, SpecialKey::Escape));
}

Input Parsing Internals

maya's InputParser is a state-machine that parses raw terminal bytes into structured events:

  • Ground → Normal character input
  • Escape → Start of escape sequence
  • CSI → Control Sequence Introducer (ESC [)
  • SS3 → Single Shift 3 (ESC O) — some function keys
  • OSC → Operating System Command
  • BracketedPaste → Paste content between markers

The parser handles: - UTF-8 multi-byte sequences - CSI parameter parsing (cursor keys, function keys, modifiers) - SGR mouse reports (click, release, move, scroll with position) - Bracketed paste (start/end markers) - Focus events (in/out) - Ambiguous Escape (50ms timeout to distinguish ESC key from escape sequence)

You never interact with InputParser directly — the framework handles it.