aboutsummaryrefslogtreecommitdiffstats
path: root/kwin-type.c
diff options
context:
space:
mode:
authorJason A. Donenfeld <Jason@zx2c4.com>2026-10-27 04:05:07 +0100
committerJason A. Donenfeld <Jason@zx2c4.com>2026-10-27 04:40:32 +0100
commit0bc344b0d4a490d91d3a7e5e0cbe6481a4e88ab4 (patch)
tree45316a01ae17151f33d267abaa57680ddd713e26 /kwin-type.c
parentInitial commit (diff)
downloadkwin-type-master.tar.xz
kwin-type-master.zip
Use KWin's EIS interface to send UTF-8 textHEADmaster
Rather than using the fake-input Wayland protocol, use the private KWin D-Bus endpoint, org.kde.KWin.EIS.RemoteDesktop, which can give us a fd to pass to libei, without needing portals. libei then lets us send 254 bytes of UTF-8 at a time. So we do a little bit of shuffling to make sure we're not in the middle of a UTF-8 string.
Diffstat (limited to '')
-rw-r--r--kwin-type.c267
1 files changed, 99 insertions, 168 deletions
diff --git a/kwin-type.c b/kwin-type.c
index 00f2369..4120f84 100644
--- a/kwin-type.c
+++ b/kwin-type.c
@@ -4,185 +4,116 @@
*/
#define _GNU_SOURCE
+#include <dbus/dbus.h>
#include <err.h>
-#include <locale.h>
+#include <libei.h>
+#include <poll.h>
#include <signal.h>
-#include <stdint.h>
#include <stdio.h>
#include <string.h>
-#include <time.h>
-#include <wchar.h>
-#include <wayland-client.h>
-#include <xkbcommon/xkbcommon.h>
-
-#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
-
-/* https://invent.kde.org/libraries/plasma-wayland-protocols/-/blob/master/src/protocols/fake-input.xml */
-static const struct wl_message input_methods[] = {
- { "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, ARRAY_SIZE(input_methods), input_methods, 0, NULL };
-
-/* https://invent.kde.org/libraries/plasma-wayland-protocols/-/blob/master/src/protocols/keystate.xml */
-static const struct wl_message state_methods[] = { { "fetchStates", "", NULL } };
-static const struct wl_message state_events[] = { { "stateChanged", "uu", NULL } };
-static const struct wl_interface state_interface = { "org_kde_kwin_keystate", 1, ARRAY_SIZE(state_methods), state_methods, ARRAY_SIZE(state_events), state_events };
-enum { AUTHENTICATE = 0, KEY = 10, DESTROY = 11, KEYSYM = 12 };
-enum { RELEASED = 0, PRESSED = 1, CAPS_LOCK_KEY = 58 };
-
-static volatile sig_atomic_t interrupted;
-
-static void on_signal(int signo)
-{
- interrupted = signo;
-}
-
-static void state_changed(void *data, struct wl_proxy *, uint32_t key, uint32_t state)
-{
- bool **caps = data;
- if (key == 0) {
- *caps[0] = true;
- *caps[1] = state != 0;
- }
-}
-
-static void registry_global(void *data, struct wl_registry *, uint32_t name, const char *interface, uint32_t version)
-{
- uint32_t **names = data;
- if (!strcmp(interface, input_interface.name) && version >= (uint32_t)input_interface.version)
- *names[0] = name;
- else if (!strcmp(interface, state_interface.name))
- *names[1] = name;
-}
-
-static void registry_global_remove(void *, struct wl_registry *, uint32_t)
-{
-}
-
-static void type_key(struct wl_proxy *input, 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);
-}
+#include <unistd.h>
int main(int argc, char **argv)
{
- bool caps_known = false, caps_locked = false, restore_caps = false, ret = false;
- uint32_t input_name = 0, keystate_name = 0;
- struct wl_proxy *input, *keystate;
- struct wl_registry *registry;
- struct wl_display *display;
- xkb_keysym_t keysym;
- wint_t cp;
+ dbus_int32_t capabilities = 1, cookie;
+ DBusError error = DBUS_ERROR_INIT;
+ DBusMessage *request, *reply;
+ DBusConnection *bus;
+ struct ei_ping *ping = NULL;
+ struct ei_device *device;
+ struct ei_event *event;
+ struct ei_seat *seat;
+ struct ei *ei;
+ size_t length = 0, chunk;
+ bool done = false, valid;
+ char text[255], saved;
+ int fd, ret;
if (argc != 1)
errx(1, "usage: %s < text.txt", argv[0]);
- if (!setlocale(LC_CTYPE, "C.UTF-8"))
- errx(1, "cannot select C.UTF-8 locale");
- if (sigaction(SIGINT, &(struct sigaction){ .sa_handler = on_signal }, NULL) < 0 ||
- sigaction(SIGTERM, &(struct sigaction){ .sa_handler = on_signal }, NULL) < 0 ||
- sigaction(SIGHUP, &(struct sigaction){ .sa_handler = on_signal }, NULL) < 0) {
- warn("installing signal handlers");
- goto err;
- }
- display = wl_display_connect(NULL);
- if (!display) {
- warn("cannot connect to display");
- goto err;
- }
- registry = wl_display_get_registry(display);
- if (!registry) {
- warn("getting registry");
- goto err_display;
- }
- wl_registry_add_listener(registry,
- &(const struct wl_registry_listener){ registry_global, registry_global_remove },
- (uint32_t *[]){ &input_name, &keystate_name });
- if (wl_display_roundtrip(display) < 0) {
- warn("synchronizing display");
- goto err_registry;
- }
- if (!input_name || !keystate_name) {
- warnx("%s-v%d and %s-v%d protocols are required",
- input_interface.name, input_interface.version, state_interface.name, state_interface.version);
- goto err_registry;
- }
- keystate = wl_registry_bind(registry, keystate_name, &state_interface, state_interface.version);
- if (!keystate) {
- warn("binding key-state interface");
- goto err_registry;
- }
- wl_proxy_add_listener(keystate,
- (void (*[])(void)){ (void (*)(void))state_changed },
- (bool *[]){ &caps_known, &caps_locked });
- input = wl_registry_bind(registry, input_name, &input_interface, input_interface.version);
- if (!input) {
- warn("binding fake-input interface");
- goto err_keystate;
- }
- 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 (wl_display_roundtrip(display) < 0) {
- warn("synchronizing display");
- goto err_input;
- }
- if (!caps_known) {
- warnx("cannot read caps lock state");
- goto err_input;
- }
-
- /* Chrome has some complicated keyboard refreshing logic. We could patch upstream kwin to
- * hack around it, or fix Chrome itself, but it's probably easier to just delay briefly
- * here. Probably other things will benefit from this hack too. Oh well. */
- nanosleep(&(struct timespec){ .tv_nsec = 100000000 }, NULL);
-
- if (interrupted)
- goto err_input;
- if (caps_locked) {
- restore_caps = true;
- type_key(input, KEY, CAPS_LOCK_KEY);
- }
-
- while (!interrupted && (cp = fgetwc(stdin)) != WEOF) {
- if (interrupted)
- break;
- keysym = xkb_utf32_to_keysym(cp);
- if (keysym == XKB_KEY_Linefeed)
- keysym = XKB_KEY_Return;
- type_key(input, KEYSYM, keysym);
- }
- if (!interrupted && ferror(stdin))
- warn("reading stdin");
- else
- ret = true;
+ bus = dbus_bus_get_private(DBUS_BUS_SESSION, &error);
+ if (!bus)
+ errx(1, "connecting to session bus: %s", error.message);
+ request = dbus_message_new_method_call("org.kde.KWin", "/org/kde/KWin/EIS/RemoteDesktop",
+ "org.kde.KWin.EIS.RemoteDesktop", "connectToEIS");
+ if (!request || !dbus_message_append_args(request, DBUS_TYPE_INT32, &capabilities, DBUS_TYPE_INVALID))
+ errx(1, "allocating D-Bus request");
+ reply = dbus_connection_send_with_reply_and_block(bus, request, -1, &error);
+ dbus_message_unref(request);
+ if (!reply)
+ errx(1, "connecting to KWin EIS: %s", error.message);
+ if (!dbus_message_get_args(reply, &error, DBUS_TYPE_UNIX_FD, &fd,
+ DBUS_TYPE_INT32, &cookie, DBUS_TYPE_INVALID))
+ errx(1, "reading KWin EIS reply: %s", error.message);
+ dbus_message_unref(reply);
-err_input:
- if (restore_caps)
- type_key(input, KEY, CAPS_LOCK_KEY);
- wl_proxy_marshal_flags(input, DESTROY, NULL, input_interface.version, WL_MARSHAL_FLAG_DESTROY);
- if (wl_display_roundtrip(display) < 0) {
- warn("synchronizing display");
- ret = false;
+ ei = ei_new_sender(NULL);
+ if (!ei)
+ errx(1, "creating libei context");
+ ei_configure_name(ei, "kwin-type");
+ ret = ei_setup_backend_fd(ei, fd);
+ if (ret < 0)
+ errx(1, "connecting to EIS: %s", strerror(-ret));
+ while (!done) {
+ ei_dispatch(ei);
+ while ((event = ei_get_event(ei))) {
+ switch (ei_event_get_type(event)) {
+ case EI_EVENT_SEAT_ADDED:
+ seat = ei_event_get_seat(event);
+ if (!ei_seat_has_capability(seat, EI_DEVICE_CAP_TEXT))
+ errx(1, "KWin EIS does not support text input");
+ ei_seat_bind_capabilities(seat, EI_DEVICE_CAP_TEXT, NULL);
+ break;
+ case EI_EVENT_DEVICE_RESUMED:
+ device = ei_event_get_device(event);
+ if (ping || !ei_device_has_capability(device, EI_DEVICE_CAP_TEXT))
+ break;
+ ei_device_start_emulating(device, 1);
+ while (!feof(stdin)) {
+ length += fread(text + length, 1, sizeof(text) - 1 - length, stdin);
+ if (ferror(stdin))
+ err(1, "reading stdin");
+ if (!length)
+ break;
+ if (memchr(text, '\0', length))
+ errx(1, "stdin must not contain NUL bytes");
+ text[length] = '\0';
+ for (chunk = length; ; --chunk) {
+ saved = text[chunk];
+ text[chunk] = '\0';
+ valid = dbus_validate_utf8(text, NULL);
+ text[chunk] = saved;
+ if (valid)
+ break;
+ if (feof(stdin) || length - chunk == 3)
+ errx(1, "stdin must be valid UTF-8");
+ }
+ ei_device_text_utf8_with_length(device, text, chunk);
+ ei_device_frame(device, ei_now(ei));
+ length -= chunk;
+ memmove(text, text + chunk, length);
+ }
+ ei_device_stop_emulating(device);
+ ping = ei_new_ping(ei);
+ ei_ping(ping);
+ break;
+ case EI_EVENT_PONG:
+ done = true;
+ break;
+ case EI_EVENT_DISCONNECT:
+ errx(1, "disconnected from KWin EIS");
+ default:
+ break;
+ }
+ ei_event_unref(event);
+ }
+ if (!done && poll(&(struct pollfd){ .fd = ei_get_fd(ei), .events = POLLIN }, 1, -1) <= 0)
+ err(1, "waiting for KWin EIS");
}
-err_keystate:
- wl_proxy_destroy(keystate);
-err_registry:
- wl_registry_destroy(registry);
-err_display:
- wl_display_disconnect(display);
-err:
- return interrupted ? (128 + interrupted) : (ret ? 0 : 1);
+ ei_ping_unref(ping);
+ ei_unref(ei);
+ dbus_connection_close(bus);
+ dbus_connection_unref(bus);
+ return 0;
}