Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# axidev-io

`axidev-io` is a C library for keyboard input injection and global keyboard
listening on Windows and Linux.
`axidev-io` is a C library for keyboard input injection, global keyboard
listening, and mouse cursor/button observation on Windows and Linux.

## Build

Expand Down Expand Up @@ -62,12 +62,36 @@ int main(void) {
}
```

Mouse polling and listening use the same single global runtime:

```c
static void on_mouse(const axidev_io_mouse_state_t *state, void *user_data) {
printf("Mouse: x=%d, y=%d, buttons=%u\n",
state->x, state->y, state->buttons);
}

int main(void) {
axidev_io_mouse_state_t state;
axidev_io_mouse_poll(&state);

if (!axidev_io_mouse_listener_start(on_mouse, NULL)) {
return 1;
}

axidev_io_mouse_listener_stop();
return 0;
}
```

## Notes

- Normal sender calls do not take sender handles, listener handles, or context
handles.
- Shared runtime state lives in the single exported `axidev_io_global`
pointer.
- Mouse callbacks report cursor/button state snapshots. On Linux, libinput
does not expose the compositor cursor position directly, so the library tracks
cursor position from observed pointer events.
- Printable text sending prefers layout-resolved key sequences; explicit
`key_down`, `key_up`, and `tap` remain available for low-level control.
- `axidev_io_keyboard_key_down(key_mod, true)` requests backend-managed repeat
Expand Down
3 changes: 3 additions & 0 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
Path("src/core/log.c"),
Path("src/internal/utf.c"),
Path("src/vendor/stb_ds_impl.c"),
Path("src/mouse/mouse_common.c"),
Path("src/keyboard/common/key_utils.c"),
Path("src/keyboard/common/keymap.c"),
]
Expand Down Expand Up @@ -158,6 +159,7 @@ def make_config(build_dir: Path) -> BuildConfig:
Path("src/keyboard/common/windows_keymap.c"),
Path("src/keyboard/sender/sender_windows.c"),
Path("src/keyboard/listener/listener_windows.c"),
Path("src/mouse/mouse_windows.c"),
]
)
platform_libs.extend(["-luser32", "-lkernel32"])
Expand Down Expand Up @@ -189,6 +191,7 @@ def make_config(build_dir: Path) -> BuildConfig:
Path("src/keyboard/common/linux_keysym.c"),
Path("src/keyboard/sender/sender_uinput.c"),
Path("src/keyboard/listener/listener_linux.c"),
Path("src/mouse/mouse_linux.c"),
]
)

Expand Down
19 changes: 17 additions & 2 deletions docs/consumers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,19 @@ Example:
- Callbacks may run on an internal background thread.
- Keep listener callbacks thread-safe and short.

## Mouse

- `axidev_io_mouse_poll()` returns the latest cursor/button snapshot.
- `axidev_io_mouse_listener_start()` starts the single global mouse listener.
- Mouse callbacks receive an `axidev_io_mouse_state_t` with cursor position,
button bitmask, scroll deltas for the current event, and a monotonic
timestamp.
- On Windows, polling reads the current cursor and button state from Win32.
- On Linux, listening uses libinput on `seat0` and updates the tracked state
from pointer events. libinput does not expose the desktop compositor cursor
position directly, so polling returns the latest tracked state rather than
querying a compositor-specific pointer API.

## Errors And Logging

- Failure details are available through `axidev_io_get_last_error()`.
Expand All @@ -183,7 +196,9 @@ Example:
## Platform Notes

- Windows uses the Win32 keyboard APIs for injection and a low-level hook for
listening.
listening. Mouse observation uses Win32 cursor/button APIs and a low-level
mouse hook.
- Linux injection uses `uinput`.
- Linux listening uses `libinput` plus `xkbcommon`.
- Linux keyboard listening uses `libinput` plus `xkbcommon`; mouse listening
uses `libinput`.
- macOS is not supported in this repository.
45 changes: 45 additions & 0 deletions include/axidev-io/c_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ extern "C" {
#define AXIDEV_IO_KEYBOARD_SENDER_STORAGE_SIZE 4096u
#define AXIDEV_IO_KEYBOARD_LISTENER_STORAGE_SIZE 16384u
#define AXIDEV_IO_KEYBOARD_KEYMAP_STORAGE_SIZE 8192u
#define AXIDEV_IO_MOUSE_STORAGE_SIZE 4096u
#define AXIDEV_IO_GLOBAL_PRIVATE_STORAGE_SIZE 2048u

typedef union axidev_io_keyboard_sender_storage_t {
Expand All @@ -55,6 +56,11 @@ typedef union axidev_io_keyboard_keymap_storage_t {
unsigned char bytes[AXIDEV_IO_KEYBOARD_KEYMAP_STORAGE_SIZE];
} axidev_io_keyboard_keymap_storage_t;

typedef union axidev_io_mouse_storage_t {
max_align_t _align;
unsigned char bytes[AXIDEV_IO_MOUSE_STORAGE_SIZE];
} axidev_io_mouse_storage_t;

typedef union axidev_io_global_private_storage_t {
max_align_t _align;
unsigned char bytes[AXIDEV_IO_GLOBAL_PRIVATE_STORAGE_SIZE];
Expand Down Expand Up @@ -356,6 +362,29 @@ typedef void (*axidev_io_keyboard_listener_cb)(
uint32_t codepoint, axidev_io_keyboard_key_with_modifier_t key_mod,
bool pressed, void *user_data);

typedef uint16_t axidev_io_mouse_button_t;

enum {
AXIDEV_IO_MOUSE_BUTTON_NONE = 0x0000,
AXIDEV_IO_MOUSE_BUTTON_LEFT = 0x0001,
AXIDEV_IO_MOUSE_BUTTON_RIGHT = 0x0002,
AXIDEV_IO_MOUSE_BUTTON_MIDDLE = 0x0004,
AXIDEV_IO_MOUSE_BUTTON_BACK = 0x0008,
AXIDEV_IO_MOUSE_BUTTON_FORWARD = 0x0010
};

typedef struct axidev_io_mouse_state_t {
int32_t x;
int32_t y;
axidev_io_mouse_button_t buttons;
double scroll_x;
double scroll_y;
uint64_t timestamp_ms;
} axidev_io_mouse_state_t;

typedef void (*axidev_io_mouse_listener_cb)(
const axidev_io_mouse_state_t *state, void *user_data);

typedef struct axidev_io_keyboard_sender_context {
bool initialized;
bool ready;
Expand Down Expand Up @@ -386,6 +415,14 @@ typedef struct axidev_io_keyboard_context {
axidev_io_keyboard_keymap_context keymap;
} axidev_io_keyboard_context;

typedef struct axidev_io_mouse_context {
bool initialized;
bool is_listening;
axidev_io_keyboard_backend_type_t backend_type;
axidev_io_mouse_state_t state;
axidev_io_mouse_storage_t storage;
} axidev_io_mouse_context;

typedef uint8_t axidev_io_log_level_t;

enum {
Expand All @@ -399,6 +436,7 @@ typedef struct axidev_io_global_context {
axidev_io_log_level_t log_level;
char *last_error;
axidev_io_keyboard_context keyboard;
axidev_io_mouse_context mouse;
axidev_io_global_private_storage_t private_storage;
} axidev_io_global_context;

Expand Down Expand Up @@ -435,6 +473,13 @@ AXIDEV_IO_API bool axidev_io_listener_start(axidev_io_keyboard_listener_cb cb,
AXIDEV_IO_API void axidev_io_listener_stop(void);
AXIDEV_IO_API bool axidev_io_listener_is_listening(void);

AXIDEV_IO_API bool axidev_io_mouse_poll(axidev_io_mouse_state_t *out_state);
AXIDEV_IO_API bool
axidev_io_mouse_listener_start(axidev_io_mouse_listener_cb cb,
void *user_data);
AXIDEV_IO_API void axidev_io_mouse_listener_stop(void);
AXIDEV_IO_API bool axidev_io_mouse_listener_is_listening(void);

AXIDEV_IO_API char *
axidev_io_keyboard_key_to_string(axidev_io_keyboard_key_t key);
AXIDEV_IO_API axidev_io_keyboard_key_t
Expand Down
49 changes: 49 additions & 0 deletions src/c_api.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "keyboard/common/keymap_internal.h"
#include "keyboard/listener/listener_internal.h"
#include "keyboard/sender/sender_internal.h"
#include "mouse/mouse_internal.h"

static void axidev_io_report_result(const char *function_name,
axidev_io_result result) {
Expand Down Expand Up @@ -440,6 +441,54 @@ AXIDEV_IO_API bool axidev_io_listener_is_listening(void) {
return axidev_io_global->keyboard.listener.is_listening;
}

AXIDEV_IO_API bool axidev_io_mouse_poll(axidev_io_mouse_state_t *out_state) {
axidev_io_result result;

axidev_io_context_ensure_runtime();
axidev_io_clear_last_error_internal();
if (out_state == NULL) {
axidev_io_report_result("axidev_io_mouse_poll",
AXIDEV_IO_RESULT_INVALID_ARGUMENT);
return false;
}

axidev_io_context_lock();
result = axidev_io_mouse_poll_internal(out_state);
if (result != AXIDEV_IO_RESULT_OK) {
axidev_io_report_result("axidev_io_mouse_poll", result);
}
axidev_io_context_unlock();
return result == AXIDEV_IO_RESULT_OK;
}

AXIDEV_IO_API bool
axidev_io_mouse_listener_start(axidev_io_mouse_listener_cb cb,
void *user_data) {
axidev_io_result result;

axidev_io_context_ensure_runtime();
axidev_io_clear_last_error_internal();
axidev_io_context_lock();
result = axidev_io_mouse_listener_start_internal(cb, user_data);
if (result != AXIDEV_IO_RESULT_OK) {
axidev_io_report_result("axidev_io_mouse_listener_start", result);
}
axidev_io_context_unlock();
return result == AXIDEV_IO_RESULT_OK;
}

AXIDEV_IO_API void axidev_io_mouse_listener_stop(void) {
axidev_io_context_ensure_runtime();
axidev_io_context_lock();
axidev_io_mouse_listener_stop_internal();
axidev_io_context_unlock();
}

AXIDEV_IO_API bool axidev_io_mouse_listener_is_listening(void) {
axidev_io_context_ensure_runtime();
return axidev_io_global->mouse.is_listening;
}

AXIDEV_IO_API char *
axidev_io_keyboard_key_to_string(axidev_io_keyboard_key_t key) {
axidev_io_context_ensure_runtime();
Expand Down
6 changes: 6 additions & 0 deletions src/core/context.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ static void axidev_io_context_init_once(void) {
axidev_io_mutex_init(&runtime->error_lock);
axidev_io_global->log_level = AXIDEV_IO_LOG_LEVEL_INFO;
axidev_io_keyboard_reset_public_state();
axidev_io_mouse_reset_public_state();
}

void axidev_io_context_ensure_runtime(void) {
Expand Down Expand Up @@ -161,3 +162,8 @@ void axidev_io_keyboard_reset_public_state(void) {
axidev_io_keyboard_reset_public_keymap_state();
axidev_io_global->keyboard.backend_type = AXIDEV_IO_BACKEND_UNKNOWN;
}

void axidev_io_mouse_reset_public_state(void) {
memset(&axidev_io_global->mouse, 0, sizeof(axidev_io_global->mouse));
axidev_io_global->mouse.backend_type = AXIDEV_IO_BACKEND_UNKNOWN;
}
9 changes: 9 additions & 0 deletions src/internal/context.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ void axidev_io_keyboard_reset_public_sender_state(void);
void axidev_io_keyboard_reset_public_listener_state(void);
void axidev_io_keyboard_reset_public_keymap_state(void);
void axidev_io_keyboard_reset_public_state(void);
void axidev_io_mouse_reset_public_state(void);

static inline void *axidev_io_sender_storage_ptr(void) {
return (void *)axidev_io_global->keyboard.sender.storage.bytes;
Expand All @@ -48,6 +49,10 @@ static inline void *axidev_io_keymap_storage_ptr(void) {
return (void *)axidev_io_global->keyboard.keymap.storage.bytes;
}

static inline void *axidev_io_mouse_storage_ptr(void) {
return (void *)axidev_io_global->mouse.storage.bytes;
}

static inline axidev_io_keyboard_sender_context *
axidev_io_sender_public_context(void) {
return &axidev_io_global->keyboard.sender;
Expand All @@ -63,6 +68,10 @@ axidev_io_keymap_public_context(void) {
return &axidev_io_global->keyboard.keymap;
}

static inline axidev_io_mouse_context *axidev_io_mouse_public_context(void) {
return &axidev_io_global->mouse;
}

static inline bool
axidev_io_keyboard_has_modifier(axidev_io_keyboard_modifier_t state,
axidev_io_keyboard_modifier_t flag) {
Expand Down
71 changes: 71 additions & 0 deletions src/mouse/mouse_common.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#include "mouse_internal.h"

axidev_io_mouse_impl *axidev_io_mouse_impl_get(void) {
return (axidev_io_mouse_impl *)axidev_io_mouse_storage_ptr();
}

bool axidev_io_mouse_prepare_locks(axidev_io_mouse_impl *impl) {
if (impl == NULL) {
return false;
}
if (!impl->callback_lock_ready) {
if (!axidev_io_mutex_init(&impl->callback_lock)) {
return false;
}
impl->callback_lock_ready = true;
}
if (!impl->state_lock_ready) {
if (!axidev_io_mutex_init(&impl->state_lock)) {
return false;
}
impl->state_lock_ready = true;
}
return true;
}

void axidev_io_mouse_store_state(axidev_io_mouse_impl *impl,
const axidev_io_mouse_state_t *state) {
if (impl == NULL || state == NULL) {
return;
}
if (impl->state_lock_ready) {
axidev_io_mutex_lock(&impl->state_lock);
}
axidev_io_mouse_public_context()->state = *state;
if (impl->state_lock_ready) {
axidev_io_mutex_unlock(&impl->state_lock);
}
}

void axidev_io_mouse_load_state(axidev_io_mouse_impl *impl,
axidev_io_mouse_state_t *out_state) {
if (out_state == NULL) {
return;
}
if (impl != NULL && impl->state_lock_ready) {
axidev_io_mutex_lock(&impl->state_lock);
}
*out_state = axidev_io_mouse_public_context()->state;
if (impl != NULL && impl->state_lock_ready) {
axidev_io_mutex_unlock(&impl->state_lock);
}
}

void axidev_io_mouse_invoke_callback(axidev_io_mouse_impl *impl,
const axidev_io_mouse_state_t *state) {
axidev_io_mouse_listener_cb callback = NULL;
void *user_data = NULL;

if (impl == NULL || state == NULL) {
return;
}

axidev_io_mutex_lock(&impl->callback_lock);
callback = impl->callback;
user_data = impl->user_data;
axidev_io_mutex_unlock(&impl->callback_lock);

if (callback != NULL) {
callback(state, user_data);
}
}
Loading
Loading