diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7c201efc..6690c6f2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -110,6 +110,7 @@ set(FLUTTER_PI_SRC
 	src/collection.c
   src/cursor.c
   src/keyboard.c
+  src/user_input.c
 	src/plugins/services.c
 )
 
@@ -163,7 +164,7 @@ target_compile_options(flutter-pi PRIVATE
   ${LIBINPUT_CFLAGS}
   ${LIBUDEV_CFLAGS}
   ${LIBXKBCOMMON_CFLAGS}
-  $<$<CONFIG:DEBUG>:-O0 -ggdb>
+  $<$<CONFIG:DEBUG>:-O0 -ggdb -DDEBUG>
 )
 
 if (BUILD_TEXT_INPUT_PLUGIN)
diff --git a/include/collection.h b/include/collection.h
index 2242f157..8044c42d 100644
--- a/include/collection.h
+++ b/include/collection.h
@@ -5,6 +5,8 @@
 #include <stdlib.h>
 #include <errno.h>
 #include <stdbool.h>
+#include <stdint.h>
+#include <assert.h>
 
 #include <pthread.h>
 
@@ -404,4 +406,25 @@ static inline void *memdup(const void *restrict src, const size_t n) {
 #define BMAP_CLEAR(p_bmap, i_bit) ((p_bmap)[(i_bit) / sizeof(*(p_bmap))] &= ~(1 << ((i_bit) & (sizeof(*(p_bmap)) - 1))))
 #define BMAP_ZERO(p_bmap, n_bits) (memset((p_bmap), 0, (((n_bits) - 1) / 8) + 1))
 
+#define min(a, b) ((a) < (b) ? (a) : (b))
+#define max(a, b) ((a) > (b) ? (a) : (b))
+
+/**
+ * @brief Get the current time of the system monotonic clock.
+ * @returns time in nanoseconds.
+ */
+static inline uint64_t get_monotonic_time(void) {
+	struct timespec time;
+	clock_gettime(CLOCK_MONOTONIC, &time);
+	return time.tv_nsec + time.tv_sec*1000000000ull;
+}
+
+#ifdef DEBUG
+#define DEBUG_ASSERT(__cond) assert(__cond)
+#define DEBUG_ASSERT_MSG(__cond, __msg) assert((__msg, (__cond))
+#else
+#define DEBUG_ASSERT(__cond) do {} while (false)
+#define DEBUG_ASSERT_MSG(__cond, __msg) do {} while (false)
+#endif
+
 #endif
\ No newline at end of file
diff --git a/include/flutter-pi.h b/include/flutter-pi.h
index 7820f23c..a44b8476 100644
--- a/include/flutter-pi.h
+++ b/include/flutter-pi.h
@@ -1,6 +1,8 @@
 #ifndef _FLUTTERPI_H
 #define _FLUTTERPI_H
 
+#define LOG_FLUTTERPI_ERROR(...) fprintf(stderr, "[flutter-pi] " __VA_ARGS__)
+
 #include <limits.h>
 #include <linux/input.h>
 #include <stdbool.h>
@@ -211,11 +213,17 @@ struct libudev {
 	 .skewY  = 0,                                 .scaleY = 1, .transY = 0, \
 	 .pers0  = -sin(((double) (deg))/180.0*M_PI), .pers1  = 0, .pers2  = cos(((double) (deg))/180.0*M_PI)})
 
+/**
+ * A flutter transformation that rotates any coords around the z-axis, counter-clockwise.
+ */
 #define FLUTTER_ROTZ_TRANSFORMATION(deg) ((FlutterTransformation) \
 	{.scaleX = cos(((double) (deg))/180.0*M_PI), .skewX  = -sin(((double) (deg))/180.0*M_PI), .transX = 0, \
 	 .skewY  = sin(((double) (deg))/180.0*M_PI), .scaleY = cos(((double) (deg))/180.0*M_PI),  .transY = 0, \
 	 .pers0  = 0,                                .pers1  = 0,                                 .pers2  = 1})
 
+/**
+ * A transformation that is the result of multiplying a with b.
+ */
 #define FLUTTER_MULTIPLIED_TRANSFORMATIONS(a, b) ((FlutterTransformation) \
 	{.scaleX = a.scaleX * b.scaleX + a.skewX  * b.skewY  + a.transX * b.pers0, \
 	 .skewX  = a.scaleX * b.skewX  + a.skewX  * b.scaleY + a.transX * b.pers1, \
@@ -227,12 +235,24 @@ struct libudev {
 	 .pers1  = a.pers0  * b.skewX  + a.pers1  * b.scaleY + a.pers2  * b.pers1, \
 	 .pers2  = a.pers0  * b.transX + a.pers1  * b.transY + a.pers2  * b.pers2})
 
+/**
+ * A transformation that is the result of adding a with b.
+ */
 #define FLUTTER_ADDED_TRANSFORMATIONS(a, b) ((FlutterTransformation) \
 	{.scaleX = a.scaleX + b.scaleX, .skewX  = a.skewX  + b.skewX,  .transX = a.transX + b.transX, \
 	 .skewY  = a.skewY  + b.skewY,  .scaleY = a.scaleY + b.scaleY, .transY = a.transY + b.transY, \
 	 .pers0  = a.pers0  + b.pers0,  .pers1  = a.pers1  + b.pers1,  .pers2  = a.pers2  + b.pers2 \
 	})
 
+/**
+ * A transformation that is the result of transponating a.
+ */
+#define FLUTTER_TRANSPONATED_TRANSFORMATION(a) ((FlutterTransformation) \
+	{.scaleX = a.scaleX, .skewX  = a.skewY,  .transX = a.pers0, \
+	 .skewY  = a.skewX,  .scaleY = a.scaleY, .transY = a.pers1, \
+	 .pers0  = a.transX, .pers1  = a.transY, .pers2  = a.pers2, \
+	})
+
 static inline void apply_flutter_transformation(
 	const FlutterTransformation t,
 	double *px,
@@ -392,21 +412,8 @@ struct flutterpi {
 	struct compositor *compositor;
 
 	/// IO
-	struct {
-		bool use_paths;
-		bool disable_text_input;
-
-		glob_t input_devices_glob;
-#		ifndef BUILD_WITHOUT_UDEV_SUPPORT
-		struct libudev libudev;
-#		endif
-		struct libinput *libinput;
-		sd_event_source *libinput_event_source;
-		struct keyboard_config *keyboard_config;
-
-		int64_t next_unused_flutter_device_id;
-		double cursor_x, cursor_y;
-	} input;
+	sd_event_source *user_input_event_source;
+	struct user_input *user_input;
 	
 	/// flutter stuff
 	struct {
@@ -453,14 +460,6 @@ struct platform_message {
 
 extern struct flutterpi flutterpi;
 
-struct input_device_data {
-	int64_t flutter_device_id_offset;
-	struct keyboard_state *keyboard_state;
-	double x, y;
-	int64_t buttons;
-	uint64_t timestamp;
-};
-
 int flutterpi_fill_view_properties(
 	bool has_orientation,
 	enum device_orientation orientation,
diff --git a/include/keyboard.h b/include/keyboard.h
index 16eb3a8a..c0761475 100644
--- a/include/keyboard.h
+++ b/include/keyboard.h
@@ -3,6 +3,8 @@
 
 #include <xkbcommon/xkbcommon.h>
 
+#define LOG_KEYBOARD_ERROR(...) fprintf(stderr, "[keyboard] " __VA_ARGS__)
+
 struct keyboard_config {
     struct xkb_context *context;
     struct xkb_keymap *default_keymap;
diff --git a/include/user_input.h b/include/user_input.h
new file mode 100644
index 00000000..2290fe46
--- /dev/null
+++ b/include/user_input.h
@@ -0,0 +1,141 @@
+#ifndef USER_INPUT_H_
+#define USER_INPUT_H_
+
+#include <xkbcommon/xkbcommon.h>
+#include <flutter_embedder.h>
+
+#define LOG_USER_INPUT_ERROR(...) fprintf(stderr, "[user input] " __VA_ARGS__)
+#define MAX_COLLECTED_FLUTTER_POINTER_EVENTS 64
+
+#define FLUTTER_POINTER_EVENT(_phase, _timestamp, _x, _y, _device, _signal_kind, _scroll_delta_x, _scroll_delta_y, _device_kind, _buttons) \
+    (FlutterPointerEvent) { \
+        .struct_size = sizeof(FlutterPointerEvent), \
+        .phase = (_phase), \
+        .timestamp = (_timestamp), \
+        .x = (_x), .y = (_y), \
+        .device = (_device), \
+        .signal_kind = (_signal_kind), \
+        .scroll_delta_x = (_scroll_delta_x), \
+        .scroll_delta_y = (_scroll_delta_y), \
+        .device_kind = (_device_kind), \
+        .buttons = (_buttons) \
+    }
+
+#define FLUTTER_POINTER_TOUCH_ADD_EVENT(_timestamp, _x, _y, _device_id) \
+    FLUTTER_POINTER_EVENT(kAdd, _timestamp, _x, _y, _device_id, kFlutterPointerSignalKindNone, 0.0, 0.0, kFlutterPointerDeviceKindTouch, 0)
+
+#define FLUTTER_POINTER_TOUCH_REMOVE_EVENT(_timestamp, _x, _y, _device_id) \
+    FLUTTER_POINTER_EVENT(kRemove, _timestamp, _x, _y, _device_id, kFlutterPointerSignalKindNone, 0.0, 0.0, kFlutterPointerDeviceKindTouch, 0)
+
+#define FLUTTER_POINTER_TOUCH_MOVE_EVENT(_timestamp, _x, _y, _device_id) \
+    FLUTTER_POINTER_EVENT(kMove, _timestamp, _x, _y, _device_id, kFlutterPointerSignalKindNone, 0.0, 0.0, kFlutterPointerDeviceKindTouch, 0)
+
+#define FLUTTER_POINTER_TOUCH_DOWN_EVENT(_timestamp, _x, _y, _device_id) \
+    FLUTTER_POINTER_EVENT(kDown, _timestamp, _x, _y, _device_id, kFlutterPointerSignalKindNone, 0.0, 0.0, kFlutterPointerDeviceKindTouch, 0)
+
+#define FLUTTER_POINTER_TOUCH_UP_EVENT(_timestamp, _x, _y, _device_id) \
+    FLUTTER_POINTER_EVENT(kUp, _timestamp, _x, _y, _device_id, kFlutterPointerSignalKindNone, 0.0, 0.0, kFlutterPointerDeviceKindTouch, 0)
+
+#define FLUTTER_POINTER_MOUSE_BUTTON_EVENT(_phase, _timestamp, _x, _y, _device_id, _buttons) \
+    FLUTTER_POINTER_EVENT(_phase, _timestamp, _x, _y, _device_id, kFlutterPointerSignalKindNone, 0.0, 0.0, kFlutterPointerDeviceKindMouse, _buttons)
+
+#define FLUTTER_POINTER_MOUSE_ADD_EVENT(_timestamp, _x, _y, _device_id, _buttons) \
+    FLUTTER_POINTER_EVENT(kAdd, _timestamp, _x, _y, _device_id, kFlutterPointerSignalKindNone, 0.0, 0.0, kFlutterPointerDeviceKindMouse, _buttons)
+
+#define FLUTTER_POINTER_MOUSE_REMOVE_EVENT(_timestamp, _x, _y, _device_id, _buttons) \
+    FLUTTER_POINTER_EVENT(kRemove, _timestamp, _x, _y, _device_id, kFlutterPointerSignalKindNone, 0.0, 0.0, kFlutterPointerDeviceKindMouse, _buttons)
+
+#define FLUTTER_POINTER_REMOVE_EVENT(_timestamp, _x, _y, _device, _buttons) \
+    FLUTTER_POINTER_EVENT(kRemove, _timestamp, _x, _y, _device_id, kFlutterPointerSignalKindNone, 0.0, 0.0, kFlutterPointerDeviceKindMouse, _buttons)
+
+#define FLUTTER_POINTER_MOUSE_MOVE_EVENT(_timestamp, _x, _y, _device_id, _buttons) \
+    FLUTTER_POINTER_EVENT( \
+        (_buttons) & kFlutterPointerButtonMousePrimary ? kMove : kHover, \
+        _timestamp, \
+        _x, _y, \
+        _device_id, \
+        kFlutterPointerSignalKindNone, \
+        0.0, 0.0, \
+        kFlutterPointerDeviceKindMouse, \
+        _buttons\
+    )
+
+typedef void (*flutter_pointer_event_callback_t)(void *userdata, const FlutterPointerEvent *events, size_t n_events);
+
+typedef void (*utf8_character_callback_t)(void *userdata, uint8_t *character);
+
+typedef void (*xkb_keysym_callback_t)(void *userdata, xkb_keysym_t keysym);
+
+typedef void (*gtk_keyevent_callback_t)(
+	void *userdata,
+	uint32_t unicode_scalar_values,
+    uint32_t key_code,
+    uint32_t scan_code,
+    uint32_t modifiers,
+    bool is_down
+);
+
+typedef void (*set_cursor_enabled_callback_t)(void *userdata, bool enabled);
+
+typedef void (*move_cursor_callback_t)(void *userdata, unsigned int x, unsigned int y);
+
+struct user_input_interface {
+    flutter_pointer_event_callback_t on_flutter_pointer_event;
+    utf8_character_callback_t on_utf8_character;
+    xkb_keysym_callback_t on_xkb_keysym;
+    gtk_keyevent_callback_t on_gtk_keyevent;
+    set_cursor_enabled_callback_t on_set_cursor_enabled;
+    move_cursor_callback_t on_move_cursor;
+};
+
+struct user_input;
+
+/**
+ * @brief Create a new user input instance. Will try to load the default keyboard config from /etc/default/keyboard
+ * and create a udev-backed libinput instance.
+ */
+struct user_input *user_input_new(
+    const struct user_input_interface *interface, 
+    void *userdata,
+    const FlutterTransformation *display_to_view_transform,
+    const FlutterTransformation *view_to_display_transform,
+	unsigned int display_width,
+	unsigned int display_height
+);
+
+/**
+ * @brief Destroy this user input instance and free all allocated memory. This will not remove any input devices
+ * added to flutter and won't invoke any callbacks in the user input interface at all.
+ */
+void user_input_destroy(struct user_input *input);
+
+/**
+ * @brief Set a 3x3 matrix and display width / height so user_input can transform any device coordinates into
+ * proper flutter view coordinates. (For example to account for a rotated display)
+ * Will also transform absolute & relative mouse movements.
+ * 
+ * @param display_to_view_transform will be copied internally.
+ */
+void user_input_set_transform(
+    struct user_input *input,
+    const FlutterTransformation *display_to_view_transform,
+    const FlutterTransformation *view_to_display_transform,
+    unsigned int display_width,
+    unsigned int display_height
+);
+
+/**
+ * @brief Returns a filedescriptor used for input event notification. The returned
+ * filedescriptor should be listened to with EPOLLIN | EPOLLRDHUP | EPOLLPRI or equivalent.
+ * When the fd becomes ready, @ref user_input_on_fd_ready should be called not long after it
+ * became ready. (libinput somehow relies on that)
+ */
+int user_input_get_fd(struct user_input *input);
+
+/**
+ * @brief Should be called when the fd returned by @ref user_input_get_fd becomes ready.
+ * The user_input_interface callbacks will be called inside this function.
+ */
+int user_input_on_fd_ready(struct user_input *input);
+
+#endif
\ No newline at end of file
diff --git a/src/flutter-pi.c b/src/flutter-pi.c
index eb5f7840..670c7cdf 100644
--- a/src/flutter-pi.c
+++ b/src/flutter-pi.c
@@ -45,6 +45,7 @@
 #include <flutter-pi.h>
 #include <compositor.h>
 #include <keyboard.h>
+#include <user_input.h>
 #include <platformchannel.h>
 #include <pluginregistry.h>
 #include <texture_registry.h>
@@ -1178,6 +1179,17 @@ int flutterpi_fill_view_properties(
 		flutterpi.view.display_to_view_transform.transX = flutterpi.display.height;
 	}
 
+	if (flutterpi.user_input != NULL) {
+		// update the user input with the new transforms
+		user_input_set_transform(
+			flutterpi.user_input,
+			&flutterpi.view.display_to_view_transform,
+			&flutterpi.view.view_to_display_transform,
+			flutterpi.display.width,
+			flutterpi.display.height
+		);
+	}
+
 	return 0;
 }
 
@@ -1879,6 +1891,7 @@ int flutterpi_schedule_exit(void) {
 /**************
  * USER INPUT *
  **************/
+/*
 static int libinput_interface_on_open(const char *path, int flags, void *userdata) {
 	return open(path, flags | O_CLOEXEC);
 }
@@ -2436,65 +2449,167 @@ static struct libinput *try_create_path_backed_libinput(void) {
 
 	return libinput;
 }
+*/
 
-static int init_user_input(void) {
-	sd_event_source *libinput_event_source;
-	struct keyboard_config *kbdcfg;
-	struct libinput *libinput;
+/**************
+ * USER INPUT *
+ **************/
+static void on_flutter_pointer_event(void *userdata, const FlutterPointerEvent *events, size_t n_events) {
+	FlutterEngineResult engine_result;
+	struct flutterpi *flutterpi;
+
+	flutterpi = userdata;
+
+	engine_result = flutterpi->flutter.libflutter_engine.FlutterEngineSendPointerEvent(
+		flutterpi->flutter.engine,
+		events,
+		n_events
+	);
+
+	if (engine_result != kSuccess) {
+		LOG_FLUTTERPI_ERROR("Error sending touchscreen / mouse events to flutter. FlutterEngineSendPointerEvent: %s\n", FLUTTER_RESULT_TO_STRING(engine_result));
+		//flutterpi_schedule_exit(flutterpi);
+	}
+}
+
+static void on_utf8_character(void *userdata, uint8_t *character) {
+	FlutterEngineResult engine_result;
+	struct flutterpi *flutterpi;
 	int ok;
 
-	libinput_event_source = NULL;
-	kbdcfg = NULL;
-	libinput = NULL;
+	flutterpi = userdata;
 
-	if (flutterpi.input.use_paths == false) {
-		libinput = try_create_udev_backed_libinput();
+#ifdef BUILD_TEXT_INPUT_PLUGIN
+	ok = textin_on_utf8_char(character);
+	if (ok != 0) {
+		LOG_FLUTTERPI_ERROR("Error handling keyboard event. textin_on_utf8_char: %s\n", strerror(ok));
+		//flutterpi_schedule_exit(flutterpi);
 	}
+#endif
+}
 
-	if (libinput == NULL) {
-		libinput = try_create_path_backed_libinput();
+static void on_xkb_keysym(void *userdata, xkb_keysym_t keysym) {
+	FlutterEngineResult engine_result;
+	struct flutterpi *flutterpi;
+	int ok;
+
+	flutterpi = userdata;
+
+#ifdef BUILD_TEXT_INPUT_PLUGIN
+	ok = textin_on_xkb_keysym(keysym);
+	if (ok != 0) {
+		LOG_FLUTTERPI_ERROR("Error handling keyboard event. textin_on_xkb_keysym: %s\n", strerror(ok));
+		//flutterpi_schedule_exit(flutterpi);
 	}
+#endif
+}
+
+static void on_gtk_keyevent(
+	void *userdata,
+	uint32_t unicode_scalar_values,
+    uint32_t key_code,
+    uint32_t scan_code,
+    uint32_t modifiers,
+    bool is_down
+) {
+	FlutterEngineResult engine_result;
+	struct flutterpi *flutterpi;
+	int ok;
+
+	flutterpi = userdata;
+
+#ifdef BUILD_RAW_KEYBOARD_PLUGIN
+	ok = rawkb_send_gtk_keyevent(
+		unicode_scalar_values,
+		key_code,
+		scan_code,
+		modifiers,
+		is_down
+	);
+	if (ok != 0) {
+		LOG_FLUTTERPI_ERROR("Error handling keyboard event. rawkb_send_gtk_keyevent: %s\n", strerror(ok));
+		//flutterpi_schedule_exit(flutterpi);
+	}
+#endif
+}
+
+static void on_set_cursor_enabled(void *userdata, bool enabled) {
+	struct flutterpi *flutterpi;
+	int ok;
+
+	flutterpi = userdata;
+
+	ok = compositor_apply_cursor_state(
+		enabled,
+		flutterpi->view.rotation,
+		flutterpi->display.pixel_ratio
+	);
+	if (ok != 0) {
+		LOG_FLUTTERPI_ERROR("Error enabling / disabling mouse cursor. compositor_apply_cursor_state: %s\n", strerror(ok));
+	}
+}
+
+static void on_move_cursor(void *userdata, unsigned int x, unsigned int y) {
+	struct flutterpi *flutterpi;
+	int ok;
+
+	flutterpi = userdata;
+
+	ok = compositor_set_cursor_pos(x, y);
+	if (ok != 0) {
+		LOG_FLUTTERPI_ERROR("Error moving mouse cursor. compositor_set_cursor_pos: %s\n", strerror(ok));
+	}
+}
+
+const static struct user_input_interface user_input_interface = {
+    .on_flutter_pointer_event = on_flutter_pointer_event,
+    .on_utf8_character = on_utf8_character,
+    .on_xkb_keysym = on_xkb_keysym,
+    .on_gtk_keyevent = on_gtk_keyevent,
+    .on_set_cursor_enabled = on_set_cursor_enabled,
+    .on_move_cursor = on_move_cursor
+};
+
+static int on_user_input_fd_ready(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+	struct user_input *input = userdata;
+	return user_input_on_fd_ready(input);
+}
+
+static int init_user_input(void) {
+	struct user_input *input;
+	sd_event_source *event_source;
+	int ok;
 	
-	if (libinput != NULL) {
+	event_source = NULL;
+	
+	input = user_input_new(
+		&user_input_interface,
+		&flutterpi,
+		&flutterpi.view.display_to_view_transform,
+		&flutterpi.view.view_to_display_transform,
+		flutterpi.display.width,
+		flutterpi.display.height
+	);
+	if (input == NULL) {
+		LOG_FLUTTERPI_ERROR("Couldn't initialize user input. flutter-pi will run without user input.\n");
+	} else {
 		ok = sd_event_add_io(
 			flutterpi.event_loop,
-			&libinput_event_source,
-			libinput_get_fd(libinput),
+			&event_source,
+			user_input_get_fd(input),
 			EPOLLIN | EPOLLRDHUP | EPOLLPRI,
-			on_libinput_ready,
-			NULL
+			on_user_input_fd_ready,
+			input
 		);
 		if (ok < 0) {
-			fprintf(stderr, "[flutter-pi] Could not add libinput callback to main loop. sd_event_add_io: %s\n", strerror(-ok));
-#			ifndef BUILD_WITHOUT_UDEV_SUPPORT
-				if (libinput_get_user_data(libinput) != NULL) {
-					struct udev *udev = libinput_get_user_data(libinput);
-					libinput_unref(libinput);
-					flutterpi.input.libudev.udev_unref(udev);
-				} else {
-					libinput_unref(libinput);
-				}
-#			else
-				libinput_unref(libinput);
-#			endif
-			return -ok;
+			LOG_FLUTTERPI_ERROR("Couldn't listen for user input. flutter-pi will run without user input. sd_event_add_io: %s\n", strerror(-ok));
+			user_input_destroy(input);
+			input = NULL;
 		}
-
-#ifdef BUILD_TEXT_INPUT_PLUGIN
-		if (flutterpi.input.disable_text_input == false) {
-			kbdcfg = keyboard_config_new();
-			if (kbdcfg == NULL) {
-				fprintf(stderr, "[flutter-pi] Could not initialize keyboard configuration. Flutter-pi will run without text/raw keyboard input.\n");
-			}
-		}
-#endif
-	} else {
-		fprintf(stderr, "[flutter-pi] Could not initialize input. Flutter-pi will run without user input.\n");
 	}
-
-	flutterpi.input.libinput = libinput;
-	flutterpi.input.libinput_event_source = libinput_event_source;
-	flutterpi.input.keyboard_config = kbdcfg;
+	
+	flutterpi.user_input = input;
+	flutterpi.user_input_event_source = event_source;
 
 	return 0;
 }
@@ -2540,12 +2655,10 @@ static bool setup_paths(void) {
 }
 
 static bool parse_cmd_args(int argc, char **argv) {
-	glob_t input_devices_glob = {0};
 	bool input_specified = false;
 	int opt;
 	int longopt_index = 0;
 	int runtime_mode_int = kDebug;
-	int disable_text_input_int = false;
 	int ok;
 
 	struct option long_options[] = {
@@ -2553,7 +2666,6 @@ static bool parse_cmd_args(int argc, char **argv) {
 		{"input", required_argument, NULL, 'i'},
 		{"orientation", required_argument, NULL, 'o'},
 		{"rotation", required_argument, NULL, 'r'},
-		{"no-text-input", no_argument, &disable_text_input_int, true},
 		{"dimensions", required_argument, NULL, 'd'},
 		{"help", no_argument, 0, 'h'},
 		{0, 0, 0, 0}
@@ -2568,10 +2680,6 @@ static bool parse_cmd_args(int argc, char **argv) {
 			case 0:
 				// flag was encountered. just continue
 				break;
-			case 'i':
-				glob(optarg, GLOB_BRACE | GLOB_TILDE | (input_specified ? GLOB_APPEND : 0), NULL, &input_devices_glob);
-				input_specified = true;
-				break;
 
 			case 'o':
 				if (STREQ(optarg, "portrait_up")) {
@@ -2648,10 +2756,6 @@ static bool parse_cmd_args(int argc, char **argv) {
 		}
 	}
 	
-	if (!input_specified) {
-		// user specified no input devices. use "/dev/input/event*"".
-		glob("/dev/input/event*", GLOB_BRACE | GLOB_TILDE, NULL, &input_devices_glob);
-	}
 
 	if (optind >= argc) {
 		fprintf(stderr, "error: expected asset bundle path after options.\n");
@@ -2659,11 +2763,8 @@ static bool parse_cmd_args(int argc, char **argv) {
 		return false;
 	}
 
-	flutterpi.input.use_paths = input_specified;
 	flutterpi.flutter.asset_bundle_path = strdup(argv[optind]);
 	flutterpi.flutter.runtime_mode = runtime_mode_int;
-	flutterpi.input.disable_text_input = disable_text_input_int;
-	flutterpi.input.input_devices_glob = input_devices_glob;
 
 	argv[optind] = argv[0];
 	flutterpi.flutter.engine_argc = argc - optind;
diff --git a/src/keyboard.c b/src/keyboard.c
index a7b86234..c405bbfb 100644
--- a/src/keyboard.c
+++ b/src/keyboard.c
@@ -73,11 +73,9 @@ static char *get_value_allocated(const char *varname, const char *buffer) {
 
     ok = find_var_offset_in_string(varname, buffer, &match);
     if (ok != 0) {
-        fprintf(stderr, "Error finding variable in buffer: find_var_offset_in_string: %s\n", strerror(ok));
         errno = ok;
         return NULL;
     } else if ((match.rm_so == -1) || (match.rm_eo == -1)) {
-        fprintf(stderr, "Could not find variable in buffer.\n");
         errno = EINVAL;
         return NULL;
     }
@@ -145,18 +143,40 @@ static char *load_file(const char *path) {
 }
 
 static struct xkb_keymap *load_default_keymap(struct xkb_context *context) {
-    char *file = load_file("/etc/default/keyboard");
+    struct xkb_keymap *keymap;
+    char *file, *xkbmodel, *xkblayout, *xkbvariant, *xkboptions; 
+
+    file = load_file("/etc/default/keyboard");
     if (file == NULL) {
-        perror("[keyboard] Could not load default keyboard configuration from \"/etc/default/keyboard\". ");
-        return NULL;
-    }
+        LOG_KEYBOARD_ERROR("Could not load keyboard configuration from \"/etc/default/keyboard\". Default keyboard config will be used. load_file: %s\n", strerror(errno));
+        xkbmodel = NULL;
+        xkblayout = NULL;
+        xkbvariant = NULL;
+        xkboptions = NULL;
+    } else {
+        // we have a config file, load its properties
+        xkbmodel = get_value_allocated("XKBMODEL", file);
+        if (xkbmodel == NULL) {
+            LOG_KEYBOARD_ERROR("Could not find \"XKBMODEL\" property inside \"/etc/default/keyboard\". Default value will be used.");
+        }
+
+        xkblayout = get_value_allocated("XKBLAYOUT", file);
+        if (xkblayout == NULL) {
+            LOG_KEYBOARD_ERROR("Could not find \"XKBLAYOUT\" property inside \"/etc/default/keyboard\". Default value will be used.");
+        }
 
-    char *xkbmodel = get_value_allocated("XKBMODEL", file);
-    char *xkblayout = get_value_allocated("XKBLAYOUT", file);
-    char *xkbvariant = get_value_allocated("XKBVARIANT", file);
-    char *xkboptions = get_value_allocated("XKBOPTIONS", file);
+        xkbvariant = get_value_allocated("XKBVARIANT", file);
+        if (xkbvariant == NULL) {
+            LOG_KEYBOARD_ERROR("Could not find \"XKBVARIANT\" property inside \"/etc/default/keyboard\". Default value will be used.");
+        }
+
+        xkboptions = get_value_allocated("XKBOPTIONS", file);
+        if (xkboptions == NULL) {
+            LOG_KEYBOARD_ERROR("Could not find \"XKBOPTIONS\" property inside \"/etc/default/keyboard\". Default value will be used.");
+        }
 
-    free(file);
+        free(file);
+    }
 
     struct xkb_rule_names names = {
         .rules = NULL,
@@ -166,7 +186,7 @@ static struct xkb_keymap *load_default_keymap(struct xkb_context *context) {
         .options = xkboptions
     };
 
-    struct xkb_keymap *keymap = xkb_keymap_new_from_names(context, &names, XKB_KEYMAP_COMPILE_NO_FLAGS);
+    keymap = xkb_keymap_new_from_names(context, &names, XKB_KEYMAP_COMPILE_NO_FLAGS);
 
     if (xkbmodel != NULL) free(xkbmodel);
     if (xkblayout != NULL) free(xkblayout);
@@ -174,15 +194,23 @@ static struct xkb_keymap *load_default_keymap(struct xkb_context *context) {
     if (xkboptions != NULL) free(xkboptions);
 
     if (keymap == NULL) {
-        fprintf(stderr, "[keyboard] Could not load default keymap.\n");
+        LOG_KEYBOARD_ERROR("Could not create xkb keymap.");
     }
 
     return keymap;
 }
 
 static struct xkb_compose_table *load_default_compose_table(struct xkb_context *context) {
+    struct xkb_compose_table *tbl;
+
     setlocale(LC_ALL, "");
-    return xkb_compose_table_new_from_locale(context, setlocale(LC_CTYPE, NULL), XKB_COMPOSE_COMPILE_NO_FLAGS);
+    
+    tbl = xkb_compose_table_new_from_locale(context, setlocale(LC_CTYPE, NULL), XKB_COMPOSE_COMPILE_NO_FLAGS);
+    if (tbl == NULL) {
+        LOG_KEYBOARD_ERROR("Could not create compose table from locale.\n");
+    }
+
+    return tbl;
 }
 
 
@@ -202,6 +230,7 @@ struct keyboard_config *keyboard_config_new(void) {
 
     ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
     if (ctx == NULL) {
+        LOG_KEYBOARD_ERROR("Could not create XKB context.\n");
         goto fail_free_cfg;
     }
 
@@ -262,16 +291,19 @@ struct keyboard_state *keyboard_state_new(
 
     xkb_state = xkb_state_new(keymap_override != NULL ? keymap_override : config->default_keymap);
     if (xkb_state == NULL) {
+        LOG_KEYBOARD_ERROR("Could not create new XKB state.\n");
         goto fail_free_state;
     }
 
     plain_xkb_state = xkb_state_new(keymap_override != NULL ? keymap_override : config->default_keymap);
     if (plain_xkb_state == NULL) {
+        LOG_KEYBOARD_ERROR("Could not create new XKB state.\n");
         goto fail_free_xkb_state;
     }
 
     compose_state = xkb_compose_state_new(compose_table_override != NULL ? compose_table_override : config->default_compose_table, XKB_COMPOSE_STATE_NO_FLAGS);
     if (compose_state == NULL) {
+        LOG_KEYBOARD_ERROR("Could not create new XKB compose state.\n");
         goto fail_free_xkb_state;
     }
 
diff --git a/src/plugins/text_input.c b/src/plugins/text_input.c
index 0a855f34..9773bbc0 100644
--- a/src/plugins/text_input.c
+++ b/src/plugins/text_input.c
@@ -747,15 +747,6 @@ int client_show_autocorrection_prompt_rect(
 /**
  * Text Input Model functions.
  */
-
-static inline int min(int a, int b) {
-    return a < b? a : b;
-}
-
-static inline int max(int a, int b) {
-    return a > b? a : b;
-}
-
 static inline int selection_start(void) {
     return min(text_input.selection_base, text_input.selection_extent);
 }
diff --git a/src/user_input.c b/src/user_input.c
new file mode 100644
index 00000000..761a73ee
--- /dev/null
+++ b/src/user_input.c
@@ -0,0 +1,1048 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <memory.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <math.h>
+
+#include <linux/input-event-codes.h>
+#include <systemd/sd-event.h>
+#include <libinput.h>
+
+#include <flutter-pi.h>
+#include <collection.h>
+#include <keyboard.h>
+#include <user_input.h>
+
+struct input_device_data {
+	int64_t flutter_device_id_offset;
+	struct keyboard_state *keyboard_state;
+	double x, y;
+	int64_t buttons;
+	uint64_t timestamp;
+
+    /**
+     * @brief Whether libinput has ever emitted any pointer / mouse events
+     * for this device.
+     * 
+     * Only applies to devices which have LIBINPUT_DEVICE_CAP_POINTER.
+     */
+    bool has_emitted_pointer_events;
+};
+
+struct user_input {
+    struct user_input_interface interface;
+    void *userdata;
+
+	struct libinput *libinput;
+	struct keyboard_config *kbdcfg;
+	int64_t next_unused_flutter_device_id;
+
+    /// TODO: Maybe fetch the transform, display dimensions, cursor pos dynamically using a callback instead?
+
+	/**
+     * @brief transforms normalized display coordinates (0 .. display_width-1, 0 .. display_height-1) to the coordinates
+     * used in the flutter pointer events
+     */
+    FlutterTransformation display_to_view_transform;
+    FlutterTransformation view_to_display_transform_nontranslating;
+	unsigned int display_width;
+	unsigned int display_height;
+	
+    
+    /**
+     * @brief The number of devices connected that want a mouse cursor.
+     * libinput calls them pointer devices, flutter calls them mice.
+     */
+    unsigned int n_cursor_devices;
+    /**
+     * @brief The flutter device id of the mouse cursor, if @ref n_cursor_devices > 0.
+     */
+    int64_t cursor_flutter_device_id;
+    /**
+     * @brief The current mouse cursor position in floating point display coordinates (0 .. display_width-1, 0 .. display_height-1)
+     */
+    double cursor_x, cursor_y;
+
+    /**
+     * @brief Buffer of collected flutter pointer events, since we can send multiple events at once to flutter.
+     */
+    FlutterPointerEvent collected_flutter_pointer_events[MAX_COLLECTED_FLUTTER_POINTER_EVENTS];
+    /**
+     * @brief Number of pointer events currently contained in @ref collected_flutter_pointer_events.
+     */
+    size_t n_collected_flutter_pointer_events;
+};
+
+// libinput interface
+static int open_restricted(const char *path, int flags, void *userdata) {
+	(void) userdata;
+	return open(path, flags | O_CLOEXEC);
+}
+
+static void close_restricted(int fd, void *userdata) {
+	(void) userdata;
+	close(fd);
+}
+
+static const struct libinput_interface libinput_interface = {
+	.open_restricted = open_restricted,
+	.close_restricted = close_restricted
+};
+
+struct user_input *user_input_new(
+    const struct user_input_interface *interface, 
+    void *userdata,
+    const FlutterTransformation *display_to_view_transform,
+    const FlutterTransformation *view_to_display_transform,
+	unsigned int display_width,
+	unsigned int display_height
+) {
+	struct keyboard_config *kbdcfg;
+	struct user_input *input;
+	sd_event_source *libinput_event_source;
+	struct libinput *libinput;
+	struct udev *udev;
+	int ok;
+
+	input = malloc(sizeof *input);
+	if (input == NULL) {
+		goto fail_return_null;
+	}
+
+	udev = udev_new();
+	if (udev == NULL) {
+		perror("[flutter-pi] Could not create udev instance. udev_new");
+		goto fail_free_input;
+	}
+
+	libinput = libinput_udev_create_context(
+		&libinput_interface,
+		input,
+		udev
+	);
+	if (libinput == NULL) {
+		perror("[flutter-pi] Could not create libinput instance. libinput_udev_create_context");
+		goto fail_unref_udev;
+	}
+
+	udev_unref(udev);
+
+	ok = libinput_udev_assign_seat(libinput, "seat0");
+	if (ok < 0) {
+		fprintf(stderr, "[flutter-pi] Could not assign udev seat to libinput instance. libinput_udev_assign_seat: %s\n", strerror(-ok));
+		goto fail_unref_libinput;
+	}
+
+#ifdef BUILD_TEXT_INPUT_PLUGIN
+	kbdcfg = keyboard_config_new();
+	if (kbdcfg == NULL) {
+		fprintf(stderr, "[flutter-pi] Could not initialize keyboard configuration. Flutter-pi will run without text/raw keyboard input.\n");
+	}
+#else
+    kbdcfg = NULL;
+#endif
+
+	input->interface = *interface;
+    input->userdata = userdata;
+
+	input->libinput = libinput;
+	input->kbdcfg = kbdcfg;
+	input->next_unused_flutter_device_id = 0;
+    
+    user_input_set_transform(
+        input,
+        display_to_view_transform,
+        view_to_display_transform,
+        display_width,
+        display_height
+    );
+
+    input->n_cursor_devices = 0;
+    input->cursor_flutter_device_id = -1;
+    input->cursor_x = 0.0;
+    input->cursor_y = 0.0;
+
+    input->n_collected_flutter_pointer_events = 0;
+
+	return input;
+
+
+	fail_unref_libinput:
+	libinput_unref(libinput);
+	goto fail_free_input;
+
+	fail_unref_udev:
+	udev_unref(udev);
+
+	fail_free_input:
+	free(input);
+
+	fail_return_null:
+	return NULL;
+}
+
+void user_input_destroy(struct user_input *input) {
+    DEBUG_ASSERT(input != NULL);
+
+    /// TODO: Destroy all the input device data, maybe add an additional
+    /// parameter to indicate whether any flutter device removal events should be
+    /// emitted.
+
+    if (input->kbdcfg != NULL) {
+        keyboard_config_destroy(input->kbdcfg);
+    }
+    libinput_unref(input->libinput);
+	free(input);
+}
+
+void user_input_set_transform(
+    struct user_input *input,
+    const FlutterTransformation *display_to_view_transform,
+    const FlutterTransformation *view_to_display_transform,
+    unsigned int display_width,
+    unsigned int display_height
+) {
+    DEBUG_ASSERT(input != NULL);
+    DEBUG_ASSERT(display_to_view_transform != NULL);
+    DEBUG_ASSERT(view_to_display_transform != NULL);
+
+    input->display_to_view_transform = *display_to_view_transform;
+    input->view_to_display_transform_nontranslating = *view_to_display_transform;
+    input->view_to_display_transform_nontranslating.transX = 0.0;
+    input->view_to_display_transform_nontranslating.transY = 0.0;
+    input->display_width = display_width;
+    input->display_height = display_height;
+}
+
+int user_input_get_fd(struct user_input *input) {
+    DEBUG_ASSERT(input != NULL);
+    return libinput_get_fd(input->libinput);
+}
+
+
+static void flush_pointer_events(struct user_input *input) {
+    DEBUG_ASSERT(input != NULL);
+
+    if (input->n_collected_flutter_pointer_events > 0) {
+        input->interface.on_flutter_pointer_event(
+            input->userdata,
+            input->collected_flutter_pointer_events,
+            input->n_collected_flutter_pointer_events
+        );
+    
+        input->n_collected_flutter_pointer_events = 0;
+    }
+}
+
+static void emit_pointer_events(struct user_input *input, const FlutterPointerEvent *events, size_t n_events) {
+    DEBUG_ASSERT(input != NULL);
+    DEBUG_ASSERT(events != NULL);
+
+    size_t to_copy;
+
+    while (n_events > 0) {
+        // if the internal buffer is full, flush it
+        if (input->n_collected_flutter_pointer_events == MAX_COLLECTED_FLUTTER_POINTER_EVENTS) {
+            flush_pointer_events(input);
+        }
+
+        // how many pointer events we can copy into the internal pointer event buffer?
+        to_copy = min(n_events, MAX_COLLECTED_FLUTTER_POINTER_EVENTS - input->n_collected_flutter_pointer_events);
+
+        // copy into the internal pointer event buffer
+        memcpy(
+            input->collected_flutter_pointer_events + input->n_collected_flutter_pointer_events,
+            events,
+            to_copy * sizeof(FlutterPointerEvent)
+        );
+
+        // advance n_events so it's now the number of remaining unemitted events
+        n_events -= to_copy;
+
+        // advance events so it points to the first remaining unemitted event
+        events += to_copy;
+        
+        // advance the number of stored pointer events
+        input->n_collected_flutter_pointer_events += to_copy;
+    }
+}
+
+/**
+ * @brief Called when input->n_cursor_devices was increased to maybe enable the mouse cursor
+ * it it isn't yet enabled.
+ */
+static void maybe_enable_mouse_cursor(struct user_input *input, uint64_t timestamp) {
+    DEBUG_ASSERT(input != NULL);
+
+    if (input->n_cursor_devices == 1) {
+        input->cursor_flutter_device_id = input->next_unused_flutter_device_id++;
+
+        emit_pointer_events(
+            input,
+            &FLUTTER_POINTER_MOUSE_ADD_EVENT(
+                timestamp,
+                input->cursor_x, input->cursor_y,
+                input->cursor_flutter_device_id,
+                0
+            ),
+            1
+        );
+    }
+}
+
+/**
+ * @brief Called when input->n_cursor_devices was decreased to maybe disable the mouse cursor.
+ */
+static void maybe_disable_mouse_cursor(struct user_input *input, uint64_t timestamp) {
+    DEBUG_ASSERT(input != NULL);
+
+    if (input->n_cursor_devices == 0) {
+        emit_pointer_events(
+            input,
+            &FLUTTER_POINTER_MOUSE_REMOVE_EVENT(
+                timestamp,
+                input->cursor_x, input->cursor_y,
+                input->cursor_flutter_device_id,
+                0
+            ),
+            1
+        );
+    }
+}
+
+
+static int on_device_added(struct user_input *input, struct libinput_event *event, uint64_t timestamp) {
+    struct input_device_data *data;
+    struct libinput_device *device;
+    int64_t device_id;
+    int ok;
+
+    DEBUG_ASSERT(input != NULL);
+    DEBUG_ASSERT(event != NULL);
+    
+    device = libinput_event_get_device(event);
+
+    data = malloc(sizeof *data);
+    if (data == NULL) {
+        return ENOMEM;
+    }
+
+    data->flutter_device_id_offset = input->next_unused_flutter_device_id;
+    data->keyboard_state = NULL;
+    data->x = 0.0;
+    data->y = 0.0;
+    data->buttons = 0;
+    data->timestamp = timestamp;
+    data->has_emitted_pointer_events = false;
+
+    libinput_device_set_user_data(device, data);
+
+    if (libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_POINTER)) {
+        // no special things to do here
+        // mouse pointer will be added as soon as the device actually sends a
+        // mouse event, as some devices will erroneously have a LIBINPUT_DEVICE_CAP_POINTER
+        // even though they aren't mice. (My keyboard for example is a mouse smh)
+    } else if (libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_TOUCH)) {
+        // add all touch slots as individual touch devices to flutter
+        for (int i = 0; i < libinput_device_touch_get_touch_count(device); i++) {
+            device_id = input->next_unused_flutter_device_id++;
+
+            emit_pointer_events(
+                input,
+                &FLUTTER_POINTER_TOUCH_ADD_EVENT(
+                    timestamp,
+                    0.0, 0.0,
+                    device_id
+                ),
+                1
+            );
+        }
+    } else if (libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_KEYBOARD)) {
+        // create a new keyboard state for this keyboard
+        if (input->kbdcfg) {
+            data->keyboard_state = keyboard_state_new(input->kbdcfg, NULL, NULL);
+        } else {
+            // If we don't have a keyboard config
+            data->keyboard_state = NULL;
+        }
+    } else {
+        // We don't handle this device, so we don't need the data.
+        libinput_device_set_user_data(device, NULL);
+        free(data);
+    }
+    
+    return 0;
+}
+
+static int on_device_removed(struct user_input *input, struct libinput_event *event, uint64_t timestamp) {
+    struct input_device_data *data;
+    struct libinput_device *device;
+
+    DEBUG_ASSERT(input != NULL);
+    DEBUG_ASSERT(event != NULL);
+    
+    device = libinput_event_get_device(event);
+    data = libinput_device_get_user_data(device);
+
+    if (libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_POINTER)) {
+        // if we don't have a mouse cursor added to flutter yet, add one
+        if (data->has_emitted_pointer_events) {
+            input->n_cursor_devices--;
+            maybe_disable_mouse_cursor(input, timestamp);
+        }
+    } else if (libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_TOUCH)) {
+        // add all touch slots as individual touch devices to flutter
+        for (int i = 0; i < libinput_device_touch_get_touch_count(device); i++) {
+            emit_pointer_events(
+                input,
+                &FLUTTER_POINTER_TOUCH_REMOVE_EVENT(
+                    timestamp,
+                    0.0, 0.0,
+                    data->flutter_device_id_offset + i
+                ),
+                1
+            );
+        }
+    } else if (libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_KEYBOARD)) {
+        // create a new keyboard state for this keyboard
+        if (data->keyboard_state != NULL) {
+            keyboard_state_destroy(data->keyboard_state);
+        }
+    }
+
+    if (data != NULL) {
+        free(data);
+    }
+    
+    libinput_device_set_user_data(device, NULL);
+
+    return 0;
+}
+
+static int on_key_event(struct user_input *input, struct libinput_event *event) {
+    struct libinput_event_keyboard *key_event;
+    struct input_device_data *data;
+    struct libinput_device *device;
+    struct keyboard_modifier_state mods;
+    enum libinput_key_state key_state;
+    xkb_keysym_t keysym;
+    uint32_t codepoint, plain_codepoint;
+    uint16_t evdev_keycode;
+    int ok;
+
+    DEBUG_ASSERT(input != NULL);
+    DEBUG_ASSERT(event != NULL);
+
+    key_event = libinput_event_get_keyboard_event(event);
+    device = libinput_event_get_device(event);
+    data = libinput_device_get_user_data(device);
+
+    evdev_keycode = (uint16_t) libinput_event_keyboard_get_key(key_event);
+    key_state = libinput_event_keyboard_get_key_state(key_event);
+
+    // If we don't have a keyboard state (for example if we couldn't load /etc/default/keyboard)
+    // we just return here.
+    if (data->keyboard_state == NULL) {
+        return 0;
+    }
+
+    // Let the keyboard advance its statemachine.
+    // keysym/codepoint are 0 when none were emitted.
+    ok = keyboard_state_process_key_event(
+        data->keyboard_state,
+        evdev_keycode,
+        (int32_t) key_state,
+        &keysym,
+        &codepoint
+    );
+    if (ok != 0) {
+        return ok;
+    }
+
+    // GTK keyevent needs the plain codepoint for some reason.
+    /// TODO: Maybe remove the evdev_value parameter?
+    plain_codepoint = keyboard_state_get_plain_codepoint(data->keyboard_state, evdev_keycode, 1);
+
+    // call the GTK keyevent callback.
+    /// TODO: Simplify the meta state construction.
+    input->interface.on_gtk_keyevent(
+        input->userdata,
+        plain_codepoint,
+        (uint32_t) keysym,
+        evdev_keycode + (uint32_t) 8,
+        keyboard_state_is_shift_active(data->keyboard_state)
+        | (keyboard_state_is_capslock_active(data->keyboard_state) << 1)
+        | (keyboard_state_is_ctrl_active(data->keyboard_state) << 2)
+        | (keyboard_state_is_alt_active(data->keyboard_state) << 3)
+        | (keyboard_state_is_numlock_active(data->keyboard_state) << 4)
+        | (keyboard_state_is_meta_active(data->keyboard_state) << 28),
+        key_state
+    );
+
+    // Call the UTF8 character callback if we've got a codepoint.
+    // Code very similiar to that of the linux kernel in drivers/tty/vt/keyboard.c, to_utf8
+    if (codepoint) {
+        if (codepoint < 0x80) {
+            // we emit UTF8 unconditionally here,
+            // maybe we should check if codepoint is a control character?
+            if (isprint(codepoint)) {
+                input->interface.on_utf8_character(
+                    input->userdata,
+                    (uint8_t[1]) {codepoint}
+                );
+            }
+        } else if (codepoint < 0x800) {
+            input->interface.on_utf8_character(
+                input->userdata,
+                (uint8_t[2]) {
+                    0xc0 | (codepoint >> 6),
+                    0x80 | (codepoint & 0x3f)
+                }
+            );
+        } else if (codepoint < 0x10000) {
+            // the console keyboard driver of the linux kernel checks
+            // at this point whether `codepoint` is a UTF16 high surrogate (U+D800 to U+DFFF)
+            // or U+FFFF and returns without emitting UTF8 in that case.
+            // don't know whether we should do this here too
+            input->interface.on_utf8_character(
+                input->userdata,
+                (uint8_t[3]) {
+                    0xe0 | (codepoint >> 12),
+                    0x80 | ((codepoint >> 6) & 0x3f),
+                    0x80 | (codepoint & 0x3f)
+                }
+            );
+        } else if (codepoint < 0x110000) {
+            input->interface.on_utf8_character(
+                input->userdata,
+                (uint8_t[4]) {
+                    0xf0 | (codepoint >> 18),
+                    0x80 | ((codepoint >> 12) & 0x3f),
+                    0x80 | ((codepoint >> 6) & 0x3f),
+                    0x80 | (codepoint & 0x3f)
+                }
+            );
+        }
+    }
+    
+    // Call the XKB keysym callback if we've got a keysym.
+    if (keysym) {
+        input->interface.on_xkb_keysym(input->userdata, keysym);
+    }
+
+    return 0;
+}
+
+static int on_mouse_motion_event(struct user_input *input, struct libinput_event *event) {
+    struct libinput_event_pointer *pointer_event;
+    struct input_device_data *data;
+    struct libinput_device *device;
+    uint64_t timestamp;
+    double new_cursor_x, new_cursor_y;
+    double dx, dy;
+
+    DEBUG_ASSERT(input != NULL);
+    DEBUG_ASSERT(event != NULL);
+    
+    pointer_event = libinput_event_get_pointer_event(event);
+    device = libinput_event_get_device(event);
+    data = libinput_device_get_user_data(device);
+    timestamp = libinput_event_pointer_get_time_usec(pointer_event);
+
+    data->timestamp = timestamp;
+
+    dx = libinput_event_pointer_get_dx(pointer_event);
+    dy = libinput_event_pointer_get_dy(pointer_event);
+
+    // transform the deltas. when the display is rotated
+    // we want the mouse to move in different directions as well.
+    // the dx and dy is still in display (not view) coordinates though,
+    // we only changed the movement direction.
+    apply_flutter_transformation(input->view_to_display_transform_nontranslating, &dx, &dy);
+
+    new_cursor_x = input->cursor_x + dx;
+    new_cursor_y = input->cursor_y + dy;
+
+    // check if we're ran over the display boundaries.
+    if (new_cursor_x < 0.0) {
+        new_cursor_x = 0.0;
+    } else if (new_cursor_x > input->display_width - 1) {
+        new_cursor_x = input->display_width - 1;
+    }
+
+    if (new_cursor_y < 0.0) {
+        new_cursor_y = 0.0;
+    } else if (new_cursor_y > input->display_height - 1) {
+        new_cursor_y = input->display_height - 1;
+    }
+
+    input->cursor_x = new_cursor_x;
+    input->cursor_y = new_cursor_y;
+
+    // transform the cursor pos to view (flutter) coordinates.
+    apply_flutter_transformation(input->display_to_view_transform, &new_cursor_x, &new_cursor_y);
+
+    if (data->has_emitted_pointer_events == false) {
+        data->has_emitted_pointer_events = true;
+        input->n_cursor_devices++;
+        maybe_enable_mouse_cursor(input, timestamp);
+    }
+
+    // send the pointer event to flutter.
+    emit_pointer_events(
+        input,
+        &FLUTTER_POINTER_MOUSE_MOVE_EVENT(
+            timestamp,
+            new_cursor_x,
+            new_cursor_y,
+            data->flutter_device_id_offset,
+            data->buttons
+        ),
+        1
+    );
+
+    // we don't invoke the interfaces' mouse move callback here, since we
+    // can have multiple mouse motion events per process_libinput_events
+    // and we don't want to invoke the callback each time.
+    // instead, we call it in user_input_on_fd_ready if the cursors
+    // display coordinates changed.
+
+    return 0;
+}
+
+static int on_mouse_motion_absolute_event(struct user_input *input, struct libinput_event *event) {
+    struct libinput_event_pointer *pointer_event;
+    struct input_device_data *data;
+    struct libinput_device *device;
+    uint64_t timestamp;
+    double x, y;
+
+    DEBUG_ASSERT(input != NULL);
+    DEBUG_ASSERT(event != NULL);
+
+    pointer_event = libinput_event_get_pointer_event(event);
+    device = libinput_event_get_device(event);
+    data = libinput_device_get_user_data(device);
+    timestamp = libinput_event_pointer_get_time_usec(pointer_event);
+
+    // get the new mouse position in display coordinates
+    x = libinput_event_pointer_get_absolute_x_transformed(pointer_event, input->display_width - 1);
+    y = libinput_event_pointer_get_absolute_y_transformed(pointer_event, input->display_height - 1);
+
+    /// FIXME: Why do we store the coordinates here?
+    data->x = x;
+    data->y = y;
+    data->timestamp = timestamp;
+
+    // update the "global" cursor position
+    input->cursor_x = x;
+    input->cursor_y = y;
+
+    // transform x & y to view (flutter) coordinates
+    apply_flutter_transformation(input->display_to_view_transform, &x, &y);
+
+    if (data->has_emitted_pointer_events == false) {
+        data->has_emitted_pointer_events = true;
+        input->n_cursor_devices++;
+        maybe_enable_mouse_cursor(input, timestamp);
+    }
+
+    emit_pointer_events(
+        input,
+        &FLUTTER_POINTER_MOUSE_MOVE_EVENT(
+            timestamp,
+            x, y,
+            data->flutter_device_id_offset,
+            data->buttons
+        ),
+        1
+    );
+
+    return 0;
+}
+
+static int on_mouse_button_event(struct user_input *input, struct libinput_event *event) {
+    struct libinput_event_pointer *pointer_event;
+    enum libinput_button_state button_state;
+    struct input_device_data *data;
+    struct libinput_device *device;
+    FlutterPointerPhase pointer_phase;
+    uint64_t timestamp;
+    uint16_t evdev_code;
+    int64_t flutter_button;
+    int64_t new_flutter_button_state;
+    double x, y;
+
+    DEBUG_ASSERT(input != NULL);
+    DEBUG_ASSERT(event != NULL);
+
+    pointer_event = libinput_event_get_pointer_event(event);
+    device = libinput_event_get_device(event);
+    data = libinput_device_get_user_data(device);
+    timestamp = libinput_event_pointer_get_time_usec(pointer_event);
+    evdev_code = (uint16_t) libinput_event_pointer_get_button(pointer_event);
+    button_state = libinput_event_pointer_get_button_state(pointer_event);
+
+    if (data->has_emitted_pointer_events == false) {
+        data->has_emitted_pointer_events = true;
+        input->n_cursor_devices++;
+        maybe_enable_mouse_cursor(input, timestamp);
+    }
+
+    // find out the flutter mouse button for this event
+    if (evdev_code == BTN_LEFT) {
+        flutter_button = kFlutterPointerButtonMousePrimary;
+    } else if (evdev_code == BTN_RIGHT) {
+        flutter_button = kFlutterPointerButtonMouseSecondary;
+    } else if (evdev_code == BTN_MIDDLE) {
+        flutter_button = kFlutterPointerButtonMouseMiddle;
+    } else if (evdev_code == BTN_BACK) {
+        flutter_button = kFlutterPointerButtonMouseBack;
+    } else if (evdev_code == BTN_FORWARD) {
+        flutter_button = kFlutterPointerButtonMouseForward;
+    } else {
+        flutter_button = 0;
+    }
+
+    // advance our button state, which is just a bitmap
+    new_flutter_button_state = data->buttons;
+    if (button_state == LIBINPUT_BUTTON_STATE_RELEASED) {
+        // remove the released button from the button state
+        new_flutter_button_state &= ~flutter_button;
+    } else {
+        // add the pressed button to the button state.
+        // note that libinput doesn't emit key repeat events.
+        new_flutter_button_state |= flutter_button;
+    }
+
+    // if our button state changed,
+    // emit a pointer event
+    if (new_flutter_button_state != data->buttons) {
+        if (new_flutter_button_state == 0) {
+            // no buttons are pressed anymore.
+            pointer_phase = kUp;
+        } else if (data->buttons == 0) {
+            // previously, there were no buttons pressed.
+            // now, at least 1 is pressed.
+            pointer_phase = kDown;
+        } else {
+            // some button was pressed or released,
+            // but it 
+            pointer_phase = kMove;
+        }
+
+        x = input->cursor_x;
+        y = input->cursor_y;
+        
+        // since the stored coords are in display, not view coordinates,
+        // we need to transform them again
+        apply_flutter_transformation(input->display_to_view_transform, &x, &y);
+
+        emit_pointer_events(
+            input,
+            &FLUTTER_POINTER_MOUSE_BUTTON_EVENT(
+                pointer_phase,
+                timestamp,
+                x, y,
+                data->flutter_device_id_offset,
+                new_flutter_button_state
+            ),
+            1
+        );
+
+        // finally apply the new button state
+        data->buttons = new_flutter_button_state;
+    }
+
+    return 0;
+}
+
+static int on_mouse_axis_event(struct user_input *input, struct libinput_event *event) {
+    DEBUG_ASSERT(input != NULL);
+    DEBUG_ASSERT(event != NULL);
+    return 0;
+}
+
+static int on_touch_down(struct user_input *input, struct libinput_event *event) {
+    struct libinput_event_touch *touch_event;
+    struct input_device_data *data;
+    uint64_t timestamp;
+    int64_t device_id;
+    double x, y;
+    int slot;
+
+    DEBUG_ASSERT(input != NULL);
+    DEBUG_ASSERT(event != NULL);
+
+    data = libinput_device_get_user_data(libinput_event_get_device(event));
+    touch_event = libinput_event_get_touch_event(event);
+    timestamp = libinput_event_touch_get_time_usec(touch_event);
+
+    // get the multitouch slot for this event
+    // can return -1 when the device is a single touch device
+    slot = libinput_event_touch_get_slot(touch_event);
+    if (slot == -1) {
+        slot = 0;
+    }
+
+    device_id = data->flutter_device_id_offset + slot;
+
+    x = libinput_event_touch_get_x_transformed(touch_event, input->display_width - 1);
+    y = libinput_event_touch_get_y_transformed(touch_event, input->display_height - 1);
+
+    // transform the display coordinates to view (flutter) coordinates
+    apply_flutter_transformation(input->display_to_view_transform, &x, &y);
+
+    // emit the flutter pointer event
+    emit_pointer_events(input, &FLUTTER_POINTER_TOUCH_DOWN_EVENT(timestamp, x, y, device_id), 1);
+
+    // alter our device state
+    data->x = x;
+    data->y = y;
+    data->timestamp = timestamp;
+
+    return 0;
+}
+
+static int on_touch_up(struct user_input *input, struct libinput_event *event) {
+    struct libinput_event_touch *touch_event;
+    struct input_device_data *data;
+    uint64_t timestamp;
+    int64_t device_id;
+    int slot;
+
+    DEBUG_ASSERT(input != NULL);
+    DEBUG_ASSERT(event != NULL);
+
+    data = libinput_device_get_user_data(libinput_event_get_device(event));
+    touch_event = libinput_event_get_touch_event(event);
+    timestamp = libinput_event_touch_get_time_usec(touch_event);
+
+    // get the multitouch slot for this event
+    // can return -1 when the device is a single touch device
+    slot = libinput_event_touch_get_slot(touch_event);
+    if (slot == -1) {
+        slot = 0;
+    }
+
+    device_id = data->flutter_device_id_offset + slot;
+
+    emit_pointer_events(input, &FLUTTER_POINTER_TOUCH_UP_EVENT(timestamp, data->x, data->y, device_id), 1);
+
+    return 0;
+}
+
+static int on_touch_motion(struct user_input *input, struct libinput_event *event) {
+    struct libinput_event_touch *touch_event;
+    struct input_device_data *data;
+    uint64_t timestamp;
+    int64_t device_id;
+    double x, y;
+    int slot;
+
+    DEBUG_ASSERT(input != NULL);
+    DEBUG_ASSERT(event != NULL);
+
+    data = libinput_device_get_user_data(libinput_event_get_device(event));
+    touch_event = libinput_event_get_touch_event(event);
+    timestamp = libinput_event_touch_get_time_usec(touch_event);
+
+    // get the multitouch slot for this event
+    // can return -1 when the device is a single touch device
+    slot = libinput_event_touch_get_slot(touch_event);
+    if (slot == -1) {
+        slot = 0;
+    }
+
+    device_id = data->flutter_device_id_offset + slot;
+
+    x = libinput_event_touch_get_x_transformed(touch_event, input->display_width - 1);
+    y = libinput_event_touch_get_y_transformed(touch_event, input->display_height - 1);
+
+    // transform the display coordinates to view (flutter) coordinates
+    apply_flutter_transformation(input->display_to_view_transform, &x, &y);
+
+    // emit the flutter pointer event
+    emit_pointer_events(input, &FLUTTER_POINTER_TOUCH_MOVE_EVENT(timestamp, x, y, device_id), 1);
+
+    // alter our device state
+    data->x = x;
+    data->y = y;
+    data->timestamp = timestamp;
+
+    return 0;
+}
+
+static int on_touch_cancel(struct user_input *input, struct libinput_event *event) {
+    DEBUG_ASSERT(input != NULL);
+    DEBUG_ASSERT(event != NULL);
+    /// TODO: Implement touch cancel
+    return 0;
+}
+
+static int on_touch_frame(struct user_input *input, struct libinput_event *event) {
+    DEBUG_ASSERT(input != NULL);
+    DEBUG_ASSERT(event != NULL);
+    /// TODO: Implement touch frame
+    return 0;
+}
+
+
+static int process_libinput_events(struct user_input *input, uint64_t timestamp) {
+    enum libinput_event_type event_type;
+    struct libinput_event *event;
+    int ok;
+
+    DEBUG_ASSERT(input != NULL);
+
+    while (libinput_next_event_type(input->libinput) != LIBINPUT_EVENT_NONE) {
+        event = libinput_get_event(input->libinput);
+        event_type = libinput_event_get_type(event);
+
+        switch (event_type) {
+            case LIBINPUT_EVENT_DEVICE_ADDED:
+                ok = on_device_added(input, event, timestamp);
+                if (ok != 0) {
+                    goto fail_destroy_event;
+                }
+                break;
+            case LIBINPUT_EVENT_DEVICE_REMOVED:
+                ok = on_device_removed(input, event, timestamp);
+                if (ok != 0) {
+                    goto fail_destroy_event;
+                }
+                break;
+            case LIBINPUT_EVENT_KEYBOARD_KEY:
+                ok = on_key_event(input, event);
+                if (ok != 0) {
+                    goto fail_destroy_event;
+                }
+                break;
+            case LIBINPUT_EVENT_POINTER_MOTION:
+                ok = on_mouse_motion_event(input, event);
+                if (ok != 0) {
+                    goto fail_destroy_event;
+                }
+                break;
+	        case LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE:
+                ok = on_mouse_motion_absolute_event(input, event);
+                if (ok != 0) {
+                    goto fail_destroy_event;
+                }
+                break;
+	        case LIBINPUT_EVENT_POINTER_BUTTON:
+                ok = on_mouse_button_event(input, event);
+                if (ok != 0) {
+                    goto fail_destroy_event;
+                }
+                break;
+	        case LIBINPUT_EVENT_POINTER_AXIS:
+                ok = on_mouse_axis_event(input, event);
+                if (ok != 0) {
+                    goto fail_destroy_event;
+                }
+                break;
+            case LIBINPUT_EVENT_TOUCH_DOWN:
+                ok = on_touch_down(input, event);
+                if (ok != 0) {
+                    goto fail_destroy_event;
+                }
+                break;
+	        case LIBINPUT_EVENT_TOUCH_UP:
+                ok = on_touch_up(input, event);
+                if (ok != 0) {
+                    goto fail_destroy_event;
+                }
+                break;
+	        case LIBINPUT_EVENT_TOUCH_MOTION:
+                ok = on_touch_motion(input, event);
+                if (ok != 0) {
+                    goto fail_destroy_event;
+                }
+                break;
+	        case LIBINPUT_EVENT_TOUCH_CANCEL:
+                ok = on_touch_cancel(input, event);
+                if (ok != 0) {
+                    goto fail_destroy_event;
+                }
+                break;
+            case LIBINPUT_EVENT_TOUCH_FRAME:
+                ok = on_touch_frame(input, event);
+                if (ok != 0) {
+                    goto fail_destroy_event;
+                }
+                break;
+            default:
+                break;
+        }
+        
+        libinput_event_destroy(event);
+    }
+
+    return 0;
+
+
+    fail_destroy_event:
+    libinput_event_destroy(event);
+
+    fail_return_ok:
+    return ok;
+}
+
+int user_input_on_fd_ready(struct user_input *input) {
+    unsigned int cursor_x, cursor_y, cursor_x_before, cursor_y_before;
+    uint64_t timestamp; 
+    bool cursor_enabled, cursor_enabled_before;
+    int ok;
+
+    DEBUG_ASSERT(input != NULL);
+
+    // get a timestamp because some libinput events don't provide one
+    // needs to be in milliseconds, since that's what the other libinput events
+    // use and what flutter pointer events require
+    timestamp = get_monotonic_time() / 1000;
+
+    // tell libinput about new events
+    ok = libinput_dispatch(input->libinput);
+    if (ok < 0) {
+        LOG_USER_INPUT_ERROR("Could not notify libinput about new input events. libinput_dispatch: %s\n", strerror(-ok));
+        return -ok;
+    }
+
+    // record cursor state before handling events
+    cursor_enabled_before = input->n_cursor_devices > 0;
+    cursor_x_before = (unsigned int) round(input->cursor_x);
+    cursor_y_before = (unsigned int) round(input->cursor_y);
+
+    // handle all available libinput events
+    ok = process_libinput_events(input, timestamp);
+    if (ok != 0) {
+        LOG_USER_INPUT_ERROR("Could not process libinput events. process_libinput_events: %s\n", strerror(ok));
+        return ok;
+    }
+
+    // record cursor state after handling events
+    cursor_enabled = input->n_cursor_devices > 0;
+    cursor_x = (unsigned int) round(input->cursor_x);
+    cursor_y = (unsigned int) round(input->cursor_y);
+
+    // make sure we've dispatched all the flutter pointer events
+    flush_pointer_events(input);
+
+    // call the interface callback if the cursor has been enabled or disabled
+    if (cursor_enabled && !cursor_enabled_before) {
+        input->interface.on_set_cursor_enabled(input->userdata, true);
+    } else if (!cursor_enabled && cursor_enabled_before) {
+        input->interface.on_set_cursor_enabled(input->userdata, false);
+    }
+
+    // only move the pointer if the cursor is enabled now
+    if (cursor_enabled && ((cursor_x != cursor_x_before) || (cursor_y != cursor_y_before))) {
+        input->interface.on_move_cursor(input->userdata, cursor_x, cursor_y);
+    }
+
+    return 0;
+}