diff --git a/river/InputManager.zig b/river/InputManager.zig
index 1e4df90..4ce937a 100644
--- a/river/InputManager.zig
+++ b/river/InputManager.zig
@@ -173,48 +173,18 @@ fn handleNewConstraint(
     };
 }
 
-fn handleNewInputMethod(
-    _: *wl.Listener(*wlr.InputMethodV2),
-    input_method: *wlr.InputMethodV2,
-) void {
-    //const self = @fieldParentPtr(Self, "new_input_method", listener);
-    const seat = @as(*Seat, @ptrFromInt(input_method.seat.data));
-    const relay = &seat.relay;
+fn handleNewInputMethod(_: *wl.Listener(*wlr.InputMethodV2), input_method: *wlr.InputMethodV2) void {
+    const seat: *Seat = @ptrFromInt(input_method.seat.data);
 
-    // Only one input_method can be bound to a seat.
-    if (relay.input_method != null) {
-        log.debug("attempted to connect second input method to a seat", .{});
-        input_method.sendUnavailable();
-        return;
-    }
+    log.debug("new input method on seat {s}", .{seat.wlr_seat.name});
 
-    relay.input_method = input_method;
-
-    input_method.events.commit.add(&relay.input_method_commit);
-    input_method.events.grab_keyboard.add(&relay.grab_keyboard);
-    input_method.events.destroy.add(&relay.input_method_destroy);
-
-    log.debug("new input method on seat {s}", .{relay.seat.wlr_seat.name});
-
-    if (seat.focused.surface()) |surface| {
-        relay.focus(surface);
-    }
+    seat.relay.newInputMethod(input_method);
 }
 
-fn handleNewTextInput(
-    _: *wl.Listener(*wlr.TextInputV3),
-    wlr_text_input: *wlr.TextInputV3,
-) void {
-    //const self = @fieldParentPtr(Self, "new_text_input", listener);
-    const seat = @as(*Seat, @ptrFromInt(wlr_text_input.seat.data));
-    const relay = &seat.relay;
-
-    const text_input_node = util.gpa.create(std.TailQueue(TextInput).Node) catch {
+fn handleNewTextInput(_: *wl.Listener(*wlr.TextInputV3), wlr_text_input: *wlr.TextInputV3) void {
+    TextInput.create(wlr_text_input) catch {
+        log.err("out of memory", .{});
         wlr_text_input.resource.postNoMemory();
         return;
     };
-    text_input_node.data.init(relay, wlr_text_input);
-    relay.text_inputs.append(text_input_node);
-
-    log.debug("new text input on seat {s}", .{relay.seat.wlr_seat.name});
 }
diff --git a/river/InputRelay.zig b/river/InputRelay.zig
index 99d4f34..d70d447 100644
--- a/river/InputRelay.zig
+++ b/river/InputRelay.zig
@@ -15,7 +15,7 @@
 // You should have received a copy of the GNU General Public License
 // along with this program. If not, see .
 
-const Self = @This();
+const InputRelay = @This();
 
 const std = @import("std");
 const assert = std.debug.assert;
@@ -30,17 +30,18 @@ const Seat = @import("Seat.zig");
 
 const log = std.log.scoped(.input_relay);
 
-/// The Relay structure manages the communication between text_inputs
-/// and input_method on a given seat.
-seat: *Seat,
-
-/// List of all TextInput bound to the relay.
-/// Multiple wlr_text_input interfaces can be bound to a relay,
-/// but only one at a time can receive events.
-text_inputs: std.TailQueue(TextInput) = .{},
+/// List of all text input objects for the seat.
+/// Multiple text input objects may be created per seat, even multiple from the same client.
+/// However, only one text input per seat may be enabled at a time.
+text_inputs: wl.list.Head(TextInput, .link),
 
+/// The input method currently in use for this seat.
+/// Only one input method per seat may be used at a time and if one is
+/// already in use new input methods are ignored.
+/// If this is null, no text input enter events will be sent.
 input_method: ?*wlr.InputMethodV2 = null,
 /// The currently enabled text input for the currently focused surface.
+/// Always null if there is no input method.
 text_input: ?*TextInput = null,
 
 input_method_commit: wl.Listener(*wlr.InputMethodV2) =
@@ -53,19 +54,44 @@ input_method_destroy: wl.Listener(*wlr.InputMethodV2) =
 grab_keyboard_destroy: wl.Listener(*wlr.InputMethodV2.KeyboardGrab) =
     wl.Listener(*wlr.InputMethodV2.KeyboardGrab).init(handleInputMethodGrabKeyboardDestroy),
 
-pub fn init(self: *Self, seat: *Seat) void {
-    self.* = .{ .seat = seat };
+pub fn init(relay: *InputRelay) void {
+    relay.* = .{ .text_inputs = undefined };
+
+    relay.text_inputs.init();
+}
+
+pub fn newInputMethod(relay: *InputRelay, input_method: *wlr.InputMethodV2) void {
+    const seat = @fieldParentPtr(Seat, "relay", relay);
+
+    log.debug("new input method on seat {s}", .{seat.wlr_seat.name});
+
+    // Only one input_method can be bound to a seat.
+    if (relay.input_method != null) {
+        log.info("seat {s} already has an input method", .{seat.wlr_seat.name});
+        input_method.sendUnavailable();
+        return;
+    }
+
+    relay.input_method = input_method;
+
+    input_method.events.commit.add(&relay.input_method_commit);
+    input_method.events.grab_keyboard.add(&relay.grab_keyboard);
+    input_method.events.destroy.add(&relay.input_method_destroy);
+
+    if (seat.focused.surface()) |surface| {
+        relay.focus(surface);
+    }
 }
 
 fn handleInputMethodCommit(
     listener: *wl.Listener(*wlr.InputMethodV2),
     input_method: *wlr.InputMethodV2,
 ) void {
-    const self = @fieldParentPtr(Self, "input_method_commit", listener);
-    assert(input_method == self.input_method);
+    const relay = @fieldParentPtr(InputRelay, "input_method_commit", listener);
+    assert(input_method == relay.input_method);
 
     if (!input_method.client_active) return;
-    const text_input = self.text_input orelse return;
+    const text_input = relay.text_input orelse return;
 
     if (input_method.current.preedit.text) |preedit_text| {
         text_input.wlr_text_input.sendPreeditString(
@@ -95,56 +121,59 @@ fn handleInputMethodDestroy(
     listener: *wl.Listener(*wlr.InputMethodV2),
     input_method: *wlr.InputMethodV2,
 ) void {
-    const self = @fieldParentPtr(Self, "input_method_destroy", listener);
-    assert(input_method == self.input_method);
+    const relay = @fieldParentPtr(InputRelay, "input_method_destroy", listener);
+    assert(input_method == relay.input_method);
 
-    self.input_method_commit.link.remove();
-    self.grab_keyboard.link.remove();
-    self.input_method_destroy.link.remove();
+    relay.input_method_commit.link.remove();
+    relay.grab_keyboard.link.remove();
+    relay.input_method_destroy.link.remove();
 
-    self.input_method = null;
+    relay.input_method = null;
 
-    self.focus(null);
+    relay.focus(null);
+
+    assert(relay.text_input == null);
 }
 
 fn handleInputMethodGrabKeyboard(
     listener: *wl.Listener(*wlr.InputMethodV2.KeyboardGrab),
     keyboard_grab: *wlr.InputMethodV2.KeyboardGrab,
 ) void {
-    const self = @fieldParentPtr(Self, "grab_keyboard", listener);
+    const relay = @fieldParentPtr(InputRelay, "grab_keyboard", listener);
+    const seat = @fieldParentPtr(Seat, "relay", relay);
 
-    const active_keyboard = self.seat.wlr_seat.getKeyboard();
+    const active_keyboard = seat.wlr_seat.getKeyboard();
     keyboard_grab.setKeyboard(active_keyboard);
 
-    keyboard_grab.events.destroy.add(&self.grab_keyboard_destroy);
+    keyboard_grab.events.destroy.add(&relay.grab_keyboard_destroy);
 }
 
 fn handleInputMethodGrabKeyboardDestroy(
     listener: *wl.Listener(*wlr.InputMethodV2.KeyboardGrab),
     keyboard_grab: *wlr.InputMethodV2.KeyboardGrab,
 ) void {
-    const self = @fieldParentPtr(Self, "grab_keyboard_destroy", listener);
-    self.grab_keyboard_destroy.link.remove();
+    const relay = @fieldParentPtr(InputRelay, "grab_keyboard_destroy", listener);
+    relay.grab_keyboard_destroy.link.remove();
 
     if (keyboard_grab.keyboard) |keyboard| {
         keyboard_grab.input_method.seat.keyboardNotifyModifiers(&keyboard.modifiers);
     }
 }
 
-pub fn disableTextInput(self: *Self) void {
-    assert(self.text_input != null);
+pub fn disableTextInput(relay: *InputRelay) void {
+    assert(relay.text_input != null);
 
-    if (self.input_method) |input_method| {
+    if (relay.input_method) |input_method| {
         input_method.sendDeactivate();
         input_method.sendDone();
     }
 
-    self.text_input = null;
+    relay.text_input = null;
 }
 
-pub fn sendInputMethodState(self: *Self) void {
-    const input_method = self.input_method.?;
-    const wlr_text_input = self.text_input.?.wlr_text_input;
+pub fn sendInputMethodState(relay: *InputRelay) void {
+    const input_method = relay.input_method.?;
+    const wlr_text_input = relay.text_input.?.wlr_text_input;
 
     // TODO Send these events only if something changed.
     // On activation all events must be sent for all active features.
@@ -171,13 +200,11 @@ pub fn sendInputMethodState(self: *Self) void {
     input_method.sendDone();
 }
 
-pub fn focus(self: *Self, new_focus: ?*wlr.Surface) void {
+pub fn focus(relay: *InputRelay, new_focus: ?*wlr.Surface) void {
     // Send leave events
     {
-        var it = self.text_inputs.first;
-        while (it) |node| : (it = node.next) {
-            const text_input = &node.data;
-
+        var it = relay.text_inputs.iterator(.forward);
+        while (it.next()) |text_input| {
             if (text_input.wlr_text_input.focused_surface) |surface| {
                 // This function should not be called unless focus changes
                 assert(surface != new_focus);
@@ -187,19 +214,17 @@ pub fn focus(self: *Self, new_focus: ?*wlr.Surface) void {
     }
 
     // Clear currently enabled text input
-    if (self.text_input != null) {
-        self.disableTextInput();
+    if (relay.text_input != null) {
+        relay.disableTextInput();
     }
 
     // Send enter events if we have an input method.
     // No text input for the new surface should be enabled yet as the client
     // should wait until it receives an enter event.
     if (new_focus) |surface| {
-        if (self.input_method != null) {
-            var it = self.text_inputs.first;
-            while (it) |node| : (it = node.next) {
-                const text_input = &node.data;
-
+        if (relay.input_method != null) {
+            var it = relay.text_inputs.iterator(.forward);
+            while (it.next()) |text_input| {
                 if (text_input.wlr_text_input.resource.getClient() == surface.resource.getClient()) {
                     text_input.wlr_text_input.sendEnter(surface);
                 }
diff --git a/river/Seat.zig b/river/Seat.zig
index 6058dd1..dc28b2f 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -68,7 +68,9 @@ pub const FocusTarget = union(enum) {
 wlr_seat: *wlr.Seat,
 
 /// Multiple mice are handled by the same Cursor
-cursor: Cursor = undefined,
+cursor: Cursor,
+/// Input Method handling
+relay: InputRelay,
 
 /// ID of the current keymap mode
 mode_id: u32 = 0,
@@ -99,9 +101,6 @@ drag: enum {
     touch,
 } = .none,
 
-/// Relay for communication between text_input and input_method.
-relay: InputRelay = undefined,
-
 request_set_selection: wl.Listener(*wlr.Seat.event.RequestSetSelection) =
     wl.Listener(*wlr.Seat.event.RequestSetSelection).init(handleRequestSetSelection),
 request_start_drag: wl.Listener(*wlr.Seat.event.RequestStartDrag) =
@@ -119,12 +118,14 @@ pub fn init(self: *Self, name: [*:0]const u8) !void {
     self.* = .{
         // This will be automatically destroyed when the display is destroyed
         .wlr_seat = try wlr.Seat.create(server.wl_server, name),
+        .cursor = undefined,
+        .relay = undefined,
         .mapping_repeat_timer = mapping_repeat_timer,
     };
     self.wlr_seat.data = @intFromPtr(self);
 
     try self.cursor.init(self);
-    self.relay.init(self);
+    self.relay.init();
 
     self.wlr_seat.events.request_set_selection.add(&self.request_set_selection);
     self.wlr_seat.events.request_start_drag.add(&self.request_start_drag);
diff --git a/river/TextInput.zig b/river/TextInput.zig
index 3375500..56661b2 100644
--- a/river/TextInput.zig
+++ b/river/TextInput.zig
@@ -15,7 +15,7 @@
 // You should have received a copy of the GNU General Public License
 // along with this program. If not, see .
 
-const Self = @This();
+const TextInput = @This();
 
 const std = @import("std");
 const assert = std.debug.assert;
@@ -29,7 +29,8 @@ const Seat = @import("Seat.zig");
 
 const log = std.log.scoped(.text_input);
 
-relay: *InputRelay,
+link: wl.list.Link,
+
 wlr_text_input: *wlr.TextInputV3,
 
 enable: wl.Listener(*wlr.TextInputV3) =
@@ -41,68 +42,79 @@ disable: wl.Listener(*wlr.TextInputV3) =
 destroy: wl.Listener(*wlr.TextInputV3) =
     wl.Listener(*wlr.TextInputV3).init(handleDestroy),
 
-pub fn init(self: *Self, relay: *InputRelay, wlr_text_input: *wlr.TextInputV3) void {
-    self.* = .{
-        .relay = relay,
+pub fn create(wlr_text_input: *wlr.TextInputV3) !void {
+    const seat: *Seat = @ptrFromInt(wlr_text_input.seat.data);
+
+    const text_input = try util.gpa.create(TextInput);
+
+    log.debug("new text input on seat {s}", .{seat.wlr_seat.name});
+
+    text_input.* = .{
+        .link = undefined,
         .wlr_text_input = wlr_text_input,
     };
 
-    wlr_text_input.events.enable.add(&self.enable);
-    wlr_text_input.events.commit.add(&self.commit);
-    wlr_text_input.events.disable.add(&self.disable);
-    wlr_text_input.events.destroy.add(&self.destroy);
+    seat.relay.text_inputs.append(text_input);
+
+    wlr_text_input.events.enable.add(&text_input.enable);
+    wlr_text_input.events.commit.add(&text_input.commit);
+    wlr_text_input.events.disable.add(&text_input.disable);
+    wlr_text_input.events.destroy.add(&text_input.destroy);
 }
 
 fn handleEnable(listener: *wl.Listener(*wlr.TextInputV3), _: *wlr.TextInputV3) void {
-    const self = @fieldParentPtr(Self, "enable", listener);
+    const text_input = @fieldParentPtr(TextInput, "enable", listener);
+    const seat: *Seat = @ptrFromInt(text_input.wlr_text_input.seat.data);
 
-    if (self.relay.text_input != null) {
+    if (seat.relay.text_input != null) {
         log.err("client requested to enable more than one text input on a single seat, ignoring request", .{});
         return;
     }
 
-    self.relay.text_input = self;
+    seat.relay.text_input = text_input;
 
-    if (self.relay.input_method) |input_method| {
+    if (seat.relay.input_method) |input_method| {
         input_method.sendActivate();
-        self.relay.sendInputMethodState();
+        seat.relay.sendInputMethodState();
     }
 }
 
 fn handleCommit(listener: *wl.Listener(*wlr.TextInputV3), _: *wlr.TextInputV3) void {
-    const self = @fieldParentPtr(Self, "commit", listener);
+    const text_input = @fieldParentPtr(TextInput, "commit", listener);
+    const seat: *Seat = @ptrFromInt(text_input.wlr_text_input.seat.data);
 
-    if (self.relay.text_input != self) {
+    if (seat.relay.text_input != text_input) {
         log.err("inactive text input tried to commit an update, client bug?", .{});
         return;
     }
 
-    if (self.relay.input_method != null) {
-        self.relay.sendInputMethodState();
+    if (seat.relay.input_method != null) {
+        seat.relay.sendInputMethodState();
     }
 }
 
 fn handleDisable(listener: *wl.Listener(*wlr.TextInputV3), _: *wlr.TextInputV3) void {
-    const self = @fieldParentPtr(Self, "disable", listener);
+    const text_input = @fieldParentPtr(TextInput, "disable", listener);
+    const seat: *Seat = @ptrFromInt(text_input.wlr_text_input.seat.data);
 
-    if (self.relay.text_input == self) {
-        self.relay.disableTextInput();
+    if (seat.relay.text_input == text_input) {
+        seat.relay.disableTextInput();
     }
 }
 
 fn handleDestroy(listener: *wl.Listener(*wlr.TextInputV3), _: *wlr.TextInputV3) void {
-    const self = @fieldParentPtr(Self, "destroy", listener);
-    const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self);
+    const text_input = @fieldParentPtr(TextInput, "destroy", listener);
+    const seat: *Seat = @ptrFromInt(text_input.wlr_text_input.seat.data);
 
-    if (self.relay.text_input == self) {
-        self.relay.disableTextInput();
+    if (seat.relay.text_input == text_input) {
+        seat.relay.disableTextInput();
     }
 
-    self.enable.link.remove();
-    self.commit.link.remove();
-    self.disable.link.remove();
-    self.destroy.link.remove();
+    text_input.enable.link.remove();
+    text_input.commit.link.remove();
+    text_input.disable.link.remove();
+    text_input.destroy.link.remove();
 
-    self.relay.text_inputs.remove(node);
-    util.gpa.destroy(node);
+    text_input.link.remove();
+    util.gpa.destroy(text_input);
 }