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
- Zero allocations in hot paths - All event processing uses pre-allocated buffers
- Lock-free reads - Cursor position and modifier state use
std::atomicfor concurrent access - Session-aware - Integrates with libseat for proper VT switch handling
- Keyboard-first - Magic keybindings (Ctrl+Alt+F1-F12, Backspace, Delete) handled internally
Input Event Flow
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:
- VT Switch Handling: When switching to a different VT, libseat releases device FDs
- Session Management: Only the active session can read from input devices
- 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 16BTN_RIGHT(0x111) → bit 17BTN_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_releaseon tail update - Consumer (pop): Uses
memory_order_acquireon tail read,memory_order_releaseon 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 Event | Handling |
|---|---|
POINTER_MOTION | Relative mouse movement |
POINTER_MOTION_ABSOLUTE | Absolute coordinate devices |
POINTER_BUTTON | Button press/release with double-click detection |
POINTER_AXIS | Scroll wheel (vertical and horizontal) |
KEYBOARD_KEY | Key press/release with modifier tracking |
| Device events | Handled automatically by libinput/udev |
API Summary
Public Methods
| Method | Purpose |
|---|---|
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 |