From 9b34bfc43561ec6890ffc0e8dc5ef4faa932f60c Mon Sep 17 00:00:00 2001 From: mrsobakin <68982655+mrsobakin@users.noreply.github.com> Date: Wed, 26 Mar 2025 13:11:43 +0300 Subject: [PATCH] Initial commit --- .gitignore | 6 ++ Makefile | 52 ++++++++++++ include/ukb.h | 51 ++++++++++++ src/backends/sway.c | 191 ++++++++++++++++++++++++++++++++++++++++++++ src/main.c | 86 ++++++++++++++++++++ src/ukb.c | 59 ++++++++++++++ src/utils.h | 11 +++ 7 files changed, 456 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 include/ukb.h create mode 100644 src/backends/sway.c create mode 100644 src/main.c create mode 100644 src/ukb.c create mode 100644 src/utils.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..276e9cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.o +*.so +*.a +ukb +.cache/ +compile_commands.json diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..702edec --- /dev/null +++ b/Makefile @@ -0,0 +1,52 @@ +CC = gcc +AR = ar + +DEPS = json-c + +CFLAGS = -Iinclude -Wall -O2 $(shell pkg-config --cflags $(DEPS)) +LDFLAGS = $(shell pkg-config --libs $(DEPS)) + +LIB_SRC = src/ukb.c $(wildcard src/backends/*.c) +LIB_OBJ = $(LIB_SRC:.c=.o) + +BIN_SRC = src/main.c +BIN_OBJ = $(BIN_SRC:.c=.o) + +PREFIX ?= /usr/local +BIN_DIR = $(PREFIX)/bin +LIB_DIR = $(PREFIX)/lib +INCLUDE_DIR = $(PREFIX)/include/libukb + +all: libukb.a libukb.so ukb + +$(LIB_OBJ): %.o: %.c + $(CC) $(CFLAGS) -fPIC -DUKB_BACKENDS_INTERNAL -c $< -o $@ + +$(BIN_OBJ): %.o: %.c + $(CC) $(CFLAGS) -c $< -o $@ + +libukb.so: $(LIB_OBJ) + $(CC) -shared -o $@ $^ $(LDFLAGS) + +libukb.a: $(LIB_OBJ) + $(AR) rcs $@ $^ + +ukb: $(BIN_OBJ) libukb.a + $(CC) $(BIN_OBJ) -L. -l:libukb.a -o $@ $(LDFLAGS) + +.PHONY: install +install: all + install -d $(BIN_DIR) $(LIB_DIR) $(INCLUDE_DIR) + install -m 0755 ukb $(BIN_DIR)/ + install -m 0644 libukb.a libukb.so $(LIB_DIR)/ + install -m 0644 include/*.h $(INCLUDE_DIR)/ + +.PHONY: uninstall +uninstall: + rm -f $(BIN_DIR)/ukb + rm -f $(LIB_DIR)/libukb.a $(LIB_DIR)/libukb.so + rm -rf $(INCLUDE_DIR) + +.PHONY: clean +clean: + rm -f $(LIB_OBJ) $(BIN_OBJ) ukb libukb.so libukb.a diff --git a/include/ukb.h b/include/ukb.h new file mode 100644 index 0000000..77cafec --- /dev/null +++ b/include/ukb.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include + + +typedef struct ukb_err { + const char* msg; +} ukb_err_t; + + +typedef void (*ukb_layout_cb_t)(const char *layout); + +typedef struct ukb_backend ukb_backend_t; + + +#ifdef UKB_BACKENDS_INTERNAL +struct ukb_backend { + const char *name; + bool (*can_use)(void); + ukb_err_t (*listen)(ukb_layout_cb_t); +}; +#endif + + +// Array of all registered backends. +extern const ukb_backend_t* ukb_backends[]; + +// Number of all registered backends. +extern size_t ukb_backends_number; + + +// Find first available keyboard backend. +// If no available backends were found, NULL is returned. +const ukb_backend_t* ukb_find_available(void); + +// Find keyboard backend with the given name. +// Before using the backend, you must verify that it is currently available by calling its `can_use` function. +// If backend with the given name was not found, NULL is returned. +const ukb_backend_t* ukb_find(const char* name); + + +// Get backend name. +const char* ukb_backend_name(const ukb_backend_t*); + +// Check whether this backend is available. +bool ukb_backend_can_use(const ukb_backend_t*); + +// Listen for layout changes and report them to the given callback. +// Upon calling this function, current layout will be immediately reported to callback. +ukb_err_t ukb_backend_listen(const ukb_backend_t*, ukb_layout_cb_t); diff --git a/src/backends/sway.c b/src/backends/sway.c new file mode 100644 index 0000000..a7dfd9c --- /dev/null +++ b/src/backends/sway.c @@ -0,0 +1,191 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ukb.h" +#include "../utils.h" + + +static char current_layout[128] = {0}; + +/********************************* SWAY IPC **********************************/ + +static const char SWAY_MAGIC[] = {'i', '3', '-', 'i', 'p', 'c'}; +static const char INPUT_SUB_MSG[] = "[\"input\"]"; + +#define SWAY_IPC_SUBSCRIBE 0x2 +#define SWAY_IPC_INPUT 0x80000015 +#define SWAY_IPC_GET_INPUTS 100 + + +typedef union header { + uint8_t raw[14]; + #pragma pack(push, 1) + struct fields { + uint8_t magic[6]; + uint32_t length; + uint32_t type; + } fields; + #pragma pack(pop) +} header_t; + + +static int sway_ipc_open(const char *socket_path) { + int socketfd; + if ((socketfd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) { + return -1; + } + + struct sockaddr_un addr = { + .sun_family = AF_UNIX, + .sun_path = {0}, + }; + strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); + + if (connect(socketfd, (struct sockaddr *)&addr, sizeof(struct sockaddr_un)) == -1) { + return -1; + } + + return socketfd; +} + +static bool sway_ipc_send(int fd, uint32_t type, uint32_t length, const char *data) { + header_t h; + memcpy(h.fields.magic, SWAY_MAGIC, 6); + h.fields.length = length; + h.fields.type = type; + + if (write(fd, h.raw, sizeof(header_t)) == -1) return false; + if (write(fd, data, length) == -1) return false; + + return true; +} + +static bool sway_ipc_recv(int fd, header_t *h, char **data) { + uint32_t left = sizeof(header_t); + uint8_t *dst = h->raw; + ssize_t n; + + while (left) { + if ((n = recv(fd, dst, left, 0)) == -1) { + return false; + } + dst += n; + left -= n; + } + + *data = (char*)malloc(h->fields.length); + left = h->fields.length; + dst = (uint8_t*)*data; + + while (left) { + if ((n = recv(fd, dst, left, 0)) == -1) { + free(*data); + return false; + } + dst += n; + left -= n; + } + + return true; +} + +static bool sway_update_layout_with_json(json_object *j) { + const char *new_layout = json_object_get_string(json_object_object_get(j, "xkb_active_layout_name")); + + if (!new_layout) return false; + + if (strncmp(current_layout, new_layout, sizeof(current_layout)) != 0) { + strncpy(current_layout, new_layout, sizeof(current_layout) - 1); + return true; + } + + return false; +} + +static bool sway_update_layout_with_event(json_object *j) { + const char *change = json_object_get_string(json_object_object_get(j, "change")); + if (strcmp(change, "xkb_layout") != 0) return false; + + return sway_update_layout_with_json(json_object_object_get(j, "input")); +} + +static ukb_err_t sway_init_layout(int fd) { + if (!sway_ipc_send(fd, SWAY_IPC_GET_INPUTS, 0, NULL)) { + UKB_ERR("layout init: ipc send error"); + } + + header_t h; + char *payload; + if (!sway_ipc_recv(fd, &h, &payload)) { + UKB_ERR("layout init: ipc recv error"); + } + + json_object *j = json_tokener_parse(payload); + + int n = json_object_array_length(j); + for (int i = 0; i < n; i++) { + sway_update_layout_with_json(json_object_array_get_idx(j, i)); + } + + json_object_put(j); + free(payload); + + UKB_OK(); +} + +/********************************* BACKEND ***********************************/ + +static bool sway_can_use(void) { + return getenv("SWAYSOCK") != NULL; +} + +static ukb_err_t sway_listen(ukb_layout_cb_t cb) { + const char *swaysock = getenv("SWAYSOCK"); + if (swaysock == NULL) { + UKB_ERR("listen: SWAYSOCK env is not defined"); + } + + int fd = sway_ipc_open(swaysock); + if (fd == -1) { + UKB_ERR("listen: ipc open error"); + } + + UKB_PROPAGATE(sway_init_layout(fd)); + + cb(current_layout); + + sway_ipc_send(fd, SWAY_IPC_SUBSCRIBE, sizeof(INPUT_SUB_MSG), INPUT_SUB_MSG); + + while (1) { + header_t h; + char *payload; + if (!sway_ipc_recv(fd, &h, &payload)) { + UKB_ERR("listen: ipc recv error"); + } + + if (h.fields.type == SWAY_IPC_INPUT) { + json_object *j = json_tokener_parse(payload); + + if (sway_update_layout_with_event(j)) { + cb(current_layout); + } + + json_object_put(j); + } + + free(payload); + } +} + +const ukb_backend_t ukb_backend_sway = { + .name = "sway", + .can_use = sway_can_use, + .listen = sway_listen, +}; diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..2d87a78 --- /dev/null +++ b/src/main.c @@ -0,0 +1,86 @@ +#include +#include +#include +#include + +#include "ukb.h" + +#define _maybe_fprintf(...) if (!args.silent) { fprintf(__VA_ARGS__); }; +#define info(msg, ...) _maybe_fprintf(stderr, "\033[33m" msg "\033[0m\n", ##__VA_ARGS__); +#define fatal(msg, ...) _maybe_fprintf(stderr, "\033[31m" msg "\033[0m\n", ##__VA_ARGS__); exit(1); + +static struct { + bool silent; + const char* backend; +} args; + +static const char* usage = + "Usage: ukb [-h] [-s] [-b backend]\n" + "\n" + " -h show this help page\n" + " -s be silent and don't print anything to stderr\n" + " -b manually specify backend that should be used\n" + " it will be used even if it is reported as unavailable\n" + ""; + + +void parse_args(int argc, char **argv) { + int opt; + + args.silent = false; + args.backend = NULL; + + while ((opt = getopt(argc, argv, "shb:")) != -1) { + switch (opt) { + case 's': + args.silent = true; + break; + + case 'b': + args.backend = optarg; + break; + + case '?': + fatal("Invalid option or missing argument"); + break; + + case 'h': + default: + fprintf(stderr, "%s", usage); + exit(1); + break; + } + } +} + + +void cb_print(const char* layout) { + printf("%s\n", layout); + fflush(stdout); +} + +int main(int argc, char **argv) { + parse_args(argc, argv); + + const ukb_backend_t *b; + + if (args.backend) { + b = ukb_find(args.backend); + if (!b) { + fatal("ukb: backend was not found: %s", args.backend); + } + } else { + b = ukb_find_available(); + if (!b) { + fatal("ukb: no available backend was found"); + } + } + + info("ukb: Using %s backend", ukb_backend_name(b)); + + ukb_err_t err = ukb_backend_listen(b, cb_print); + + if (err.msg) { + fatal("ukb: %s", err.msg); + } +} diff --git a/src/ukb.c b/src/ukb.c new file mode 100644 index 0000000..c968498 --- /dev/null +++ b/src/ukb.c @@ -0,0 +1,59 @@ +#include + +#include +#include + +#include "utils.h" + + +#define DECLARE(backend) extern const ukb_backend_t backend; +#define REFERENCE(backend) &backend, + +/********************************* BACKENDS **********************************/ + +#define UKB_BACKENDS(M) \ + M(ukb_backend_sway) + +/*****************************************************************************/ + +UKB_BACKENDS(DECLARE) + +const ukb_backend_t* ukb_backends[] = { + UKB_BACKENDS(REFERENCE) +}; + +size_t ukb_backends_number = ARRAY_SIZE(ukb_backends); + + +const ukb_backend_t* ukb_find_available(void) { + for (size_t i = 0; i < ukb_backends_number; i++) { + if (ukb_backends[i]->can_use()) { + return ukb_backends[i]; + } + } + + return NULL; +} + +const ukb_backend_t* ukb_find(const char* name) { + for (size_t i = 0; i < ukb_backends_number; i++) { + if (strcmp(ukb_backends[i]->name, name) == 0) { + return ukb_backends[i]; + } + } + + return NULL; +} + + +const char* ukb_backend_name(const ukb_backend_t *backend) { + return backend->name; +} + +bool ukb_backend_can_use(const ukb_backend_t *backend) { + return backend->can_use(); +} + +ukb_err_t ukb_backend_listen(const ukb_backend_t *backend, ukb_layout_cb_t cb) { + return backend->listen(cb); +} diff --git a/src/utils.h b/src/utils.h new file mode 100644 index 0000000..fee5425 --- /dev/null +++ b/src/utils.h @@ -0,0 +1,11 @@ +#pragma once + +#define ARRAY_SIZE(array) (sizeof(array) / sizeof(array[0])) + +#define UKB_OK() return (ukb_err_t) { .msg = NULL }; +#define UKB_ERR(m) return (ukb_err_t) { .msg = m }; +#define UKB_PROPAGATE(expr) \ + { \ + ukb_err_t err = expr; \ + if (err.msg != NULL) return err; \ + };