aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Makefile16
-rw-r--r--README.md19
-rw-r--r--kwin-type.c304
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;
+}