diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Makefile | 16 | ||||
| -rw-r--r-- | README.md | 19 | ||||
| -rw-r--r-- | kwin-type.c | 304 |
4 files changed, 340 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f11683a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/kwin-type diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f190f47 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved. + +PKG_CONFIG ?= pkg-config +CFLAGS ?= -O2 -march=native +CFLAGS += -std=gnu23 -Wall -Wextra +PACKAGES := 'wayland-client >= 1.20' 'xkbcommon >= 1.0' +CFLAGS += $(shell $(PKG_CONFIG) --cflags $(PACKAGES)) +LDLIBS += $(shell $(PKG_CONFIG) --libs $(PACKAGES)) + +kwin-type: + +clean: + $(RM) kwin-type + +.PHONY: clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..a4a199c --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +## kwin-type +#### by Jason A. Donenfeld ([Jason@zx2c4.com](mailto:Jason@zx2c4.com)) + +Autotype text from stdin, on KDE's KWin Wayland compositor. + +### Building + +Requires `wayland-client` and `xkbcommon`. + +``` bash +$ make +``` + +### Usage + +```bash +$ printf 'Hello, world!\n' | kwin-type +$ kwin-type < text.txt +``` diff --git a/kwin-type.c b/kwin-type.c new file mode 100644 index 0000000..3a34f45 --- /dev/null +++ b/kwin-type.c @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2026 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved. + */ + +#define _GNU_SOURCE +#include <err.h> +#include <errno.h> +#include <endian.h> +#include <iconv.h> +#include <poll.h> +#include <signal.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/types.h> +#include <time.h> +#include <wayland-client.h> +#include <xkbcommon/xkbcommon.h> + +/* https://invent.kde.org/libraries/plasma-wayland-protocols/-/blob/master/src/protocols/fake-input.xml */ +static const struct wl_message input_requests[] = { + { "authenticate", "ss", NULL }, + { "pointer_motion", "ff", NULL }, + { "button", "uu", NULL }, + { "axis", "uf", NULL }, + { "touch_down", "2uff", NULL }, + { "touch_motion", "2uff", NULL }, + { "touch_up", "2u", NULL }, + { "touch_cancel", "2", NULL }, + { "touch_frame", "2", NULL }, + { "pointer_motion_absolute", "3ff", NULL }, + { "keyboard_key", "4uu", NULL }, + { "destroy", "5", NULL }, + { "keyboard_keysym", "6uu", NULL }, +}; +static const struct wl_interface input_interface = { "org_kde_kwin_fake_input", 6, 13, input_requests, 0, NULL }; + +/* https://invent.kde.org/libraries/plasma-wayland-protocols/-/blob/master/src/protocols/keystate.xml */ +static const struct wl_message state_requests[] = { { "fetchStates", "", NULL } }; +static const struct wl_message state_events[] = { { "stateChanged", "uu", NULL } }; +static const struct wl_interface state_interface = { "org_kde_kwin_keystate", 1, 1, state_requests, 1, state_events }; +enum { AUTHENTICATE = 0, KEY = 10, DESTROY = 11, KEYSYM = 12 }; +enum { RELEASED = 0, PRESSED = 1, CAPS_LOCK_KEY = 58 }; + +static struct wl_display *display; +static struct wl_proxy *input, *keystate; +static sigset_t wait_mask; +static bool caps_known, caps_locked; +static volatile sig_atomic_t interrupted; + +static void on_signal(int signo) +{ + interrupted = signo; +} + +static bool read_text(xkb_keysym_t **keysyms, size_t *length) +{ + iconv_t converter; + xkb_keysym_t *buffer = NULL; + char *text = NULL, *resized, *in, *out; + size_t capacity = 4096, bytes = 0, count, in_left, out_left, i; + + converter = iconv_open(__BYTE_ORDER == __LITTLE_ENDIAN ? "UTF-32LE" : "UTF-32BE", "UTF-8"); + if (converter == (iconv_t)-1) { + warn("opening UTF-8 converter"); + return false; + } + do { + resized = reallocarray(text, capacity, 2); + if (!resized) { + warn("allocating input buffer"); + goto err; + } + text = resized; + capacity *= 2; + bytes += fread(text + bytes, 1, capacity - bytes, stdin); + } while (!feof(stdin) && !ferror(stdin)); + if (ferror(stdin)) { + warn("reading stdin"); + goto err; + } + buffer = reallocarray(NULL, bytes ?: 1, sizeof(*buffer)); + if (!buffer) { + warn("allocating keysym buffer"); + goto err; + } + in = text; + in_left = bytes; + out = (char *)buffer; + out_left = bytes * sizeof(*buffer); + if (iconv(converter, &in, &in_left, &out, &out_left) == (size_t)-1) { + warn("decoding UTF-8"); + goto err; + } + count = (out - (char *)buffer) / sizeof(*buffer); + for (i = 0; i < count; ++i) { + buffer[i] = xkb_utf32_to_keysym(buffer[i]); + if (buffer[i] == XKB_KEY_Linefeed) + buffer[i] = XKB_KEY_Return; + } + free(text); + iconv_close(converter); + *keysyms = buffer; + *length = count; + return true; + +err: + free(buffer); + free(text); + iconv_close(converter); + return false; +} + +static void sync_done(void *data, struct wl_callback *, uint32_t) +{ + *(bool *)data = true; +} + +static bool sync_display(void) +{ + static const struct wl_callback_listener listener = { .done = sync_done }; + bool done = false, ok = false; + struct wl_callback *callback = wl_display_sync(display); + struct timespec deadline, remaining; + struct pollfd fd; + int ready; + + if (!callback) + return false; + wl_callback_add_listener(callback, &listener, &done); + clock_gettime(CLOCK_MONOTONIC, &deadline); + deadline.tv_sec += 2; + while (!done) { + if (wl_display_dispatch_pending(display) < 0) + break; + if (done) { + ok = true; + break; + } + if (wl_display_prepare_read(display) < 0) + continue; + fd = (struct pollfd){ .fd = wl_display_get_fd(display), .events = POLLIN }; + if (wl_display_flush(display) < 0) { + if (errno != EAGAIN) { + wl_display_cancel_read(display); + break; + } + fd.events |= POLLOUT; + } + clock_gettime(CLOCK_MONOTONIC, &remaining); + remaining.tv_sec = deadline.tv_sec - remaining.tv_sec; + remaining.tv_nsec = deadline.tv_nsec - remaining.tv_nsec; + if (remaining.tv_nsec < 0) { + --remaining.tv_sec; + remaining.tv_nsec += 1000000000; + } + ready = remaining.tv_sec >= 0 ? ppoll(&fd, 1, &remaining, &wait_mask) : 0; + if (ready <= 0 || (fd.revents & (POLLERR | POLLHUP | POLLNVAL))) { + wl_display_cancel_read(display); + if (ready < 0 && errno == EINTR) + continue; + break; + } + if (fd.revents & POLLIN) { + if (wl_display_read_events(display) < 0) + break; + } else { + wl_display_cancel_read(display); + } + } + wl_callback_destroy(callback); + if (!ok) + warnx("Wayland connection failed or timed out"); + return ok; +} + +static void state_changed(void *, struct wl_proxy *, uint32_t key, uint32_t state) +{ + if (key == 0) { + caps_known = true; + caps_locked = state != 0; + } +} + +static void global(void *, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) +{ + static void (*listener[])(void) = { (void (*)(void))state_changed }; + + if (!strcmp(interface, input_interface.name) && version >= (uint32_t)input_interface.version && !input) { + input = wl_registry_bind(registry, name, &input_interface, input_interface.version); + } else if (!strcmp(interface, state_interface.name) && !keystate) { + keystate = wl_registry_bind(registry, name, &state_interface, state_interface.version); + wl_proxy_add_listener(keystate, listener, NULL); + } +} + +static void global_remove(void *, struct wl_registry *, uint32_t) +{ +} + +static void type_key(uint32_t request, uint32_t key) +{ + wl_proxy_marshal_flags(input, request, NULL, input_interface.version, 0, key, PRESSED); + wl_proxy_marshal_flags(input, request, NULL, input_interface.version, 0, key, RELEASED); +} + +int main(int argc, char **argv) +{ + static const struct wl_registry_listener listener = { global, global_remove }; + struct sigaction action = { .sa_handler = on_signal }; + xkb_keysym_t *keysyms = NULL; + size_t length = 0, i; + int result = 1; + struct wl_registry *registry = NULL; + bool restore_caps = false, restore_mask = false; + + if (argc != 1) + errx(1, "usage: %s < text.txt", argv[0]); + if (!read_text(&keysyms, &length)) + goto out; + if (!length) { + result = 0; + goto out; + } + sigemptyset(&action.sa_mask); + sigaddset(&action.sa_mask, SIGINT); + sigaddset(&action.sa_mask, SIGTERM); + sigaddset(&action.sa_mask, SIGHUP); + if (sigprocmask(SIG_BLOCK, &action.sa_mask, &wait_mask) < 0) { + warn("blocking signals"); + goto out; + } + restore_mask = true; + if (sigaction(SIGINT, &action, NULL) < 0 || sigaction(SIGTERM, &action, NULL) < 0 || + sigaction(SIGHUP, &action, NULL) < 0) { + warn("installing signal handlers"); + goto out; + } + + display = wl_display_connect(NULL); + if (!display) { + warn("cannot connect to the Wayland display"); + goto out; + } + registry = wl_display_get_registry(display); + wl_registry_add_listener(registry, &listener, NULL); + if (!sync_display()) + goto out; + if (!input || !keystate) { + warnx("KWin Fake Input v%d and key-state protocols are required", input_interface.version); + goto out; + } + wl_proxy_marshal_flags(input, AUTHENTICATE, NULL, input_interface.version, 0, + "kwin-type", "Type text from stdin"); + wl_proxy_marshal_flags(keystate, 0, NULL, state_interface.version, 0); + if (!sync_display()) + goto out; + if (!caps_known) { + warnx("cannot read Caps Lock state"); + goto out; + } + if (interrupted) + goto out; + if (caps_locked) { + restore_caps = true; + type_key(KEY, CAPS_LOCK_KEY); + if (!sync_display()) + goto out; + if (caps_locked) { + restore_caps = false; + warnx("could not turn Caps Lock off"); + goto out; + } + } + result = 0; + for (i = 0; i < length && !interrupted; ++i) { + type_key(KEYSYM, keysyms[i]); + if ((i + 1) % 64 == 0 && !sync_display()) { + result = 1; + break; + } + } +out: + if (input) { + if (restore_caps) + type_key(KEY, CAPS_LOCK_KEY); + wl_proxy_marshal_flags(input, DESTROY, NULL, input_interface.version, WL_MARSHAL_FLAG_DESTROY); + if (!sync_display()) + result = 1; + } + if (keystate) + wl_proxy_destroy(keystate); + if (registry) + wl_registry_destroy(registry); + if (display) + wl_display_disconnect(display); + free(keysyms); + if (restore_mask) + sigprocmask(SIG_SETMASK, &wait_mask, NULL); + return interrupted ? 128 + interrupted : result; +} |
