Skip to content
Waxed Display Server
← Back to Docs

Input Manager

Input Manager

Overview

The InputManager is Waxed’s core input handling subsystem, responsible for processing all keyboard and mouse events through libinput. It provides a lock-free interface to the rest of the system via atomic variables and SPSC (Single Producer Single Consumer) ring buffers.

Located in src/core/input_manager.cpp and include/waxed/core/input_manager.h.

Architecture

The InputManager operates as a single producer (libinput thread) pushing events to multiple consumers (plugins via CoreAPI). It uses:

  • libinput for low-level device access and event handling
  • udev for automatic device discovery and hot-plugging
  • libseat (via SeatManager) for proper session device ownership
  • epoll integration for zero-copy event notification
  • atomic operations for lock-free cursor position queries
  • lock-free ring buffers for event queuing

Key Design Principles

  1. Zero allocations in hot paths - All event processing uses pre-allocated buffers
  2. Lock-free reads - Cursor position and modifier state use std::atomic for concurrent access
  3. Session-aware - Integrates with libseat for proper VT switch handling
  4. Keyboard-first - Magic keybindings (Ctrl+Alt+F1-F12, Backspace, Delete) handled internally

Input Event Flow

Core API (core_api.cpp)

SPSC Ring Buffers

Atomic State

Event Processing

Main Thread

Kernel Layer

Hardware

kernel input

subsystem

device path

get_fd()

FD readable

libinput_interface

check

Magic Keybindings

Ctrl+Alt+F1-F12

→ vt_switch_callback_

Ctrl+Alt+Backspace

→ restart_callback_

Ctrl+Alt+Delete

→ shutdown_callback_

Keyboard/Mouse

Touchpad/etc

udev

(device discovery

and hotplug)

libinput

(context)

epoll loop

SeatManager

(libseat)

InputManager::

process_events()

Mouse Events

Keyboard Events

cursor_x_/y_

(atomic)

buttons_held_

(atomic)

modifiers_held_

(atomic)

Keybinding Check

(Ctrl+Alt+*)

event_queue_

keyboard_event_queue_

core_mouse_event_pop()

core_keyboard_event_pop()

core_get_cursor_position()

Plugins

(Desktop etc)

Plugins

(Desktop etc)

libinput Integration

Context Initialization

auto InputManager::init(seat::SeatManager& seat_manager) -> bool {
    // 1. Store SeatManager reference globally for callbacks
    seat_manager_ = &seat_manager;
    g_input_seat_manager = &seat_manager;

    // 2. Create udev context for device discovery
    udev_ = udev_new();

    // 3. Create libinput context with custom interface
    li_ = libinput_udev_create_context(&interface_, nullptr, udev_);

    // 4. Assign to seat0 (default seat)
    libinput_udev_assign_seat(li_, "seat0");
}

libinput_interface Callbacks

libinput requires callbacks for opening and closing input devices. Waxed routes these through SeatManager to integrate with libseat for proper session management.

static constexpr libinput_interface interface_ = {
    .open_restricted = [](const char* path, int flags, void*) -> int {
        if (g_input_seat_manager) {
            auto result = g_input_seat_manager->open_device(path);
            if (result) {
                return result.value().release();  // Transfer ownership to libinput
            }
        }
        // Fallback: direct open if libseat fails
        int fd = open(path, flags | O_CLOEXEC);
        return fd < 0 ? -errno : fd;
    },
    .close_restricted = [](int fd, void*) -> void {
        if (g_input_seat_manager) {
            core::utils::UniqueFd fd_wrapper(fd);
            auto result = g_input_seat_manager->close_device(std::move(fd_wrapper));
            if (!result) {
                close(fd);  // Last resort fallback
            }
        } else {
            close(fd);
        }
    }
};

SeatManager Integration

SeatManager provides device ownership through libseat:

  1. VT Switch Handling: When switching to a different VT, libseat releases device FDs
  2. Session Management: Only the active session can read from input devices
  3. Hot-plug Support: New devices are automatically discovered via udev and opened through the interface

udev Device Discovery

udev handles automatic device discovery and hot-plugging:

  • Context created with udev_new() during initialization
  • Seat assignment via libinput_udev_assign_seat() binds to “seat0”
  • Automatic monitoring: libinput monitors udev for device changes
  • Hot-plug: Devices added/removed at runtime are handled automatically

No manual device enumeration is required - udev notifies libinput of changes.

epoll-based Event Handling

The InputManager integrates with epoll for efficient I/O multiplexing:

// Get libinput FD for epoll
int fd = input_manager.get_fd();

// Add to epoll
struct epoll_event ev = {.events = EPOLLIN, .data.fd = fd};
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev);

// In event loop:
if (events[i].data.fd == input_fd) {
    input_manager.process_events();
}

process_events() Flow

auto InputManager::process_events() -> void {
    // 1. Dispatch libinput events (reads from FD)
    libinput_dispatch(li_);

    // 2. Process all pending events
    while ((event = libinput_get_event(li_)) != nullptr) {
        switch (libinput_event_get_type(event)) {
            case LIBINPUT_EVENT_POINTER_MOTION: ...
            case LIBINPUT_EVENT_POINTER_BUTTON: ...
            case LIBINPUT_EVENT_KEYBOARD_KEY: ...
            // ...
        }
        libinput_event_destroy(event);
    }
}

Cursor Position Tracking

Atomic Storage

Cursor position is stored in atomic integers for lock-free concurrent access:

std::atomic<int32_t> cursor_x_{0};
std::atomic<int32_t> cursor_y_{0};

Relative Motion (Mouse)

case LIBINPUT_EVENT_POINTER_MOTION: {
    struct libinput_event_pointer* pointer_event =
        libinput_event_get_pointer_event(event);
    double dx = libinput_event_pointer_get_dx(pointer_event);
    double dy = libinput_event_pointer_get_dy(pointer_event);

    // Atomically read current position
    int new_x = cursor_x_.load(std::memory_order_relaxed) + static_cast<int>(dx);
    int new_y = cursor_y_.load(std::memory_order_relaxed) + static_cast<int>(dy);

    // Clamp and store
    new_x = std::clamp(new_x, 0, max_w_);
    new_y = std::clamp(new_y, 0, max_h_);

    cursor_x_.store(new_x, std::memory_order_relaxed);
    cursor_y_.store(new_y, std::memory_order_relaxed);
}

Absolute Motion (Touchpad/Touchscreen)

case LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE: {
    struct libinput_event_pointer* pointer_event =
        libinput_event_get_pointer_event(event);

    uint32_t screen_x = libinput_event_pointer_get_absolute_x_transformed(
        pointer_event, static_cast<uint32_t>(max_w_));
    uint32_t screen_y = libinput_event_pointer_get_absolute_y_transformed(
        pointer_event, static_cast<uint32_t>(max_h_));

    int new_x = std::clamp(static_cast<int>(screen_x), 0, max_w_);
    int new_y = std::clamp(static_cast<int>(screen_y), 0, max_h_);

    cursor_x_.store(new_x, std::memory_order_relaxed);
    cursor_y_.store(new_y, std::memory_order_relaxed);
}

Access Pattern

Plugins can read cursor position without locks:

int x = input_manager.x();  // Atomic load with relaxed ordering
int y = input_manager.y();

Screen Bounds Clamping

Cursor position is clamped to configured screen bounds:

auto InputManager::set_bounds(int width, int height) -> void {
    max_w_ = width;
    max_h_ = height;
}
  • Default bounds: 1920x1080
  • Typical usage: Set to total display layout dimensions
  • Multi-monitor: For extend mode, use sum of all display widths
  • Confinement: set_position() allows RenderLoop to apply irregular display-area-specific clamping

Button State Tracking

Button states are tracked in a bitmask atomically:

std::atomic<uint32_t> buttons_held_{0};

Button Map

uint32_t button_bit = 1 << (button & 0x1F);  // Map to bit
  • BTN_LEFT (0x110) → bit 16
  • BTN_RIGHT (0x111) → bit 17
  • BTN_MIDDLE (0x112) → bit 18

State Updates

case LIBINPUT_EVENT_POINTER_BUTTON: {
    uint32_t button = libinput_event_pointer_get_button(event);
    libinput_button_state state = libinput_event_pointer_get_button_state(event);

    uint32_t button_bit = 1 << (button & 0x1F);
    if (state == LIBINPUT_BUTTON_STATE_PRESSED) {
        buttons_held_.fetch_or(button_bit, std::memory_order_relaxed);
    } else {
        buttons_held_.fetch_and(~button_bit, std::memory_order_relaxed);
    }
}

Access Pattern

uint32_t buttons = input_manager.get_buttons_held()->load(std::memory_order_relaxed);
if (buttons & (1 << 16)) {
    // Left button held
}

Modifier State (Ctrl, Alt, Shift)

Modifier keys are tracked for keybinding detection:

bool ctrl_pressed_{false};
bool alt_pressed_{false};
std::atomic<uint32_t> modifiers_held_{0};

Modifier Tracking

case LIBINPUT_EVENT_KEYBOARD_KEY: {
    uint32_t key = libinput_event_keyboard_get_key(event);
    libinput_key_state key_state = libinput_event_keyboard_get_key_state(event);

    if (key == KEY_LEFTCTRL || key == KEY_RIGHTCTRL) {
        ctrl_pressed_ = (key_state == LIBINPUT_KEY_STATE_PRESSED);
    } else if (key == KEY_LEFTALT || key == KEY_RIGHTALT) {
        alt_pressed_ = (key_state == LIBINPUT_KEY_STATE_PRESSED);
    }

    // Build modifier mask
    uint32_t mod_mask = 0;
    if (ctrl_pressed_) mod_mask |= KEYBOARD_MOD_CTRL;
    if (alt_pressed_) mod_mask |= KEYBOARD_MOD_ALT;
    modifiers_held_.store(mod_mask, std::memory_order_relaxed);
}

Modifier Constants

// From core_api.h
constexpr uint32_t KEYBOARD_MOD_CTRL   = 1 << 0;
constexpr uint32_t KEYBOARD_MOD_ALT    = 1 << 1;
constexpr uint32_t KEYBOARD_MOD_SHIFT  = 1 << 2;

Double-Click Detection

Double-click detection uses a time-based window:

uint64_t last_click_time_{0};
int last_click_button_{0};
static constexpr uint64_t DOUBLE_CLICK_THRESHOLD_NS = 500000000; // 500ms

Detection Logic

if (state == LIBINPUT_BUTTON_STATE_PRESSED) {
    uint64_t now = libinput_event_pointer_get_time_usec(event) * 1000;

    if (button == static_cast<uint32_t>(last_click_button_) &&
        (now - last_click_time_) < DOUBLE_CLICK_THRESHOLD_NS) {
        // Double click detected
        evt.type = MouseEvent::DoubleClick;
        event_queue_.push(evt);
        last_click_time_ = 0;  // Reset to prevent triple-click
    } else {
        // Single click
        evt.type = MouseEvent::ButtonPress;
        event_queue_.push(evt);
        last_click_time_ = now;
        last_click_button_ = static_cast<int>(button);
    }
}

VT Switch Callback

Triggered by Ctrl+Alt+F1-F12:

using VTSwitchCallback = std::function<void(int vt_number)>;

auto set_vt_switch_callback(VTSwitchCallback cb) -> void {
    vt_switch_callback_ = std::move(cb);
}

Detection

if (key_state == LIBINPUT_KEY_STATE_PRESSED && ctrl_pressed_ && alt_pressed_) {
    if (key >= KEY_F1 && key <= KEY_F12) {
        int vt_number = static_cast<int>(key - KEY_F1 + 1);
        LOGC_INFO("CTRL+ALT+F{} detected - requesting VT switch", vt_number);
        if (vt_switch_callback_) {
            vt_switch_callback_(vt_number);
        }
    }
}

Restart Callback

Triggered by Ctrl+Alt+Backspace:

using RestartCallback = std::function<void()>;

auto set_restart_callback(RestartCallback cb) -> void {
    restart_callback_ = std::move(cb);
}

Detection

else if (key == KEY_BACKSPACE) {
    LOGC_INFO("CTRL+ALT+BACKSPACE detected - requesting restart");
    if (restart_callback_) {
        restart_callback_();
    }
}

Shutdown Callback

Triggered by Ctrl+Alt+Delete:

using ShutdownCallback = std::function<void()>;

auto set_shutdown_callback(ShutdownCallback cb) -> void {
    shutdown_callback_ = std::move(cb);
}

Detection

else if (key == KEY_DELETE) {
    LOGC_INFO("CTRL+ALT+DELETE detected - requesting shutdown");
    if (shutdown_callback_) {
        shutdown_callback_();
    }
}

Event Queues

The InputManager uses lock-free SPSC ring buffers to pass events to plugins.

Mouse Event Queue

static constexpr size_t EVENT_QUEUE_SIZE = 64;

struct EventQueue {
    std::array<MouseEvent, EVENT_QUEUE_SIZE> buffer;
    std::atomic<size_t> head{0};
    std::atomic<size_t> tail{0};

    auto push(const MouseEvent& event) -> bool;  // Producer (InputManager)
    auto pop(MouseEvent& event) -> bool;         // Consumer (CoreAPI)
};

Keyboard Event Queue

struct KeyboardEventQueue {
    std::array<KeyboardEvent, EVENT_QUEUE_SIZE> buffer;
    std::atomic<size_t> head{0};
    std::atomic<size_t> tail{0};

    auto push(const KeyboardEvent& event) -> bool;
    auto pop(KeyboardEvent& event) -> bool;
};

Memory Ordering

  • Producer (push): Uses memory_order_release on tail update
  • Consumer (pop): Uses memory_order_acquire on tail read, memory_order_release on head update
  • Relaxed loads: Used for head/tail checks within each thread

Integration with CoreAPI

CoreAPI provides the C interface for plugins to access input events:

// Mouse events
auto core_mouse_event_pop(void* queue, MouseEvent* event) -> bool;

// Keyboard events
auto core_keyboard_event_pop(void* queue, KeyboardEvent* event) -> bool;

// Direct state access
auto core_get_cursor_position(void* input_manager, int* x, int* y) -> void;

MouseEvent Structure

struct MouseEvent {
    enum Type {
        Motion = 0,
        ButtonPress,
        ButtonRelease,
        DoubleClick,
        WheelScroll
    };

    Type type;
    uint8_t button;      // Linux input button code (truncated to 8 bits)
    int16_t delta;       // Scroll delta
    int32_t x;           // Cursor X position
    int32_t y;           // Cursor Y position
    uint64_t timestamp;  // Nanoseconds
};

KeyboardEvent Structure

struct KeyboardEvent {
    enum Type {
        KeyPress = 0,
        KeyRelease
    };

    Type type;
    uint32_t key_code;   // Linux input key code (KEY_* defines)
    uint32_t modifiers;  // Bitmask of KEYBOARD_MOD_* flags
    uint64_t timestamp;  // Nanoseconds
};

Destruction Safety

The destructor carefully prevents use-after-free:

InputManager::~InputManager() {
    // Clear global pointer FIRST to prevent callbacks from accessing SeatManager
    if (g_input_seat_manager == seat_manager_) {
        g_input_seat_manager = nullptr;
    }

    if (li_) {
        libinput_unref(li_);
        li_ = nullptr;
    }
    if (udev_) {
        udev_unref(udev_);
        udev_ = nullptr;
    }
}

This prevents libinput’s close_restricted callback from accessing a destructed SeatManager.

Supported Event Types

libinput EventHandling
POINTER_MOTIONRelative mouse movement
POINTER_MOTION_ABSOLUTEAbsolute coordinate devices
POINTER_BUTTONButton press/release with double-click detection
POINTER_AXISScroll wheel (vertical and horizontal)
KEYBOARD_KEYKey press/release with modifier tracking
Device eventsHandled automatically by libinput/udev

API Summary

Public Methods

MethodPurpose
init(seat_manager)Initialize libinput with udev and SeatManager
get_fd()Get file descriptor for epoll
process_events()Handle all pending libinput events
x(), y()Get current cursor position (atomic)
set_position(x, y)Set cursor position directly
set_bounds(w, h)Configure screen clamping bounds
set_vt_switch_callback(cb)Register VT switch handler
set_restart_callback(cb)Register restart handler
set_shutdown_callback(cb)Register shutdown handler
get_event_queue()Get mouse event queue for CoreAPI
get_keyboard_event_queue()Get keyboard event queue for CoreAPI
get_buttons_held()Get atomic button state pointer
get_modifiers_held()Get atomic modifier state pointer
get_cursor_x_atomic()Get atomic cursor X pointer
get_cursor_y_atomic()Get atomic cursor Y pointer