diff --git a/include/sway/commands.h b/include/sway/commands.h
index b1f0423d..4ee7af2a 100644
--- a/include/sway/commands.h
+++ b/include/sway/commands.h
@@ -1,6 +1,8 @@
 #ifndef _SWAY_COMMANDS_H
 #define _SWAY_COMMANDS_H
 
+#include "config.h"
+
 /**
  * Indicates the result of a command's execution.
  */
@@ -15,6 +17,7 @@ enum cmd_status {
 	CMD_BLOCK_BAR,
 	CMD_BLOCK_BAR_COLORS,
 	CMD_BLOCK_INPUT,
+	CMD_BLOCK_SEAT,
 	CMD_BLOCK_COMMANDS,
 	CMD_BLOCK_IPC,
 	CMD_BLOCK_IPC_EVENTS,
@@ -107,6 +110,7 @@ sway_cmd cmd_gaps;
 sway_cmd cmd_hide_edge_borders;
 sway_cmd cmd_include;
 sway_cmd cmd_input;
+sway_cmd cmd_seat;
 sway_cmd cmd_ipc;
 sway_cmd cmd_kill;
 sway_cmd cmd_layout;
@@ -176,6 +180,7 @@ sway_cmd bar_colors_cmd_statusline;
 sway_cmd bar_colors_cmd_focused_statusline;
 sway_cmd bar_colors_cmd_urgent_workspace;
 
+sway_cmd input_cmd_seat;
 sway_cmd input_cmd_accel_profile;
 sway_cmd input_cmd_click_method;
 sway_cmd input_cmd_drag_lock;
@@ -187,6 +192,14 @@ sway_cmd input_cmd_natural_scroll;
 sway_cmd input_cmd_pointer_accel;
 sway_cmd input_cmd_scroll_method;
 sway_cmd input_cmd_tap;
+sway_cmd input_cmd_xkb_layout;
+sway_cmd input_cmd_xkb_model;
+sway_cmd input_cmd_xkb_options;
+sway_cmd input_cmd_xkb_rules;
+sway_cmd input_cmd_xkb_variant;
+
+sway_cmd seat_cmd_fallback;
+sway_cmd seat_cmd_attach;
 
 sway_cmd cmd_ipc_cmd;
 sway_cmd cmd_ipc_events;
diff --git a/include/sway/config.h b/include/sway/config.h
index afff2738..83ded720 100644
--- a/include/sway/config.h
+++ b/include/sway/config.h
@@ -67,10 +67,33 @@ struct input_config {
 	int send_events;
 	int tap;
 
+	char *xkb_layout;
+	char *xkb_model;
+	char *xkb_options;
+	char *xkb_rules;
+	char *xkb_variant;
+
 	bool capturable;
 	struct wlr_box region;
 };
 
+/**
+ * Options for misc device configurations that happen in the seat block
+ */
+struct seat_attachment_config {
+	char *identifier;
+	// TODO other things are configured here for some reason
+};
+
+/**
+ * Options for multiseat and other misc device configurations
+ */
+struct seat_config {
+	char *name;
+	int fallback; // -1 means not set
+	list_t *attachments; // list of seat_attachment configs
+};
+
 /**
  * Size and position configuration for a particular output.
  *
@@ -262,6 +285,7 @@ struct sway_config {
 	list_t *pid_workspaces;
 	list_t *output_configs;
 	list_t *input_configs;
+	list_t *seat_configs;
 	list_t *criteria;
 	list_t *no_focus;
 	list_t *active_bar_modifiers;
@@ -358,9 +382,19 @@ char *do_var_replacement(char *str);
 struct cmd_results *check_security_config();
 
 int input_identifier_cmp(const void *item, const void *data);
+struct input_config *new_input_config(const char* identifier);
 void merge_input_config(struct input_config *dst, struct input_config *src);
-void apply_input_config(struct input_config *ic, struct libinput_device *dev);
 void free_input_config(struct input_config *ic);
+void apply_input_config(struct input_config *input);
+
+int seat_name_cmp(const void *item, const void *data);
+struct seat_config *new_seat_config(const char* name);
+void merge_seat_config(struct seat_config *dst, struct seat_config *src);
+void free_seat_config(struct seat_config *ic);
+struct seat_attachment_config *seat_attachment_config_new();
+struct seat_attachment_config *seat_config_get_attachment(
+		struct seat_config *seat_config, char *identifier);
+void apply_seat_config(struct seat_config *seat);
 
 int output_name_cmp(const void *item, const void *data);
 struct output_config *new_output_config();
diff --git a/include/sway/container.h b/include/sway/container.h
index b15e0428..9a5e312b 100644
--- a/include/sway/container.h
+++ b/include/sway/container.h
@@ -3,6 +3,7 @@
 #include <stdint.h>
 #include <sys/types.h>
 #include <wlr/types/wlr_box.h>
+#include <wlr/types/wlr_surface.h>
 #include "list.h"
 
 typedef struct sway_container swayc_t;
@@ -123,6 +124,10 @@ struct sway_container {
 	 * Marks applied to the container, list_t of char*.
 	 */
 	list_t *marks;
+
+	struct {
+		struct wl_signal destroy;
+	} events;
 };
 
 void swayc_descendants_of_type(swayc_t *root, enum swayc_types type,
@@ -137,4 +142,7 @@ swayc_t *destroy_view(swayc_t *view);
 
 swayc_t *swayc_parent_by_type(swayc_t *container, enum swayc_types type);
 
+swayc_t *swayc_at(swayc_t *parent, double lx, double ly,
+		struct wlr_surface **surface, double *sx, double *sy);
+
 #endif
diff --git a/include/sway/input/cursor.h b/include/sway/input/cursor.h
new file mode 100644
index 00000000..2f70cf4b
--- /dev/null
+++ b/include/sway/input/cursor.h
@@ -0,0 +1,30 @@
+#ifndef _SWAY_INPUT_CURSOR_H
+#define _SWAY_INPUT_CURSOR_H
+
+#include "sway/input/seat.h"
+
+struct sway_cursor {
+	struct sway_seat *seat;
+	struct wlr_cursor *cursor;
+	struct wlr_xcursor_manager *xcursor_manager;
+
+	double x, y;
+
+	struct wl_listener motion;
+	struct wl_listener motion_absolute;
+	struct wl_listener button;
+	struct wl_listener axis;
+
+	struct wl_listener touch_down;
+	struct wl_listener touch_up;
+	struct wl_listener touch_motion;
+
+	struct wl_listener tool_axis;
+	struct wl_listener tool_tip;
+
+	struct wl_listener request_set_cursor;
+};
+
+struct sway_cursor *sway_cursor_create(struct sway_seat *seat);
+
+#endif
diff --git a/include/sway/input/input-manager.h b/include/sway/input/input-manager.h
new file mode 100644
index 00000000..53064eed
--- /dev/null
+++ b/include/sway/input/input-manager.h
@@ -0,0 +1,49 @@
+#ifndef _SWAY_INPUT_INPUT_MANAGER_H
+#define _SWAY_INPUT_INPUT_MANAGER_H
+#include <libinput.h>
+#include "sway/server.h"
+#include "sway/config.h"
+#include "list.h"
+
+extern struct input_config *current_input_config;
+extern struct seat_config *current_seat_config;
+
+/**
+ * The global singleton input manager
+ * TODO: make me not a global
+ */
+extern struct sway_input_manager *input_manager;
+
+struct sway_input_device {
+	char *identifier;
+	struct wlr_input_device *wlr_device;
+	struct input_config *config;
+	struct wl_list link;
+};
+
+struct sway_input_manager {
+	struct wl_listener input_add;
+	struct wl_listener input_remove;
+	struct sway_server *server;
+	struct wl_list devices;
+	struct wl_list seats;
+};
+
+struct sway_input_manager *sway_input_manager_create(
+		struct sway_server *server);
+
+bool sway_input_manager_has_focus(struct sway_input_manager *input,
+		swayc_t *container);
+
+void sway_input_manager_set_focus(struct sway_input_manager *input,
+		swayc_t *container);
+
+void sway_input_manager_configure_xcursor(struct sway_input_manager *input);
+
+void sway_input_manager_apply_input_config(struct sway_input_manager *input,
+		struct input_config *input_config);
+
+void sway_input_manager_apply_seat_config(struct sway_input_manager *input,
+		struct seat_config *seat_config);
+
+#endif
diff --git a/include/sway/input/keyboard.h b/include/sway/input/keyboard.h
new file mode 100644
index 00000000..d9251f4c
--- /dev/null
+++ b/include/sway/input/keyboard.h
@@ -0,0 +1,22 @@
+#ifndef _SWAY_INPUT_KEYBOARD_H
+#define _SWAY_INPUT_KEYBOARD_H
+
+#include "sway/input/seat.h"
+
+struct sway_keyboard {
+	struct sway_seat_device *seat_device;
+
+	struct xkb_keymap *keymap;
+
+	struct wl_listener keyboard_key;
+	struct wl_listener keyboard_modifiers;
+};
+
+struct sway_keyboard *sway_keyboard_create(struct sway_seat *seat,
+		struct sway_seat_device *device);
+
+void sway_keyboard_configure(struct sway_keyboard *keyboard);
+
+void sway_keyboard_destroy(struct sway_keyboard *keyboard);
+
+#endif
diff --git a/include/sway/input/seat.h b/include/sway/input/seat.h
new file mode 100644
index 00000000..d703f94c
--- /dev/null
+++ b/include/sway/input/seat.h
@@ -0,0 +1,47 @@
+#ifndef _SWAY_INPUT_SEAT_H
+#define _SWAY_INPUT_SEAT_H
+
+#include <wlr/types/wlr_seat.h>
+#include "sway/input/input-manager.h"
+
+struct sway_seat_device {
+	struct sway_seat *sway_seat;
+	struct sway_input_device *input_device;
+	struct sway_keyboard *keyboard;
+	struct seat_attachment_config *attachment_config;
+	struct wl_list link; // sway_seat::devices
+};
+
+struct sway_seat {
+	struct wlr_seat *wlr_seat;
+	struct seat_config *config;
+	struct sway_cursor *cursor;
+	struct sway_input_manager *input;
+	swayc_t *focus;
+
+	struct wl_listener focus_destroy;
+
+	struct wl_list devices; // sway_seat_device::link
+
+	struct wl_list link; // input_manager::seats
+};
+
+struct sway_seat *sway_seat_create(struct sway_input_manager *input,
+		const char *seat_name);
+
+void sway_seat_add_device(struct sway_seat *seat,
+		struct sway_input_device *device);
+
+void sway_seat_configure_device(struct sway_seat *seat,
+		struct sway_input_device *device);
+
+void sway_seat_remove_device(struct sway_seat *seat,
+		struct sway_input_device *device);
+
+void sway_seat_configure_xcursor(struct sway_seat *seat);
+
+void sway_seat_set_focus(struct sway_seat *seat, swayc_t *container);
+
+void sway_seat_set_config(struct sway_seat *seat, struct seat_config *seat_config);
+
+#endif
diff --git a/include/sway/ipc-json.h b/include/sway/ipc-json.h
index 9435b664..eef5a018 100644
--- a/include/sway/ipc-json.h
+++ b/include/sway/ipc-json.h
@@ -2,10 +2,12 @@
 #define _SWAY_IPC_JSON_H
 #include <json-c/json.h>
 #include "sway/container.h"
+#include "sway/input/input-manager.h"
 
 json_object *ipc_json_get_version();
 
 json_object *ipc_json_describe_container(swayc_t *c);
 json_object *ipc_json_describe_container_recursive(swayc_t *c);
+json_object *ipc_json_describe_input(struct sway_input_device *device);
 
 #endif
diff --git a/include/sway/server.h b/include/sway/server.h
index 3fa72e84..76a05476 100644
--- a/include/sway/server.h
+++ b/include/sway/server.h
@@ -22,7 +22,7 @@ struct sway_server {
 	struct wlr_compositor *compositor;
 	struct wlr_data_device_manager *data_device_manager;
 
-	struct sway_input *input;
+	struct sway_input_manager *input;
 
 	struct wl_listener output_add;
 	struct wl_listener output_remove;
diff --git a/meson.build b/meson.build
index 8e7b98ed..029aea46 100644
--- a/meson.build
+++ b/meson.build
@@ -29,6 +29,7 @@ xkbcommon      = dependency('xkbcommon')
 pango          = dependency('pango')
 pixman         = dependency('pixman-1')
 libcap         = dependency('libcap')
+libinput       = dependency('libinput')
 math           = cc.find_library('m')
 git = find_program('git', required: false)
 a2x = find_program('a2x', required: false)
diff --git a/sway/commands.c b/sway/commands.c
index d6cf7a64..34afb6a0 100644
--- a/sway/commands.c
+++ b/sway/commands.c
@@ -9,6 +9,7 @@
 #include "sway/commands.h"
 #include "sway/config.h"
 #include "sway/security.h"
+#include "sway/input/input-manager.h"
 #include "stringop.h"
 #include "log.h"
 
@@ -56,6 +57,40 @@ struct cmd_results *checkarg(int argc, const char *name, enum expected_args type
 	return error;
 }
 
+void apply_input_config(struct input_config *input) {
+	int i;
+	i = list_seq_find(config->input_configs, input_identifier_cmp, input->identifier);
+	if (i >= 0) {
+		// merge existing config
+		struct input_config *ic = config->input_configs->items[i];
+		merge_input_config(ic, input);
+		free_input_config(input);
+		input = ic;
+	} else {
+		list_add(config->input_configs, input);
+	}
+
+	current_input_config = input;
+	sway_input_manager_apply_input_config(input_manager, input);
+}
+
+void apply_seat_config(struct seat_config *seat) {
+	int i;
+	i = list_seq_find(config->seat_configs, seat_name_cmp, seat->name);
+	if (i >= 0) {
+		// merge existing config
+		struct seat_config *sc = config->seat_configs->items[i];
+		merge_seat_config(sc, seat);
+		free_seat_config(seat);
+		seat = sc;
+	} else {
+		list_add(config->seat_configs, seat);
+	}
+
+	current_seat_config = seat;
+	sway_input_manager_apply_seat_config(input_manager, seat);
+}
+
 /**
  * Check and add color to buffer.
  *
@@ -96,7 +131,9 @@ static struct cmd_handler handlers[] = {
 	{ "exec_always", cmd_exec_always },
 	{ "exit", cmd_exit },
 	{ "include", cmd_include },
+	{ "input", cmd_input },
 	{ "output", cmd_output },
+	{ "seat", cmd_seat },
 };
 
 static int handler_compare(const void *_a, const void *_b) {
@@ -105,37 +142,51 @@ static int handler_compare(const void *_a, const void *_b) {
 	return strcasecmp(a->command, b->command);
 }
 
+// must be in order for the bsearch
+static struct cmd_handler input_handlers[] = {
+	{ "accel_profile", input_cmd_accel_profile },
+	{ "click_method", input_cmd_click_method },
+	{ "drag_lock", input_cmd_drag_lock },
+	{ "dwt", input_cmd_dwt },
+	{ "events", input_cmd_events },
+	{ "left_handed", input_cmd_left_handed },
+	{ "middle_emulation", input_cmd_middle_emulation },
+	{ "natural_scroll", input_cmd_natural_scroll },
+	{ "pointer_accel", input_cmd_pointer_accel },
+	{ "scroll_method", input_cmd_scroll_method },
+	{ "tap", input_cmd_tap },
+	{ "xkb_layout", input_cmd_xkb_layout },
+	{ "xkb_model", input_cmd_xkb_model },
+	{ "xkb_options", input_cmd_xkb_options },
+	{ "xkb_rules", input_cmd_xkb_rules },
+	{ "xkb_variant", input_cmd_xkb_variant },
+};
+
+// must be in order for the bsearch
+static struct cmd_handler seat_handlers[] = {
+	{ "attach", seat_cmd_attach },
+	{ "fallback", seat_cmd_fallback },
+};
+
 static struct cmd_handler *find_handler(char *line, enum cmd_status block) {
 	struct cmd_handler d = { .command=line };
 	struct cmd_handler *res = NULL;
-	sway_log(L_DEBUG, "find_handler(%s) %d", line, block == CMD_BLOCK_INPUT);
-	/* TODO
-	if (block == CMD_BLOCK_BAR) {
-		res = bsearch(&d, bar_handlers,
-			sizeof(bar_handlers) / sizeof(struct cmd_handler),
-			sizeof(struct cmd_handler), handler_compare);
-	} else if (block == CMD_BLOCK_BAR_COLORS){
-		res = bsearch(&d, bar_colors_handlers,
-			sizeof(bar_colors_handlers) / sizeof(struct cmd_handler),
-			sizeof(struct cmd_handler), handler_compare);
-	} else if (block == CMD_BLOCK_INPUT) {
+	sway_log(L_DEBUG, "find_handler(%s) %d", line, block == CMD_BLOCK_SEAT);
+
+	if (block == CMD_BLOCK_INPUT) {
 		res = bsearch(&d, input_handlers,
-			sizeof(input_handlers) / sizeof(struct cmd_handler),
-			sizeof(struct cmd_handler), handler_compare);
-	} else if (block == CMD_BLOCK_IPC) {
-		res = bsearch(&d, ipc_handlers,
-			sizeof(ipc_handlers) / sizeof(struct cmd_handler),
-			sizeof(struct cmd_handler), handler_compare);
-	} else if (block == CMD_BLOCK_IPC_EVENTS) {
-		res = bsearch(&d, ipc_event_handlers,
-			sizeof(ipc_event_handlers) / sizeof(struct cmd_handler),
-			sizeof(struct cmd_handler), handler_compare);
+				sizeof(input_handlers) / sizeof(struct cmd_handler),
+				sizeof(struct cmd_handler), handler_compare);
+	} else if (block == CMD_BLOCK_SEAT) {
+		res = bsearch(&d, seat_handlers,
+				sizeof(seat_handlers) / sizeof(struct cmd_handler),
+				sizeof(struct cmd_handler), handler_compare);
 	} else {
-	*/
 		res = bsearch(&d, handlers,
-			sizeof(handlers) / sizeof(struct cmd_handler),
-			sizeof(struct cmd_handler), handler_compare);
-	//}
+				sizeof(handlers) / sizeof(struct cmd_handler),
+				sizeof(struct cmd_handler), handler_compare);
+	}
+
 	return res;
 }
 
@@ -239,8 +290,8 @@ struct cmd_results *config_command(char *exec, enum cmd_status block) {
 		argv[i] = do_var_replacement(argv[i]);
 		unescape_string(argv[i]);
 	}
-	/* Strip quotes for first argument.
-	 * TODO This part needs to be handled much better */
+	// Strip quotes for first argument.
+	// TODO This part needs to be handled much better
 	if (argc>1 && (*argv[1] == '\"' || *argv[1] == '\'')) {
 		strip_quotes(argv[1]);
 	}
diff --git a/sway/commands/input.c b/sway/commands/input.c
new file mode 100644
index 00000000..ccb1d276
--- /dev/null
+++ b/sway/commands/input.c
@@ -0,0 +1,65 @@
+#include <string.h>
+#include <strings.h>
+#include "sway/commands.h"
+#include "sway/input/input-manager.h"
+#include "log.h"
+
+struct cmd_results *cmd_input(int argc, char **argv) {
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "input", EXPECTED_AT_LEAST, 2))) {
+		return error;
+	}
+
+	if (config->reading && strcmp("{", argv[1]) == 0) {
+		current_input_config = new_input_config(argv[0]);
+		sway_log(L_DEBUG, "entering input block: %s", current_input_config->identifier);
+		return cmd_results_new(CMD_BLOCK_INPUT, NULL, NULL);
+	}
+
+	if ((error = checkarg(argc, "input", EXPECTED_AT_LEAST, 3))) {
+		return error;
+	}
+
+	int argc_new = argc-2;
+	char **argv_new = argv+2;
+
+	struct cmd_results *res;
+	current_input_config = new_input_config(argv[0]);
+	if (strcasecmp("accel_profile", argv[1]) == 0) {
+		res = input_cmd_accel_profile(argc_new, argv_new);
+	} else if (strcasecmp("click_method", argv[1]) == 0) {
+		res = input_cmd_click_method(argc_new, argv_new);
+	} else if (strcasecmp("drag_lock", argv[1]) == 0) {
+		res = input_cmd_drag_lock(argc_new, argv_new);
+	} else if (strcasecmp("dwt", argv[1]) == 0) {
+		res = input_cmd_dwt(argc_new, argv_new);
+	} else if (strcasecmp("events", argv[1]) == 0) {
+		res = input_cmd_events(argc_new, argv_new);
+	} else if (strcasecmp("left_handed", argv[1]) == 0) {
+		res = input_cmd_left_handed(argc_new, argv_new);
+	} else if (strcasecmp("middle_emulation", argv[1]) == 0) {
+		res = input_cmd_middle_emulation(argc_new, argv_new);
+	} else if (strcasecmp("natural_scroll", argv[1]) == 0) {
+		res = input_cmd_natural_scroll(argc_new, argv_new);
+	} else if (strcasecmp("pointer_accel", argv[1]) == 0) {
+		res = input_cmd_pointer_accel(argc_new, argv_new);
+	} else if (strcasecmp("scroll_method", argv[1]) == 0) {
+		res = input_cmd_scroll_method(argc_new, argv_new);
+	} else if (strcasecmp("tap", argv[1]) == 0) {
+		res = input_cmd_tap(argc_new, argv_new);
+	} else if (strcasecmp("xkb_layout", argv[1]) == 0) {
+		res = input_cmd_xkb_layout(argc_new, argv_new);
+	} else if (strcasecmp("xkb_model", argv[1]) == 0) {
+		res = input_cmd_xkb_model(argc_new, argv_new);
+	} else if (strcasecmp("xkb_options", argv[1]) == 0) {
+		res = input_cmd_xkb_options(argc_new, argv_new);
+	} else if (strcasecmp("xkb_rules", argv[1]) == 0) {
+		res = input_cmd_xkb_rules(argc_new, argv_new);
+	} else if (strcasecmp("xkb_variant", argv[1]) == 0) {
+		res = input_cmd_xkb_variant(argc_new, argv_new);
+	} else {
+		res = cmd_results_new(CMD_INVALID, "input <device>", "Unknown command %s", argv[1]);
+	}
+	current_input_config = NULL;
+	return res;
+}
diff --git a/sway/commands/input/accel_profile.c b/sway/commands/input/accel_profile.c
new file mode 100644
index 00000000..f72b7d48
--- /dev/null
+++ b/sway/commands/input/accel_profile.c
@@ -0,0 +1,30 @@
+#include <string.h>
+#include <strings.h>
+#include "sway/config.h"
+#include "sway/commands.h"
+#include "sway/input/input-manager.h"
+
+struct cmd_results *input_cmd_accel_profile(int argc, char **argv) {
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "accel_profile", EXPECTED_AT_LEAST, 1))) {
+		return error;
+	}
+	if (!current_input_config) {
+		return cmd_results_new(CMD_FAILURE, "accel_profile",
+				"No input device defined.");
+	}
+	struct input_config *new_config =
+		new_input_config(current_input_config->identifier);
+
+	if (strcasecmp(argv[0], "adaptive") == 0) {
+		new_config->accel_profile = LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE;
+	} else if (strcasecmp(argv[0], "flat") == 0) {
+		new_config->accel_profile = LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT;
+	} else {
+		return cmd_results_new(CMD_INVALID, "accel_profile",
+				"Expected 'accel_profile <adaptive|flat>'");
+	}
+
+	apply_input_config(new_config);
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
diff --git a/sway/commands/input/click_method.c b/sway/commands/input/click_method.c
new file mode 100644
index 00000000..dcf64c1a
--- /dev/null
+++ b/sway/commands/input/click_method.c
@@ -0,0 +1,35 @@
+#include <string.h>
+#include <strings.h>
+#include "sway/commands.h"
+#include "sway/config.h"
+#include "sway/input/input-manager.h"
+#include "log.h"
+
+struct cmd_results *input_cmd_click_method(int argc, char **argv) {
+	sway_log(L_DEBUG, "click_method for device:  %d %s",
+		current_input_config==NULL, current_input_config->identifier);
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "click_method", EXPECTED_AT_LEAST, 1))) {
+		return error;
+	}
+	if (!current_input_config) {
+		return cmd_results_new(CMD_FAILURE, "click_method",
+			"No input device defined.");
+	}
+	struct input_config *new_config =
+		new_input_config(current_input_config->identifier);
+
+	if (strcasecmp(argv[0], "none") == 0) {
+		new_config->click_method = LIBINPUT_CONFIG_CLICK_METHOD_NONE;
+	} else if (strcasecmp(argv[0], "button_areas") == 0) {
+		new_config->click_method = LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS;
+	} else if (strcasecmp(argv[0], "clickfinger") == 0) {
+		new_config->click_method = LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER;
+	} else {
+		return cmd_results_new(CMD_INVALID, "click_method",
+			"Expected 'click_method <none|button_areas|clickfinger'");
+	}
+
+	apply_input_config(new_config);
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
diff --git a/sway/commands/input/drag_lock.c b/sway/commands/input/drag_lock.c
new file mode 100644
index 00000000..1783978a
--- /dev/null
+++ b/sway/commands/input/drag_lock.c
@@ -0,0 +1,30 @@
+#include <string.h>
+#include <strings.h>
+#include "sway/config.h"
+#include "sway/commands.h"
+#include "sway/input/input-manager.h"
+
+struct cmd_results *input_cmd_drag_lock(int argc, char **argv) {
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "drag_lock", EXPECTED_AT_LEAST, 1))) {
+		return error;
+	}
+	if (!current_input_config) {
+		return cmd_results_new(CMD_FAILURE,
+			"drag_lock", "No input device defined.");
+	}
+	struct input_config *new_config =
+		new_input_config(current_input_config->identifier);
+
+	if (strcasecmp(argv[0], "enabled") == 0) {
+		new_config->drag_lock = LIBINPUT_CONFIG_DRAG_LOCK_ENABLED;
+	} else if (strcasecmp(argv[0], "disabled") == 0) {
+		new_config->drag_lock = LIBINPUT_CONFIG_DRAG_LOCK_DISABLED;
+	} else {
+		return cmd_results_new(CMD_INVALID, "drag_lock",
+			"Expected 'drag_lock <enabled|disabled>'");
+	}
+
+	apply_input_config(new_config);
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
diff --git a/sway/commands/input/dwt.c b/sway/commands/input/dwt.c
new file mode 100644
index 00000000..8108a110
--- /dev/null
+++ b/sway/commands/input/dwt.c
@@ -0,0 +1,29 @@
+#include <string.h>
+#include <strings.h>
+#include "sway/config.h"
+#include "sway/commands.h"
+#include "sway/input/input-manager.h"
+
+struct cmd_results *input_cmd_dwt(int argc, char **argv) {
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "dwt", EXPECTED_AT_LEAST, 1))) {
+		return error;
+	}
+	if (!current_input_config) {
+		return cmd_results_new(CMD_FAILURE, "dwt", "No input device defined.");
+	}
+	struct input_config *new_config =
+		new_input_config(current_input_config->identifier);
+
+	if (strcasecmp(argv[0], "enabled") == 0) {
+		new_config->dwt = LIBINPUT_CONFIG_DWT_ENABLED;
+	} else if (strcasecmp(argv[0], "disabled") == 0) {
+		new_config->dwt = LIBINPUT_CONFIG_DWT_DISABLED;
+	} else {
+		return cmd_results_new(CMD_INVALID, "dwt",
+			"Expected 'dwt <enabled|disabled>'");
+	}
+
+	apply_input_config(new_config);
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
diff --git a/sway/commands/input/events.c b/sway/commands/input/events.c
new file mode 100644
index 00000000..8a74c11e
--- /dev/null
+++ b/sway/commands/input/events.c
@@ -0,0 +1,36 @@
+#include <string.h>
+#include <strings.h>
+#include "sway/config.h"
+#include "sway/commands.h"
+#include "sway/input/input-manager.h"
+#include "log.h"
+
+struct cmd_results *input_cmd_events(int argc, char **argv) {
+	sway_log(L_DEBUG, "events for device: %s",
+		current_input_config->identifier);
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "events", EXPECTED_AT_LEAST, 1))) {
+		return error;
+	}
+	if (!current_input_config) {
+		return cmd_results_new(CMD_FAILURE, "events",
+			"No input device defined.");
+	}
+	struct input_config *new_config =
+		new_input_config(current_input_config->identifier);
+
+	if (strcasecmp(argv[0], "enabled") == 0) {
+		new_config->send_events = LIBINPUT_CONFIG_SEND_EVENTS_ENABLED;
+	} else if (strcasecmp(argv[0], "disabled") == 0) {
+		new_config->send_events = LIBINPUT_CONFIG_SEND_EVENTS_DISABLED;
+	} else if (strcasecmp(argv[0], "disabled_on_external_mouse") == 0) {
+		new_config->send_events =
+			LIBINPUT_CONFIG_SEND_EVENTS_DISABLED_ON_EXTERNAL_MOUSE;
+	} else {
+		return cmd_results_new(CMD_INVALID, "events",
+			"Expected 'events <enabled|disabled|disabled_on_external_mouse>'");
+	}
+
+	apply_input_config(new_config);
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
diff --git a/sway/commands/input/left_handed.c b/sway/commands/input/left_handed.c
new file mode 100644
index 00000000..35740df3
--- /dev/null
+++ b/sway/commands/input/left_handed.c
@@ -0,0 +1,30 @@
+#include <string.h>
+#include <strings.h>
+#include "sway/config.h"
+#include "sway/commands.h"
+#include "sway/input/input-manager.h"
+
+struct cmd_results *input_cmd_left_handed(int argc, char **argv) {
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "left_handed", EXPECTED_AT_LEAST, 1))) {
+		return error;
+	}
+	if (!current_input_config) {
+		return cmd_results_new(CMD_FAILURE, "left_handed",
+			"No input device defined.");
+	}
+	struct input_config *new_config =
+		new_input_config(current_input_config->identifier);
+
+	if (strcasecmp(argv[0], "enabled") == 0) {
+		new_config->left_handed = 1;
+	} else if (strcasecmp(argv[0], "disabled") == 0) {
+		new_config->left_handed = 0;
+	} else {
+		return cmd_results_new(CMD_INVALID, "left_handed",
+			"Expected 'left_handed <enabled|disabled>'");
+	}
+
+	apply_input_config(new_config);
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
diff --git a/sway/commands/input/middle_emulation.c b/sway/commands/input/middle_emulation.c
new file mode 100644
index 00000000..7bc08ae6
--- /dev/null
+++ b/sway/commands/input/middle_emulation.c
@@ -0,0 +1,31 @@
+#include <string.h>
+#include <strings.h>
+#include "sway/config.h"
+#include "sway/commands.h"
+#include "sway/input/input-manager.h"
+
+struct cmd_results *input_cmd_middle_emulation(int argc, char **argv) {
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "middle_emulation", EXPECTED_AT_LEAST, 1))) {
+		return error;
+	}
+	if (!current_input_config) {
+		return cmd_results_new(CMD_FAILURE, "middle_emulation",
+			"No input device defined.");
+	}
+	struct input_config *new_config =
+		new_input_config(current_input_config->identifier);
+
+	if (strcasecmp(argv[0], "enabled") == 0) {
+		new_config->middle_emulation = LIBINPUT_CONFIG_MIDDLE_EMULATION_ENABLED;
+	} else if (strcasecmp(argv[0], "disabled") == 0) {
+		new_config->middle_emulation =
+			LIBINPUT_CONFIG_MIDDLE_EMULATION_DISABLED;
+	} else {
+		return cmd_results_new(CMD_INVALID, "middle_emulation",
+			"Expected 'middle_emulation <enabled|disabled>'");
+	}
+
+	apply_input_config(new_config);
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
diff --git a/sway/commands/input/natural_scroll.c b/sway/commands/input/natural_scroll.c
new file mode 100644
index 00000000..a7dcdc2c
--- /dev/null
+++ b/sway/commands/input/natural_scroll.c
@@ -0,0 +1,30 @@
+#include <string.h>
+#include <strings.h>
+#include "sway/config.h"
+#include "sway/commands.h"
+#include "sway/input/input-manager.h"
+
+struct cmd_results *input_cmd_natural_scroll(int argc, char **argv) {
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "natural_scroll", EXPECTED_AT_LEAST, 1))) {
+		return error;
+	}
+	if (!current_input_config) {
+		return cmd_results_new(CMD_FAILURE, "natural_scoll",
+			"No input device defined.");
+	}
+	struct input_config *new_config =
+		new_input_config(current_input_config->identifier);
+
+	if (strcasecmp(argv[0], "enabled") == 0) {
+		new_config->natural_scroll = 1;
+	} else if (strcasecmp(argv[0], "disabled") == 0) {
+		new_config->natural_scroll = 0;
+	} else {
+		return cmd_results_new(CMD_INVALID, "natural_scroll",
+			"Expected 'natural_scroll <enabled|disabled>'");
+	}
+
+	apply_input_config(new_config);
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
diff --git a/sway/commands/input/pointer_accel.c b/sway/commands/input/pointer_accel.c
new file mode 100644
index 00000000..d2261a63
--- /dev/null
+++ b/sway/commands/input/pointer_accel.c
@@ -0,0 +1,28 @@
+#include <stdlib.h>
+#include <string.h>
+#include "sway/config.h"
+#include "sway/commands.h"
+#include "sway/input/input-manager.h"
+
+struct cmd_results *input_cmd_pointer_accel(int argc, char **argv) {
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "pointer_accel", EXPECTED_AT_LEAST, 1))) {
+		return error;
+	}
+	if (!current_input_config) {
+		return cmd_results_new(CMD_FAILURE,
+			"pointer_accel", "No input device defined.");
+	}
+	struct input_config *new_config =
+		new_input_config(current_input_config->identifier);
+
+	float pointer_accel = atof(argv[0]);
+	if (pointer_accel < -1 || pointer_accel > 1) {
+		return cmd_results_new(CMD_INVALID, "pointer_accel",
+			"Input out of range [-1, 1]");
+	}
+	new_config->pointer_accel = pointer_accel;
+
+	apply_input_config(new_config);
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
diff --git a/sway/commands/input/scroll_method.c b/sway/commands/input/scroll_method.c
new file mode 100644
index 00000000..035262cf
--- /dev/null
+++ b/sway/commands/input/scroll_method.c
@@ -0,0 +1,34 @@
+#include <string.h>
+#include <strings.h>
+#include "sway/config.h"
+#include "sway/commands.h"
+#include "sway/input/input-manager.h"
+
+struct cmd_results *input_cmd_scroll_method(int argc, char **argv) {
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "scroll_method", EXPECTED_AT_LEAST, 1))) {
+		return error;
+	}
+	if (!current_input_config) {
+		return cmd_results_new(CMD_FAILURE, "scroll_method",
+			"No input device defined.");
+	}
+	struct input_config *new_config =
+		new_input_config(current_input_config->identifier);
+
+	if (strcasecmp(argv[0], "none") == 0) {
+		new_config->scroll_method = LIBINPUT_CONFIG_SCROLL_NO_SCROLL;
+	} else if (strcasecmp(argv[0], "two_finger") == 0) {
+		new_config->scroll_method = LIBINPUT_CONFIG_SCROLL_2FG;
+	} else if (strcasecmp(argv[0], "edge") == 0) {
+		new_config->scroll_method = LIBINPUT_CONFIG_SCROLL_EDGE;
+	} else if (strcasecmp(argv[0], "on_button_down") == 0) {
+		new_config->scroll_method = LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN;
+	} else {
+		return cmd_results_new(CMD_INVALID, "scroll_method",
+			"Expected 'scroll_method <none|two_finger|edge|on_button_down>'");
+	}
+
+	apply_input_config(new_config);
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
diff --git a/sway/commands/input/tap.c b/sway/commands/input/tap.c
new file mode 100644
index 00000000..8547c0cd
--- /dev/null
+++ b/sway/commands/input/tap.c
@@ -0,0 +1,33 @@
+#include <string.h>
+#include <strings.h>
+#include "sway/config.h"
+#include "sway/commands.h"
+#include "sway/input/input-manager.h"
+#include "log.h"
+
+struct cmd_results *input_cmd_tap(int argc, char **argv) {
+	sway_log(L_DEBUG, "tap for device: %s", current_input_config->identifier);
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "tap", EXPECTED_AT_LEAST, 1))) {
+		return error;
+	}
+	if (!current_input_config) {
+		return cmd_results_new(CMD_FAILURE, "tap", "No input device defined.");
+	}
+	struct input_config *new_config =
+		new_input_config(current_input_config->identifier);
+
+	if (strcasecmp(argv[0], "enabled") == 0) {
+		new_config->tap = LIBINPUT_CONFIG_TAP_ENABLED;
+	} else if (strcasecmp(argv[0], "disabled") == 0) {
+		new_config->tap = LIBINPUT_CONFIG_TAP_DISABLED;
+	} else {
+		return cmd_results_new(CMD_INVALID, "tap",
+			"Expected 'tap <enabled|disabled>'");
+	}
+
+	sway_log(L_DEBUG, "apply-tap for device: %s",
+		current_input_config->identifier);
+	apply_input_config(new_config);
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
diff --git a/sway/commands/input/xkb_layout.c b/sway/commands/input/xkb_layout.c
new file mode 100644
index 00000000..a25d3850
--- /dev/null
+++ b/sway/commands/input/xkb_layout.c
@@ -0,0 +1,25 @@
+#define _XOPEN_SOURCE 700
+#include "sway/config.h"
+#include "sway/commands.h"
+#include "sway/input/input-manager.h"
+#include "log.h"
+
+struct cmd_results *input_cmd_xkb_layout(int argc, char **argv) {
+	sway_log(L_DEBUG, "xkb layout for device: %s", current_input_config->identifier);
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "xkb_layout", EXPECTED_EQUAL_TO, 1))) {
+		return error;
+	}
+	if (!current_input_config) {
+		return cmd_results_new(CMD_FAILURE, "xkb_layout", "No input device defined.");
+	}
+	struct input_config *new_config =
+		new_input_config(current_input_config->identifier);
+
+	new_config->xkb_layout = strdup(argv[0]);
+
+	sway_log(L_DEBUG, "apply-xkb_layout for device: %s layout: %s",
+		current_input_config->identifier, new_config->xkb_layout);
+	apply_input_config(new_config);
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
diff --git a/sway/commands/input/xkb_model.c b/sway/commands/input/xkb_model.c
new file mode 100644
index 00000000..9729e869
--- /dev/null
+++ b/sway/commands/input/xkb_model.c
@@ -0,0 +1,25 @@
+#define _XOPEN_SOURCE 700
+#include "sway/config.h"
+#include "sway/commands.h"
+#include "sway/input/input-manager.h"
+#include "log.h"
+
+struct cmd_results *input_cmd_xkb_model(int argc, char **argv) {
+	sway_log(L_DEBUG, "xkb model for device: %s", current_input_config->identifier);
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "xkb_model", EXPECTED_EQUAL_TO, 1))) {
+		return error;
+	}
+	if (!current_input_config) {
+		return cmd_results_new(CMD_FAILURE, "xkb_model", "No input device defined.");
+	}
+	struct input_config *new_config =
+		new_input_config(current_input_config->identifier);
+
+	new_config->xkb_model = strdup(argv[0]);
+
+	sway_log(L_DEBUG, "apply-xkb_model for device: %s model: %s",
+		current_input_config->identifier, new_config->xkb_model);
+	apply_input_config(new_config);
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
diff --git a/sway/commands/input/xkb_options.c b/sway/commands/input/xkb_options.c
new file mode 100644
index 00000000..504849cc
--- /dev/null
+++ b/sway/commands/input/xkb_options.c
@@ -0,0 +1,25 @@
+#define _XOPEN_SOURCE 700
+#include "sway/config.h"
+#include "sway/commands.h"
+#include "sway/input/input-manager.h"
+#include "log.h"
+
+struct cmd_results *input_cmd_xkb_options(int argc, char **argv) {
+	sway_log(L_DEBUG, "xkb options for device: %s", current_input_config->identifier);
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "xkb_options", EXPECTED_EQUAL_TO, 1))) {
+		return error;
+	}
+	if (!current_input_config) {
+		return cmd_results_new(CMD_FAILURE, "xkb_options", "No input device defined.");
+	}
+	struct input_config *new_config =
+		new_input_config(current_input_config->identifier);
+
+	new_config->xkb_options = strdup(argv[0]);
+
+	sway_log(L_DEBUG, "apply-xkb_options for device: %s options: %s",
+		current_input_config->identifier, new_config->xkb_options);
+	apply_input_config(new_config);
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
diff --git a/sway/commands/input/xkb_rules.c b/sway/commands/input/xkb_rules.c
new file mode 100644
index 00000000..db7d8abe
--- /dev/null
+++ b/sway/commands/input/xkb_rules.c
@@ -0,0 +1,25 @@
+#define _XOPEN_SOURCE 700
+#include "sway/config.h"
+#include "sway/commands.h"
+#include "sway/input/input-manager.h"
+#include "log.h"
+
+struct cmd_results *input_cmd_xkb_rules(int argc, char **argv) {
+	sway_log(L_DEBUG, "xkb rules for device: %s", current_input_config->identifier);
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "xkb_rules", EXPECTED_EQUAL_TO, 1))) {
+		return error;
+	}
+	if (!current_input_config) {
+		return cmd_results_new(CMD_FAILURE, "xkb_rules", "No input device defined.");
+	}
+	struct input_config *new_config =
+		new_input_config(current_input_config->identifier);
+
+	new_config->xkb_rules = strdup(argv[0]);
+
+	sway_log(L_DEBUG, "apply-xkb_rules for device: %s rules: %s",
+		current_input_config->identifier, new_config->xkb_rules);
+	apply_input_config(new_config);
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
diff --git a/sway/commands/input/xkb_variant.c b/sway/commands/input/xkb_variant.c
new file mode 100644
index 00000000..855e6abc
--- /dev/null
+++ b/sway/commands/input/xkb_variant.c
@@ -0,0 +1,25 @@
+#define _XOPEN_SOURCE 700
+#include "sway/config.h"
+#include "sway/commands.h"
+#include "sway/input/input-manager.h"
+#include "log.h"
+
+struct cmd_results *input_cmd_xkb_variant(int argc, char **argv) {
+	sway_log(L_DEBUG, "xkb variant for device: %s", current_input_config->identifier);
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "xkb_variant", EXPECTED_EQUAL_TO, 1))) {
+		return error;
+	}
+	if (!current_input_config) {
+		return cmd_results_new(CMD_FAILURE, "xkb_variant", "No input device defined.");
+	}
+	struct input_config *new_config =
+		new_input_config(current_input_config->identifier);
+
+	new_config->xkb_variant = strdup(argv[0]);
+
+	sway_log(L_DEBUG, "apply-xkb_variant for device: %s variant: %s",
+		current_input_config->identifier, new_config->xkb_variant);
+	apply_input_config(new_config);
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
diff --git a/sway/commands/seat.c b/sway/commands/seat.c
new file mode 100644
index 00000000..155bc510
--- /dev/null
+++ b/sway/commands/seat.c
@@ -0,0 +1,37 @@
+#include <string.h>
+#include <strings.h>
+#include "sway/commands.h"
+#include "sway/input/input-manager.h"
+#include "log.h"
+
+struct cmd_results *cmd_seat(int argc, char **argv) {
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "seat", EXPECTED_AT_LEAST, 2))) {
+		return error;
+	}
+
+	if (config->reading && strcmp("{", argv[1]) == 0) {
+		current_seat_config = new_seat_config(argv[0]);
+		sway_log(L_DEBUG, "entering seat block: %s", current_seat_config->name);
+		return cmd_results_new(CMD_BLOCK_SEAT, NULL, NULL);
+	}
+
+	if ((error = checkarg(argc, "seat", EXPECTED_AT_LEAST, 3))) {
+		return error;
+	}
+
+	int argc_new = argc-2;
+	char **argv_new = argv+2;
+
+	struct cmd_results *res;
+	current_seat_config = new_seat_config(argv[0]);
+	if (strcasecmp("attach", argv[1]) == 0) {
+		res = seat_cmd_attach(argc_new, argv_new);
+	} else if (strcasecmp("fallback", argv[1]) == 0) {
+		res = seat_cmd_fallback(argc_new, argv_new);
+	} else {
+		res = cmd_results_new(CMD_INVALID, "seat <name>", "Unknown command %s", argv[1]);
+	}
+	current_seat_config = NULL;
+	return res;
+}
diff --git a/sway/commands/seat/attach.c b/sway/commands/seat/attach.c
new file mode 100644
index 00000000..80ec63ce
--- /dev/null
+++ b/sway/commands/seat/attach.c
@@ -0,0 +1,26 @@
+#define _XOPEN_SOURCE 700
+#include <string.h>
+#include <strings.h>
+#include "sway/input/input-manager.h"
+#include "sway/commands.h"
+#include "sway/config.h"
+#include "log.h"
+#include "stringop.h"
+
+struct cmd_results *seat_cmd_attach(int argc, char **argv) {
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "attach", EXPECTED_AT_LEAST, 1))) {
+		return error;
+	}
+	if (!current_seat_config) {
+		return cmd_results_new(CMD_FAILURE, "attach", "No seat defined");
+	}
+
+	struct seat_config *new_config = new_seat_config(current_seat_config->name);
+	struct seat_attachment_config *new_attachment = seat_attachment_config_new();
+	new_attachment->identifier = strdup(argv[0]);
+	list_add(new_config->attachments, new_attachment);
+
+	apply_seat_config(new_config);
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
diff --git a/sway/commands/seat/fallback.c b/sway/commands/seat/fallback.c
new file mode 100644
index 00000000..7c129aae
--- /dev/null
+++ b/sway/commands/seat/fallback.c
@@ -0,0 +1,29 @@
+#include <string.h>
+#include <strings.h>
+#include "sway/config.h"
+#include "sway/commands.h"
+#include "sway/input/input-manager.h"
+
+struct cmd_results *seat_cmd_fallback(int argc, char **argv) {
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "fallback", EXPECTED_AT_LEAST, 1))) {
+		return error;
+	}
+	if (!current_seat_config) {
+		return cmd_results_new(CMD_FAILURE, "fallback", "No seat defined");
+	}
+	struct seat_config *new_config =
+		new_seat_config(current_seat_config->name);
+
+	if (strcasecmp(argv[0], "true") == 0) {
+		new_config->fallback = 1;
+	} else if (strcasecmp(argv[0], "false") == 0) {
+		new_config->fallback = 0;
+	} else {
+		return cmd_results_new(CMD_INVALID, "fallback",
+			"Expected 'fallback <true|false>'");
+	}
+
+	apply_seat_config(new_config);
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
diff --git a/sway/config.c b/sway/config.c
index 61131845..b591ae9e 100644
--- a/sway/config.c
+++ b/sway/config.c
@@ -12,7 +12,6 @@
 #include <signal.h>
 #include <libinput.h>
 #include <limits.h>
-#include <float.h>
 #include <dirent.h>
 #include <strings.h>
 #ifdef __linux__
@@ -21,6 +20,7 @@
 #include <dev/evdev/input-event-codes.h>
 #endif
 #include <wlr/types/wlr_output.h>
+#include "sway/input/input-manager.h"
 #include "sway/commands.h"
 #include "sway/config.h"
 #include "sway/layout.h"
@@ -44,11 +44,13 @@ static void config_defaults(struct sway_config *config) {
 	if (!(config->criteria = create_list())) goto cleanup;
 	if (!(config->no_focus = create_list())) goto cleanup;
 	if (!(config->input_configs = create_list())) goto cleanup;
+	if (!(config->seat_configs = create_list())) goto cleanup;
 	if (!(config->output_configs = create_list())) goto cleanup;
 
 	if (!(config->cmd_queue = create_list())) goto cleanup;
 
-	if (!(config->current_mode = malloc(sizeof(struct sway_mode)))) goto cleanup;
+	if (!(config->current_mode = malloc(sizeof(struct sway_mode))))
+		goto cleanup;
 	if (!(config->current_mode->name = malloc(sizeof("default")))) goto cleanup;
 	strcpy(config->current_mode->name, "default");
 	if (!(config->current_mode->bindings = create_list())) goto cleanup;
@@ -256,8 +258,9 @@ bool load_main_config(const char *file, bool is_active) {
 	bool success = true;
 	DIR *dir = opendir(SYSCONFDIR "/sway/security.d");
 	if (!dir) {
-		sway_log(L_ERROR, "%s does not exist, sway will have no security configuration"
-				" and will probably be broken", SYSCONFDIR "/sway/security.d");
+		sway_log(L_ERROR,
+			"%s does not exist, sway will have no security configuration"
+			" and will probably be broken", SYSCONFDIR "/sway/security.d");
 	} else {
 		list_t *secconfigs = create_list();
 		char *base = SYSCONFDIR "/sway/security.d/";
@@ -281,8 +284,12 @@ bool load_main_config(const char *file, bool is_active) {
 		list_qsort(secconfigs, qstrcmp);
 		for (int i = 0; i < secconfigs->length; ++i) {
 			char *_path = secconfigs->items[i];
-			if (stat(_path, &s) || s.st_uid != 0 || s.st_gid != 0 || (((s.st_mode & 0777) != 0644) && (s.st_mode & 0777) != 0444)) {
-				sway_log(L_ERROR, "Refusing to load %s - it must be owned by root and mode 644 or 444", _path);
+			if (stat(_path, &s) || s.st_uid != 0 || s.st_gid != 0 ||
+					(((s.st_mode & 0777) != 0644) &&
+					(s.st_mode & 0777) != 0444)) {
+				sway_log(L_ERROR,
+					"Refusing to load %s - it must be owned by root "
+					"and mode 644 or 444", _path);
 				success = false;
 			} else {
 				success = success && load_config(_path, config);
@@ -311,7 +318,8 @@ bool load_main_config(const char *file, bool is_active) {
 	return success;
 }
 
-static bool load_include_config(const char *path, const char *parent_dir, struct sway_config *config) {
+static bool load_include_config(const char *path, const char *parent_dir,
+		struct sway_config *config) {
 	// save parent config
 	const char *parent_config = config->current_config;
 
@@ -321,7 +329,8 @@ static bool load_include_config(const char *path, const char *parent_dir, struct
 		len = len + strlen(parent_dir) + 2;
 		full_path = malloc(len * sizeof(char));
 		if (!full_path) {
-			sway_log(L_ERROR, "Unable to allocate full path to included config");
+			sway_log(L_ERROR,
+				"Unable to allocate full path to included config");
 			return false;
 		}
 		snprintf(full_path, len, "%s/%s", parent_dir, path);
@@ -340,7 +349,9 @@ static bool load_include_config(const char *path, const char *parent_dir, struct
 	for (j = 0; j < config->config_chain->length; ++j) {
 		char *old_path = config->config_chain->items[j];
 		if (strcmp(real_path, old_path) == 0) {
-			sway_log(L_DEBUG, "%s already included once, won't be included again.", real_path);
+			sway_log(L_DEBUG,
+				"%s already included once, won't be included again.",
+				real_path);
 			free(real_path);
 			return false;
 		}
@@ -400,6 +411,7 @@ bool load_include_configs(const char *path, struct sway_config *config) {
 	return true;
 }
 
+
 bool read_config(FILE *file, struct sway_config *config) {
 	bool success = true;
 	enum cmd_status block = CMD_BLOCK_END;
@@ -427,8 +439,8 @@ bool read_config(FILE *file, struct sway_config *config) {
 		switch(res->status) {
 		case CMD_FAILURE:
 		case CMD_INVALID:
-			sway_log(L_ERROR, "Error on line %i '%s': %s (%s)", line_number, line,
-				res->error, config->current_config);
+			sway_log(L_ERROR, "Error on line %i '%s': %s (%s)", line_number,
+				line, res->error, config->current_config);
 			success = false;
 			break;
 
@@ -453,6 +465,14 @@ bool read_config(FILE *file, struct sway_config *config) {
 			}
 			break;
 
+		case CMD_BLOCK_SEAT:
+			if (block == CMD_BLOCK_END) {
+				block = CMD_BLOCK_SEAT;
+			} else {
+				sway_log(L_ERROR, "Invalid block '%s'", line);
+			}
+			break;
+
 		case CMD_BLOCK_BAR:
 			if (block == CMD_BLOCK_END) {
 				block = CMD_BLOCK_BAR;
@@ -503,8 +523,13 @@ bool read_config(FILE *file, struct sway_config *config) {
 
 			case CMD_BLOCK_INPUT:
 				sway_log(L_DEBUG, "End of input block");
-				// TODO: input
-				//current_input_config = NULL;
+				current_input_config = NULL;
+				block = CMD_BLOCK_END;
+				break;
+
+			case CMD_BLOCK_SEAT:
+				sway_log(L_DEBUG, "End of seat block");
+				current_seat_config = NULL;
 				block = CMD_BLOCK_END;
 				break;
 
@@ -569,7 +594,8 @@ char *do_var_replacement(char *str) {
 				char *newstr = malloc(strlen(str) - vnlen + vvlen + 1);
 				if (!newstr) {
 					sway_log(L_ERROR,
-							"Unable to allocate replacement during variable expansion");
+						"Unable to allocate replacement "
+						"during variable expansion");
 					break;
 				}
 				char *newptr = newstr;
diff --git a/sway/config/input.c b/sway/config/input.c
new file mode 100644
index 00000000..6f8d31f7
--- /dev/null
+++ b/sway/config/input.c
@@ -0,0 +1,105 @@
+#define _XOPEN_SOURCE 700
+#include <stdlib.h>
+#include <limits.h>
+#include <float.h>
+#include "sway/config.h"
+#include "log.h"
+
+struct input_config *new_input_config(const char* identifier) {
+	struct input_config *input = calloc(1, sizeof(struct input_config));
+	if (!input) {
+		sway_log(L_DEBUG, "Unable to allocate input config");
+		return NULL;
+	}
+	sway_log(L_DEBUG, "new_input_config(%s)", identifier);
+	if (!(input->identifier = strdup(identifier))) {
+		free(input);
+		sway_log(L_DEBUG, "Unable to allocate input config");
+		return NULL;
+	}
+
+	input->tap = INT_MIN;
+	input->drag_lock = INT_MIN;
+	input->dwt = INT_MIN;
+	input->send_events = INT_MIN;
+	input->click_method = INT_MIN;
+	input->middle_emulation = INT_MIN;
+	input->natural_scroll = INT_MIN;
+	input->accel_profile = INT_MIN;
+	input->pointer_accel = FLT_MIN;
+	input->scroll_method = INT_MIN;
+	input->left_handed = INT_MIN;
+
+	return input;
+}
+
+void merge_input_config(struct input_config *dst, struct input_config *src) {
+	if (src->identifier) {
+		free(dst->identifier);
+		dst->identifier = strdup(src->identifier);
+	}
+	if (src->accel_profile != INT_MIN) {
+		dst->accel_profile = src->accel_profile;
+	}
+	if (src->click_method != INT_MIN) {
+		dst->click_method = src->click_method;
+	}
+	if (src->drag_lock != INT_MIN) {
+		dst->drag_lock = src->drag_lock;
+	}
+	if (src->dwt != INT_MIN) {
+		dst->dwt = src->dwt;
+	}
+	if (src->middle_emulation != INT_MIN) {
+		dst->middle_emulation = src->middle_emulation;
+	}
+	if (src->natural_scroll != INT_MIN) {
+		dst->natural_scroll = src->natural_scroll;
+	}
+	if (src->pointer_accel != FLT_MIN) {
+		dst->pointer_accel = src->pointer_accel;
+	}
+	if (src->scroll_method != INT_MIN) {
+		dst->scroll_method = src->scroll_method;
+	}
+	if (src->send_events != INT_MIN) {
+		dst->send_events = src->send_events;
+	}
+	if (src->tap != INT_MIN) {
+		dst->tap = src->tap;
+	}
+	if (src->xkb_layout) {
+		free(dst->xkb_layout);
+		dst->xkb_layout = strdup(src->xkb_layout);
+	}
+	if (src->xkb_model) {
+		free(dst->xkb_model);
+		dst->xkb_model = strdup(src->xkb_model);
+	}
+	if (src->xkb_options) {
+		free(dst->xkb_options);
+		dst->xkb_options = strdup(src->xkb_options);
+	}
+	if (src->xkb_rules) {
+		free(dst->xkb_rules);
+		dst->xkb_rules = strdup(src->xkb_rules);
+	}
+	if (src->xkb_variant) {
+		free(dst->xkb_variant);
+		dst->xkb_variant = strdup(src->xkb_variant);
+	}
+}
+
+void free_input_config(struct input_config *ic) {
+	if (!ic) {
+		return;
+	}
+	free(ic->identifier);
+	free(ic);
+}
+
+int input_identifier_cmp(const void *item, const void *data) {
+	const struct input_config *ic = item;
+	const char *identifier = data;
+	return strcmp(ic->identifier, identifier);
+}
diff --git a/sway/config/seat.c b/sway/config/seat.c
new file mode 100644
index 00000000..113139e8
--- /dev/null
+++ b/sway/config/seat.c
@@ -0,0 +1,135 @@
+#define _XOPEN_SOURCE 700
+#include <stdlib.h>
+#include <string.h>
+#include "sway/config.h"
+#include "log.h"
+
+struct seat_config *new_seat_config(const char* name) {
+	struct seat_config *seat = calloc(1, sizeof(struct seat_config));
+	if (!seat) {
+		sway_log(L_DEBUG, "Unable to allocate seat config");
+		return NULL;
+	}
+
+	sway_log(L_DEBUG, "new_seat_config(%s)", name);
+	seat->name = strdup(name);
+	if (!sway_assert(seat->name, "could not allocate name for seat")) {
+		free(seat);
+		return NULL;
+	}
+
+	seat->fallback = -1;
+	seat->attachments = create_list();
+	if (!sway_assert(seat->attachments,
+				"could not allocate seat attachments list")) {
+		free(seat->name);
+		free(seat);
+		return NULL;
+	}
+
+	return seat;
+}
+
+struct seat_attachment_config *seat_attachment_config_new() {
+	struct seat_attachment_config *attachment =
+		calloc(1, sizeof(struct seat_attachment_config));
+	if (!attachment) {
+		sway_log(L_DEBUG, "cannot allocate attachment config");
+		return NULL;
+	}
+	return attachment;
+}
+
+static void seat_attachment_config_free(
+		struct seat_attachment_config *attachment) {
+	free(attachment->identifier);
+	free(attachment);
+	return;
+}
+
+static struct seat_attachment_config *seat_attachment_config_copy(
+		struct seat_attachment_config *attachment) {
+	struct seat_attachment_config *copy = seat_attachment_config_new();
+	if (!copy) {
+		return NULL;
+	}
+
+	copy->identifier = strdup(attachment->identifier);
+
+	return copy;
+}
+
+static void merge_seat_attachment_config(struct seat_attachment_config *dest,
+		struct seat_attachment_config *source) {
+	// nothing to merge yet, but there will be some day
+}
+
+void merge_seat_config(struct seat_config *dest, struct seat_config *source) {
+	if (source->name) {
+		free(dest->name);
+		dest->name = strdup(source->name);
+	}
+
+	if (source->fallback != -1) {
+		dest->fallback = source->fallback;
+	}
+
+	for (int i = 0; i < source->attachments->length; ++i) {
+		struct seat_attachment_config *source_attachment =
+			source->attachments->items[i];
+		bool found = false;
+		for (int j = 0; j < dest->attachments->length; ++j) {
+			struct seat_attachment_config *dest_attachment =
+				dest->attachments->items[j];
+			if (strcmp(source_attachment->identifier,
+						dest_attachment->identifier) == 0) {
+				merge_seat_attachment_config(dest_attachment,
+					source_attachment);
+				found = true;
+			}
+		}
+
+		if (!found) {
+			struct seat_attachment_config *copy =
+				seat_attachment_config_copy(source_attachment);
+			if (copy) {
+				list_add(dest->attachments, copy);
+			}
+		}
+	}
+}
+
+void free_seat_config(struct seat_config *seat) {
+	if (!seat) {
+		return;
+	}
+
+	free(seat->name);
+	for (int i = 0; i < seat->attachments->length; ++i) {
+		struct seat_attachment_config *attachment =
+			seat->attachments->items[i];
+		seat_attachment_config_free(attachment);
+	}
+
+	list_free(seat->attachments);
+	free(seat);
+}
+
+int seat_name_cmp(const void *item, const void *data) {
+	const struct seat_config *sc = item;
+	const char *name = data;
+	return strcmp(sc->name, name);
+}
+
+struct seat_attachment_config *seat_config_get_attachment(
+		struct seat_config *seat_config, char *identifier) {
+	for (int i = 0; i < seat_config->attachments->length; ++i) {
+		struct seat_attachment_config *attachment =
+			seat_config->attachments->items[i];
+		if (strcmp(attachment->identifier, identifier) == 0) {
+			return attachment;
+		}
+	}
+
+	return NULL;
+}
diff --git a/sway/desktop/output.c b/sway/desktop/output.c
index ad843b31..3b87c2e7 100644
--- a/sway/desktop/output.c
+++ b/sway/desktop/output.c
@@ -12,6 +12,8 @@
 #include "sway/output.h"
 #include "sway/server.h"
 #include "sway/view.h"
+#include "sway/input/input-manager.h"
+#include "sway/input/seat.h"
 
 static void output_frame_view(swayc_t *view, void *data) {
 	struct sway_output *output = data;
@@ -23,8 +25,8 @@ static void output_frame_view(swayc_t *view, void *data) {
 	}
 	// TODO
 	// - Deal with wlr_output_layout
-	int width = sway_view->width;
-	int height = sway_view->height;
+	int width = sway_view->surface->current->width;
+	int height = sway_view->surface->current->height;
 	int render_width = width * wlr_output->scale;
 	int render_height = height * wlr_output->scale;
 	double ox = view->x, oy = view->y;
@@ -38,19 +40,33 @@ static void output_frame_view(swayc_t *view, void *data) {
 	//		return;
 	//}
 
+	// if the shell specifies window geometry, make the top left corner of the
+	// window in the top left corner of the container to avoid arbitrarily
+	// sized gaps based on the attached buffer size
+	int window_offset_x = 0;
+	int window_offset_y = 0;
+
+	if (view->sway_view->type == SWAY_XDG_SHELL_V6_VIEW) {
+		window_offset_x = view->sway_view->wlr_xdg_surface_v6->geometry->x;
+		window_offset_y = view->sway_view->wlr_xdg_surface_v6->geometry->y;
+	}
+
 	// TODO
 	double rotation = 0;
 	float matrix[16];
 
 	float translate_origin[16];
 	wlr_matrix_translate(&translate_origin,
-		(int)ox + render_width / 2, (int)oy + render_height / 2, 0);
+		(int)ox + render_width / 2 - window_offset_x,
+		(int)oy + render_height / 2 - window_offset_y,
+		0);
 
 	float rotate[16];
 	wlr_matrix_rotate(&rotate, rotation);
 
 	float translate_center[16];
-	wlr_matrix_translate(&translate_center, -render_width / 2,
+	wlr_matrix_translate(&translate_center,
+		-render_width / 2,
 		-render_height / 2, 0);
 
 	float scale[16];
@@ -115,6 +131,8 @@ void output_add_notify(struct wl_listener *listener, void *data) {
 		return;
 	}
 
+	sway_input_manager_configure_xcursor(input_manager);
+
 	output->frame.notify = output_frame_notify;
 	wl_signal_add(&wlr_output->events.frame, &output->frame);
 }
diff --git a/sway/desktop/wl_shell.c b/sway/desktop/wl_shell.c
index 3f5a358a..e7150bf3 100644
--- a/sway/desktop/wl_shell.c
+++ b/sway/desktop/wl_shell.c
@@ -7,6 +7,8 @@
 #include "sway/layout.h"
 #include "sway/server.h"
 #include "sway/view.h"
+#include "sway/input/seat.h"
+#include "sway/input/input-manager.h"
 #include "log.h"
 
 static bool assert_wl_shell(struct sway_view *view) {
@@ -53,8 +55,6 @@ static void handle_commit(struct wl_listener *listener, void *data) {
 	struct sway_wl_shell_surface *sway_surface =
 		wl_container_of(listener, sway_surface, commit);
 	struct sway_view *view = sway_surface->view;
-	sway_log(L_DEBUG, "wl_shell surface commit %dx%d",
-			sway_surface->pending_width, sway_surface->pending_height);
 	// NOTE: We intentionally discard the view's desired width here
 	// TODO: Let floating views do whatever
 	view->width = sway_surface->pending_width;
@@ -126,4 +126,6 @@ void handle_wl_shell_surface(struct wl_listener *listener, void *data) {
 	sway_view->swayc = cont;
 
 	arrange_windows(cont->parent, -1, -1);
+
+	sway_input_manager_set_focus(input_manager, cont);
 }
diff --git a/sway/desktop/xdg_shell_v6.c b/sway/desktop/xdg_shell_v6.c
index 2435c256..015cc9d0 100644
--- a/sway/desktop/xdg_shell_v6.c
+++ b/sway/desktop/xdg_shell_v6.c
@@ -7,6 +7,8 @@
 #include "sway/layout.h"
 #include "sway/server.h"
 #include "sway/view.h"
+#include "sway/input/seat.h"
+#include "sway/input/input-manager.h"
 #include "log.h"
 
 static bool assert_xdg(struct sway_view *view) {
@@ -59,8 +61,6 @@ static void handle_commit(struct wl_listener *listener, void *data) {
 	struct sway_xdg_surface_v6 *sway_surface =
 		wl_container_of(listener, sway_surface, commit);
 	struct sway_view *view = sway_surface->view;
-	sway_log(L_DEBUG, "xdg surface commit %dx%d",
-			sway_surface->pending_width, sway_surface->pending_height);
 	// NOTE: We intentionally discard the view's desired width here
 	// TODO: Let floating views do whatever
 	view->width = sway_surface->pending_width;
@@ -132,4 +132,6 @@ void handle_xdg_shell_v6_surface(struct wl_listener *listener, void *data) {
 	sway_view->swayc = cont;
 
 	arrange_windows(cont->parent, -1, -1);
+
+	sway_input_manager_set_focus(input_manager, cont);
 }
diff --git a/sway/desktop/xwayland.c b/sway/desktop/xwayland.c
index e3799d2d..42e82c64 100644
--- a/sway/desktop/xwayland.c
+++ b/sway/desktop/xwayland.c
@@ -10,6 +10,8 @@
 #include "sway/server.h"
 #include "sway/view.h"
 #include "sway/output.h"
+#include "sway/input/seat.h"
+#include "sway/input/input-manager.h"
 #include "log.h"
 
  static bool assert_xwayland(struct sway_view *view) {
@@ -82,8 +84,6 @@ static void handle_commit(struct wl_listener *listener, void *data) {
 	struct sway_xwayland_surface *sway_surface =
 		wl_container_of(listener, sway_surface, commit);
 	struct sway_view *view = sway_surface->view;
-	sway_log(L_DEBUG, "xwayland surface commit %dx%d",
-		sway_surface->pending_width, sway_surface->pending_height);
 	// NOTE: We intentionally discard the view's desired width here
 	// TODO: Let floating views do whatever
 	view->width = sway_surface->pending_width;
@@ -171,4 +171,5 @@ void handle_xwayland_surface(struct wl_listener *listener, void *data) {
 	sway_view->swayc = cont;
 
 	arrange_windows(cont->parent, -1, -1);
+	sway_input_manager_set_focus(input_manager, cont);
 }
diff --git a/sway/input/cursor.c b/sway/input/cursor.c
new file mode 100644
index 00000000..3b5cfce5
--- /dev/null
+++ b/sway/input/cursor.c
@@ -0,0 +1,181 @@
+#define _XOPEN_SOURCE 700
+#ifdef __linux__
+#include <linux/input-event-codes.h>
+#elif __FreeBSD__
+#include <dev/evdev/input-event-codes.h>
+#endif
+#include <wlr/types/wlr_cursor.h>
+#include <wlr/types/wlr_xcursor_manager.h>
+#include "sway/input/cursor.h"
+#include "sway/view.h"
+#include "list.h"
+#include "log.h"
+
+static void cursor_update_position(struct sway_cursor *cursor) {
+	double x = cursor->cursor->x;
+	double y = cursor->cursor->y;
+
+	cursor->x = x;
+	cursor->y = y;
+}
+
+static void cursor_send_pointer_motion(struct sway_cursor *cursor,
+		uint32_t time) {
+	struct wlr_seat *seat = cursor->seat->wlr_seat;
+	struct wlr_surface *surface = NULL;
+	double sx, sy;
+	swayc_t *swayc =
+		swayc_at(&root_container, cursor->x, cursor->y, &surface, &sx, &sy);
+	if (swayc) {
+		wlr_seat_pointer_notify_enter(seat, surface, sx, sy);
+		wlr_seat_pointer_notify_motion(seat, time, sx, sy);
+	} else {
+		wlr_seat_pointer_clear_focus(seat);
+	}
+}
+
+static void handle_cursor_motion(struct wl_listener *listener, void *data) {
+	struct sway_cursor *cursor =
+		wl_container_of(listener, cursor, motion);
+	struct wlr_event_pointer_motion *event = data;
+	wlr_cursor_move(cursor->cursor, event->device,
+		event->delta_x, event->delta_y);
+	cursor_update_position(cursor);
+	cursor_send_pointer_motion(cursor, event->time_msec);
+}
+
+static void handle_cursor_motion_absolute(struct wl_listener *listener,
+		void *data) {
+	struct sway_cursor *cursor =
+		wl_container_of(listener, cursor, motion_absolute);
+	struct wlr_event_pointer_motion_absolute *event = data;
+	wlr_cursor_warp_absolute(cursor->cursor, event->device,
+		event->x_mm / event->width_mm, event->y_mm / event->height_mm);
+	cursor_update_position(cursor);
+	cursor_send_pointer_motion(cursor, event->time_msec);
+}
+
+static void handle_cursor_button(struct wl_listener *listener, void *data) {
+	struct sway_cursor *cursor =
+		wl_container_of(listener, cursor, button);
+	struct wlr_event_pointer_button *event = data;
+
+	if (event->button == BTN_LEFT) {
+		struct wlr_surface *surface = NULL;
+		double sx, sy;
+		swayc_t *swayc =
+			swayc_at(&root_container, cursor->x, cursor->y, &surface, &sx, &sy);
+
+		sway_seat_set_focus(cursor->seat, swayc);
+	}
+
+	wlr_seat_pointer_notify_button(cursor->seat->wlr_seat, event->time_msec,
+		event->button, event->state);
+}
+
+static void handle_cursor_axis(struct wl_listener *listener, void *data) {
+	struct sway_cursor *cursor =
+		wl_container_of(listener, cursor, axis);
+	struct wlr_event_pointer_axis *event = data;
+	wlr_seat_pointer_notify_axis(cursor->seat->wlr_seat, event->time_msec,
+		event->orientation, event->delta);
+}
+
+static void handle_touch_down(struct wl_listener *listener, void *data) {
+	struct sway_cursor *cursor =
+		wl_container_of(listener, cursor, touch_down);
+	struct wlr_event_touch_down *event = data;
+	sway_log(L_DEBUG, "TODO: handle touch down event: %p", event);
+}
+
+static void handle_touch_up(struct wl_listener *listener, void *data) {
+	struct sway_cursor *cursor =
+		wl_container_of(listener, cursor, touch_up);
+	struct wlr_event_touch_up *event = data;
+	sway_log(L_DEBUG, "TODO: handle touch up event: %p", event);
+}
+
+static void handle_touch_motion(struct wl_listener *listener, void *data) {
+	struct sway_cursor *cursor =
+		wl_container_of(listener, cursor, touch_motion);
+	struct wlr_event_touch_motion *event = data;
+	sway_log(L_DEBUG, "TODO: handle touch motion event: %p", event);
+}
+
+static void handle_tool_axis(struct wl_listener *listener, void *data) {
+	struct sway_cursor *cursor =
+		wl_container_of(listener, cursor, tool_axis);
+	struct wlr_event_tablet_tool_axis *event = data;
+	sway_log(L_DEBUG, "TODO: handle tool axis event: %p", event);
+}
+
+static void handle_tool_tip(struct wl_listener *listener, void *data) {
+	struct sway_cursor *cursor =
+		wl_container_of(listener, cursor, tool_tip);
+	struct wlr_event_tablet_tool_tip *event = data;
+	sway_log(L_DEBUG, "TODO: handle tool tip event: %p", event);
+}
+
+static void handle_request_set_cursor(struct wl_listener *listener,
+		void *data) {
+	struct sway_cursor *cursor =
+		wl_container_of(listener, cursor, request_set_cursor);
+	struct wlr_seat_pointer_request_set_cursor_event *event = data;
+	sway_log(L_DEBUG, "TODO: handle request set cursor event: %p", event);
+}
+
+struct sway_cursor *sway_cursor_create(struct sway_seat *seat) {
+	struct sway_cursor *cursor = calloc(1, sizeof(struct sway_cursor));
+	if (!sway_assert(cursor, "could not allocate sway cursor")) {
+		return NULL;
+	}
+
+	struct wlr_cursor *wlr_cursor = wlr_cursor_create();
+	if (!sway_assert(wlr_cursor, "could not allocate wlr cursor")) {
+		free(cursor);
+		return NULL;
+	}
+
+	cursor->seat = seat;
+	wlr_cursor_attach_output_layout(wlr_cursor,
+		root_container.sway_root->output_layout);
+
+	// input events
+	wl_signal_add(&wlr_cursor->events.motion, &cursor->motion);
+	cursor->motion.notify = handle_cursor_motion;
+
+	wl_signal_add(&wlr_cursor->events.motion_absolute,
+		&cursor->motion_absolute);
+	cursor->motion_absolute.notify = handle_cursor_motion_absolute;
+
+	wl_signal_add(&wlr_cursor->events.button, &cursor->button);
+	cursor->button.notify = handle_cursor_button;
+
+	wl_signal_add(&wlr_cursor->events.axis, &cursor->axis);
+	cursor->axis.notify = handle_cursor_axis;
+
+	wl_signal_add(&wlr_cursor->events.touch_down, &cursor->touch_down);
+	cursor->touch_down.notify = handle_touch_down;
+
+	wl_signal_add(&wlr_cursor->events.touch_up, &cursor->touch_up);
+	cursor->touch_up.notify = handle_touch_up;
+
+	wl_signal_add(&wlr_cursor->events.touch_motion,
+		&cursor->touch_motion);
+	cursor->touch_motion.notify = handle_touch_motion;
+
+	wl_signal_add(&wlr_cursor->events.tablet_tool_axis,
+		&cursor->tool_axis);
+	cursor->tool_axis.notify = handle_tool_axis;
+
+	wl_signal_add(&wlr_cursor->events.tablet_tool_tip, &cursor->tool_tip);
+	cursor->tool_tip.notify = handle_tool_tip;
+
+	wl_signal_add(&seat->wlr_seat->events.request_set_cursor,
+			&cursor->request_set_cursor);
+	cursor->request_set_cursor.notify = handle_request_set_cursor;
+
+	cursor->cursor = wlr_cursor;
+
+	return cursor;
+}
diff --git a/sway/input/input-manager.c b/sway/input/input-manager.c
new file mode 100644
index 00000000..4459c43b
--- /dev/null
+++ b/sway/input/input-manager.c
@@ -0,0 +1,293 @@
+#define _XOPEN_SOURCE 700
+#include <ctype.h>
+#include <float.h>
+#include <limits.h>
+#include <stdio.h>
+#include <string.h>
+#include <libinput.h>
+#include <math.h>
+#include "sway/config.h"
+#include "sway/input/input-manager.h"
+#include "sway/input/seat.h"
+#include "sway/server.h"
+#include "stringop.h"
+#include "list.h"
+#include "log.h"
+
+static const char *default_seat = "seat0";
+
+// TODO make me not global
+struct sway_input_manager *input_manager;
+
+struct input_config *current_input_config = NULL;
+struct seat_config *current_seat_config = NULL;
+
+static struct sway_seat *input_manager_get_seat(
+		struct sway_input_manager *input, const char *seat_name) {
+	struct sway_seat *seat = NULL;
+	wl_list_for_each(seat, &input->seats, link) {
+		if (strcmp(seat->wlr_seat->name, seat_name) == 0) {
+			return seat;
+		}
+	}
+
+	return sway_seat_create(input, seat_name);
+}
+
+static char *get_device_identifier(struct wlr_input_device *device) {
+	int vendor = device->vendor;
+	int product = device->product;
+	char *name = strdup(device->name);
+	name = strip_whitespace(name);
+
+	char *p = name;
+	for (; *p; ++p) {
+		if (*p == ' ') {
+			*p = '_';
+		}
+	}
+
+	const char *fmt = "%d:%d:%s";
+	int len = snprintf(NULL, 0, fmt, vendor, product, name) + 1;
+	char *identifier = malloc(len);
+	if (!identifier) {
+		sway_log(L_ERROR, "Unable to allocate unique input device name");
+		return NULL;
+	}
+
+	snprintf(identifier, len, fmt, vendor, product, name);
+	free(name);
+	return identifier;
+}
+
+static struct sway_input_device *input_sway_device_from_wlr(
+		struct sway_input_manager *input, struct wlr_input_device *device) {
+	struct sway_input_device *input_device = NULL;
+	wl_list_for_each(input_device, &input->devices, link) {
+		if (input_device->wlr_device == device) {
+			return input_device;
+		}
+	}
+	return NULL;
+}
+
+static bool input_has_seat_configuration(struct sway_input_manager *input) {
+	struct sway_seat *seat = NULL;
+	wl_list_for_each(seat, &input->seats, link) {
+		if (seat->config) {
+			return true;
+		}
+	}
+
+	return false;
+}
+
+static void input_add_notify(struct wl_listener *listener, void *data) {
+	struct sway_input_manager *input =
+		wl_container_of(listener, input, input_add);
+	struct wlr_input_device *device = data;
+
+	struct sway_input_device *input_device =
+		calloc(1, sizeof(struct sway_input_device));
+	if (!sway_assert(input_device, "could not allocate input device")) {
+		return;
+	}
+
+	input_device->wlr_device = device;
+	input_device->identifier = get_device_identifier(device);
+	wl_list_insert(&input->devices, &input_device->link);
+
+	sway_log(L_DEBUG, "adding device: '%s'",
+		input_device->identifier);
+
+	// find config
+	for (int i = 0; i < config->input_configs->length; ++i) {
+		struct input_config *input_config = config->input_configs->items[i];
+		if (strcmp(input_config->identifier, input_device->identifier) == 0) {
+			input_device->config = input_config;
+			break;
+		}
+	}
+
+	struct sway_seat *seat = NULL;
+	if (!input_has_seat_configuration(input)) {
+		sway_log(L_DEBUG, "no seat configuration, using default seat");
+		seat = input_manager_get_seat(input, default_seat);
+		sway_seat_add_device(seat, input_device);
+		return;
+	}
+
+	bool added = false;
+	wl_list_for_each(seat, &input->seats, link) {
+		bool has_attachment = seat->config &&
+			(seat_config_get_attachment(seat->config, input_device->identifier) ||
+			 seat_config_get_attachment(seat->config, "*"));
+
+		if (has_attachment) {
+			sway_seat_add_device(seat, input_device);
+			added = true;
+		}
+	}
+
+	if (!added) {
+		wl_list_for_each(seat, &input->seats, link) {
+			if (seat->config && seat->config->fallback == 1) {
+				sway_seat_add_device(seat, input_device);
+				added = true;
+			}
+		}
+	}
+
+	if (!added) {
+		sway_log(L_DEBUG,
+			"device '%s' is not configured on any seats",
+			input_device->identifier);
+	}
+}
+
+static void input_remove_notify(struct wl_listener *listener, void *data) {
+	struct sway_input_manager *input =
+		wl_container_of(listener, input, input_remove);
+	struct wlr_input_device *device = data;
+
+	struct sway_input_device *input_device =
+		input_sway_device_from_wlr(input, device);
+
+	if (!sway_assert(input_device, "could not find sway device")) {
+		return;
+	}
+
+	sway_log(L_DEBUG, "removing device: '%s'",
+		input_device->identifier);
+
+	struct sway_seat *seat = NULL;
+	wl_list_for_each(seat, &input->seats, link) {
+		sway_seat_remove_device(seat, input_device);
+	}
+
+	wl_list_remove(&input_device->link);
+	free(input_device->identifier);
+	free(input_device);
+}
+
+struct sway_input_manager *sway_input_manager_create(
+		struct sway_server *server) {
+	struct sway_input_manager *input =
+		calloc(1, sizeof(struct sway_input_manager));
+	if (!input) {
+		return NULL;
+	}
+	input->server = server;
+
+	wl_list_init(&input->devices);
+	wl_list_init(&input->seats);
+
+	// create the default seat
+	input_manager_get_seat(input, default_seat);
+
+	input->input_add.notify = input_add_notify;
+	wl_signal_add(&server->backend->events.input_add, &input->input_add);
+
+	input->input_remove.notify = input_remove_notify;
+	wl_signal_add(&server->backend->events.input_remove, &input->input_remove);
+
+	return input;
+}
+
+bool sway_input_manager_has_focus(struct sway_input_manager *input,
+		swayc_t *container) {
+	struct sway_seat *seat = NULL;
+	wl_list_for_each(seat, &input->seats, link) {
+		if (seat->focus == container) {
+			return true;
+		}
+	}
+
+	return false;
+}
+
+void sway_input_manager_set_focus(struct sway_input_manager *input,
+		swayc_t *container) {
+	struct sway_seat *seat ;
+	wl_list_for_each(seat, &input->seats, link) {
+		sway_seat_set_focus(seat, container);
+	}
+}
+
+void sway_input_manager_apply_input_config(struct sway_input_manager *input,
+		struct input_config *input_config) {
+	struct sway_input_device *input_device = NULL;
+	wl_list_for_each(input_device, &input->devices, link) {
+		if (strcmp(input_device->identifier, input_config->identifier) == 0) {
+			input_device->config = input_config;
+
+			struct sway_seat *seat = NULL;
+			wl_list_for_each(seat, &input->seats, link) {
+				sway_seat_configure_device(seat, input_device);
+			}
+		}
+	}
+}
+
+void sway_input_manager_apply_seat_config(struct sway_input_manager *input,
+		struct seat_config *seat_config) {
+	sway_log(L_DEBUG, "applying new seat config for seat %s",
+		seat_config->name);
+	struct sway_seat *seat = input_manager_get_seat(input, seat_config->name);
+	if (!seat) {
+		return;
+	}
+
+	sway_seat_set_config(seat, seat_config);
+
+	// for every device, try to add it to a seat and if no seat has it
+	// attached, add it to the fallback seats.
+	struct sway_input_device *input_device = NULL;
+	wl_list_for_each(input_device, &input->devices, link) {
+		list_t *seat_list = create_list();
+		struct sway_seat *seat = NULL;
+		wl_list_for_each(seat, &input->seats, link) {
+			if (!seat->config) {
+				continue;
+			}
+			if (seat_config_get_attachment(seat->config, "*") ||
+					seat_config_get_attachment(seat->config,
+						input_device->identifier)) {
+				list_add(seat_list, seat);
+			}
+		}
+
+		if (seat_list->length) {
+			wl_list_for_each(seat, &input->seats, link) {
+				bool attached = false;
+				for (int i = 0; i < seat_list->length; ++i) {
+					if (seat == seat_list->items[i]) {
+						attached = true;
+						break;
+					}
+				}
+				if (attached) {
+					sway_seat_add_device(seat, input_device);
+				} else {
+					sway_seat_remove_device(seat, input_device);
+				}
+			}
+		} else {
+			wl_list_for_each(seat, &input->seats, link) {
+				if (seat->config && seat->config->fallback == 1) {
+					sway_seat_add_device(seat, input_device);
+				} else {
+					sway_seat_remove_device(seat, input_device);
+				}
+			}
+		}
+		list_free(seat_list);
+	}
+}
+
+void sway_input_manager_configure_xcursor(struct sway_input_manager *input) {
+	struct sway_seat *seat = NULL;
+	wl_list_for_each(seat, &input->seats, link) {
+		sway_seat_configure_xcursor(seat);
+	}
+}
diff --git a/sway/input/input.c b/sway/input/input.c
deleted file mode 100644
index 02b4995e..00000000
--- a/sway/input/input.c
+++ /dev/null
@@ -1,77 +0,0 @@
-#define _XOPEN_SOURCE 700
-#include <ctype.h>
-#include <float.h>
-#include <limits.h>
-#include <stdio.h>
-#include <string.h>
-#include <libinput.h>
-#include "sway/config.h"
-#include "sway/input.h"
-#include "sway/server.h"
-#include "list.h"
-#include "log.h"
-
-struct input_config *current_input_config = NULL;
-
-struct sway_input *sway_input_create(struct sway_server *server) {
-	struct sway_input *input = calloc(1, sizeof(struct sway_input));
-	if (!input) {
-		return NULL;
-	}
-	return input;
-}
-
-struct input_config *new_input_config(const char* identifier) {
-	struct input_config *input = calloc(1, sizeof(struct input_config));
-	if (!input) {
-		sway_log(L_DEBUG, "Unable to allocate input config");
-		return NULL;
-	}
-	sway_log(L_DEBUG, "new_input_config(%s)", identifier);
-	if (!(input->identifier = strdup(identifier))) {
-		free(input);
-		sway_log(L_DEBUG, "Unable to allocate input config");
-		return NULL;
-	}
-
-	input->tap = INT_MIN;
-	input->drag_lock = INT_MIN;
-	input->dwt = INT_MIN;
-	input->send_events = INT_MIN;
-	input->click_method = INT_MIN;
-	input->middle_emulation = INT_MIN;
-	input->natural_scroll = INT_MIN;
-	input->accel_profile = INT_MIN;
-	input->pointer_accel = FLT_MIN;
-	input->scroll_method = INT_MIN;
-	input->left_handed = INT_MIN;
-
-	return input;
-}
-
-char *libinput_dev_unique_id(struct libinput_device *device) {
-	int vendor = libinput_device_get_id_vendor(device);
-	int product = libinput_device_get_id_product(device);
-	char *name = strdup(libinput_device_get_name(device));
-
-	char *p = name;
-	for (; *p; ++p) {
-		if (*p == ' ') {
-			*p = '_';
-		}
-	}
-
-	sway_log(L_DEBUG, "rewritten name %s", name);
-
-	int len = strlen(name) + sizeof(char) * 6;
-	char *identifier = malloc(len);
-	if (!identifier) {
-		sway_log(L_ERROR, "Unable to allocate unique input device name");
-		return NULL;
-	}
-
-	const char *fmt = "%d:%d:%s";
-	snprintf(identifier, len, fmt, vendor, product, name);
-	free(name);
-	return identifier;
-}
diff --git a/sway/input/keyboard.c b/sway/input/keyboard.c
new file mode 100644
index 00000000..724941d8
--- /dev/null
+++ b/sway/input/keyboard.c
@@ -0,0 +1,123 @@
+#include "sway/input/seat.h"
+#include "sway/input/keyboard.h"
+#include "sway/input/input-manager.h"
+#include "log.h"
+
+static void handle_keyboard_key(struct wl_listener *listener, void *data) {
+	struct sway_keyboard *keyboard =
+		wl_container_of(listener, keyboard, keyboard_key);
+	struct wlr_seat *wlr_seat = keyboard->seat_device->sway_seat->wlr_seat;
+	struct wlr_input_device *wlr_device =
+		keyboard->seat_device->input_device->wlr_device;
+	struct wlr_event_keyboard_key *event = data;
+	wlr_seat_set_keyboard(wlr_seat, wlr_device);
+	wlr_seat_keyboard_notify_key(wlr_seat, event->time_msec,
+		event->keycode, event->state);
+}
+
+static void handle_keyboard_modifiers(struct wl_listener *listener,
+		void *data) {
+	struct sway_keyboard *keyboard =
+		wl_container_of(listener, keyboard, keyboard_modifiers);
+	struct wlr_seat *wlr_seat = keyboard->seat_device->sway_seat->wlr_seat;
+	struct wlr_input_device *wlr_device =
+		keyboard->seat_device->input_device->wlr_device;
+	wlr_seat_set_keyboard(wlr_seat, wlr_device);
+	wlr_seat_keyboard_notify_modifiers(wlr_seat);
+}
+
+struct sway_keyboard *sway_keyboard_create(struct sway_seat *seat,
+		struct sway_seat_device *device) {
+	struct sway_keyboard *keyboard =
+		calloc(1, sizeof(struct sway_keyboard));
+	if (!sway_assert(keyboard, "could not allocate sway keyboard")) {
+		return NULL;
+	}
+
+	keyboard->seat_device = device;
+	device->keyboard = keyboard;
+
+	wl_list_init(&keyboard->keyboard_key.link);
+	wl_list_init(&keyboard->keyboard_modifiers.link);
+
+	return keyboard;
+}
+
+void sway_keyboard_configure(struct sway_keyboard *keyboard) {
+	struct xkb_rule_names rules;
+	memset(&rules, 0, sizeof(rules));
+	struct input_config *input_config =
+		keyboard->seat_device->input_device->config;
+	struct wlr_input_device *wlr_device =
+		keyboard->seat_device->input_device->wlr_device;
+
+	if (input_config && input_config->xkb_layout) {
+		rules.layout = input_config->xkb_layout;
+	} else {
+		rules.layout = getenv("XKB_DEFAULT_LAYOUT");
+	}
+	if (input_config && input_config->xkb_model) {
+		rules.model = input_config->xkb_model;
+	} else {
+		rules.model = getenv("XKB_DEFAULT_MODEL");
+	}
+
+	if (input_config && input_config->xkb_options) {
+		rules.options = input_config->xkb_options;
+	} else {
+		rules.options = getenv("XKB_DEFAULT_OPTIONS");
+	}
+
+	if (input_config && input_config->xkb_rules) {
+		rules.rules = input_config->xkb_rules;
+	} else {
+		rules.rules = getenv("XKB_DEFAULT_RULES");
+	}
+
+	if (input_config && input_config->xkb_variant) {
+		rules.variant = input_config->xkb_variant;
+	} else {
+		rules.variant = getenv("XKB_DEFAULT_VARIANT");
+	}
+
+	struct xkb_context *context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
+	if (!sway_assert(context, "cannot create XKB context")) {
+		return;
+	}
+
+	struct xkb_keymap *keymap =
+		xkb_keymap_new_from_names(context, &rules, XKB_KEYMAP_COMPILE_NO_FLAGS);
+
+	if (!keymap) {
+		sway_log(L_DEBUG, "cannot configure keyboard: keymap does not exist");
+		xkb_context_unref(context);
+		return;
+	}
+
+	xkb_keymap_unref(keyboard->keymap);
+	keyboard->keymap = keymap;
+	wlr_keyboard_set_keymap(wlr_device->keyboard, keyboard->keymap);
+
+	wlr_keyboard_set_repeat_info(wlr_device->keyboard, 25, 600);
+	xkb_context_unref(context);
+	struct wlr_seat *seat = keyboard->seat_device->sway_seat->wlr_seat;
+	wlr_seat_set_keyboard(seat, wlr_device);
+
+	wl_list_remove(&keyboard->keyboard_key.link);
+	wl_signal_add(&wlr_device->keyboard->events.key, &keyboard->keyboard_key);
+	keyboard->keyboard_key.notify = handle_keyboard_key;
+
+	wl_list_remove(&keyboard->keyboard_modifiers.link);
+	wl_signal_add( &wlr_device->keyboard->events.modifiers,
+		&keyboard->keyboard_modifiers);
+	keyboard->keyboard_modifiers.notify = handle_keyboard_modifiers;
+}
+
+void sway_keyboard_destroy(struct sway_keyboard *keyboard) {
+	if (!keyboard) {
+		return;
+	}
+	wl_list_remove(&keyboard->keyboard_key.link);
+	wl_list_remove(&keyboard->keyboard_modifiers.link);
+	free(keyboard);
+}
diff --git a/sway/input/seat.c b/sway/input/seat.c
new file mode 100644
index 00000000..fe90565a
--- /dev/null
+++ b/sway/input/seat.c
@@ -0,0 +1,253 @@
+#define _XOPEN_SOURCE 700
+#include <wlr/types/wlr_cursor.h>
+#include <wlr/types/wlr_xcursor_manager.h>
+#include "sway/input/seat.h"
+#include "sway/input/cursor.h"
+#include "sway/input/input-manager.h"
+#include "sway/input/keyboard.h"
+#include "sway/output.h"
+#include "sway/view.h"
+#include "log.h"
+
+static void seat_device_destroy(struct sway_seat_device *seat_device) {
+	if (!seat_device) {
+		return;
+	}
+
+	sway_keyboard_destroy(seat_device->keyboard);
+	wlr_cursor_detach_input_device(seat_device->sway_seat->cursor->cursor,
+		seat_device->input_device->wlr_device);
+	wl_list_remove(&seat_device->link);
+	free(seat_device);
+}
+
+struct sway_seat *sway_seat_create(struct sway_input_manager *input,
+		const char *seat_name) {
+	struct sway_seat *seat = calloc(1, sizeof(struct sway_seat));
+	if (!seat) {
+		return NULL;
+	}
+
+	seat->wlr_seat = wlr_seat_create(input->server->wl_display, seat_name);
+	if (!sway_assert(seat->wlr_seat, "could not allocate seat")) {
+		free(seat);
+		return NULL;
+	}
+
+	seat->cursor = sway_cursor_create(seat);
+	if (!seat->cursor) {
+		wlr_seat_destroy(seat->wlr_seat);
+		free(seat);
+		return NULL;
+	}
+
+	seat->input = input;
+	wl_list_init(&seat->devices);
+
+	wlr_seat_set_capabilities(seat->wlr_seat,
+		WL_SEAT_CAPABILITY_KEYBOARD |
+		WL_SEAT_CAPABILITY_POINTER |
+		WL_SEAT_CAPABILITY_TOUCH);
+
+	sway_seat_configure_xcursor(seat);
+
+	wl_list_insert(&input->seats, &seat->link);
+
+	return seat;
+}
+
+static void seat_configure_pointer(struct sway_seat *seat,
+		struct sway_seat_device *sway_device) {
+	// TODO pointer configuration
+	wlr_cursor_attach_input_device(seat->cursor->cursor,
+		sway_device->input_device->wlr_device);
+}
+
+static void seat_configure_keyboard(struct sway_seat *seat,
+		struct sway_seat_device *seat_device) {
+	if (!seat_device->keyboard) {
+		sway_keyboard_create(seat, seat_device);
+	}
+	sway_keyboard_configure(seat_device->keyboard);
+	wlr_seat_set_keyboard(seat->wlr_seat,
+		seat_device->input_device->wlr_device);
+	if (seat->focus) {
+		// force notify reenter to pick up the new configuration
+		wlr_seat_keyboard_clear_focus(seat->wlr_seat);
+		wlr_seat_keyboard_notify_enter(seat->wlr_seat, seat->focus->sway_view->surface);
+	}
+}
+
+static struct sway_seat_device *sway_seat_get_device(struct sway_seat *seat,
+		struct sway_input_device *input_device) {
+	struct sway_seat_device *seat_device = NULL;
+	wl_list_for_each(seat_device, &seat->devices, link) {
+		if (seat_device->input_device == input_device) {
+			return seat_device;
+		}
+	}
+
+	return NULL;
+}
+
+void sway_seat_configure_device(struct sway_seat *seat,
+		struct sway_input_device *input_device) {
+	struct sway_seat_device *seat_device =
+		sway_seat_get_device(seat, input_device);
+	if (!seat_device) {
+		return;
+	}
+
+	if (seat->config) {
+		seat_device->attachment_config =
+			seat_config_get_attachment(seat->config, input_device->identifier);
+	}
+
+	switch (input_device->wlr_device->type) {
+		case WLR_INPUT_DEVICE_POINTER:
+			seat_configure_pointer(seat, seat_device);
+			break;
+		case WLR_INPUT_DEVICE_KEYBOARD:
+			seat_configure_keyboard(seat, seat_device);
+			break;
+		case WLR_INPUT_DEVICE_TOUCH:
+		case WLR_INPUT_DEVICE_TABLET_PAD:
+		case WLR_INPUT_DEVICE_TABLET_TOOL:
+			sway_log(L_DEBUG, "TODO: configure other devices");
+			break;
+	}
+}
+
+void sway_seat_add_device(struct sway_seat *seat,
+		struct sway_input_device *input_device) {
+	if (sway_seat_get_device(seat, input_device)) {
+		sway_seat_configure_device(seat, input_device);
+		return;
+	}
+
+	struct sway_seat_device *seat_device =
+		calloc(1, sizeof(struct sway_seat_device));
+	if (!seat_device) {
+		sway_log(L_DEBUG, "could not allocate seat device");
+		return;
+	}
+
+	sway_log(L_DEBUG, "adding device %s to seat %s",
+		input_device->identifier, seat->wlr_seat->name);
+
+	seat_device->sway_seat = seat;
+	seat_device->input_device = input_device;
+	wl_list_insert(&seat->devices, &seat_device->link);
+
+	sway_seat_configure_device(seat, input_device);
+}
+
+void sway_seat_remove_device(struct sway_seat *seat,
+		struct sway_input_device *input_device) {
+	struct sway_seat_device *seat_device =
+		sway_seat_get_device(seat, input_device);
+
+	if (!seat_device) {
+		return;
+	}
+
+	sway_log(L_DEBUG, "removing device %s from seat %s",
+		input_device->identifier, seat->wlr_seat->name);
+
+	seat_device_destroy(seat_device);
+}
+
+void sway_seat_configure_xcursor(struct sway_seat *seat) {
+	// TODO configure theme and size
+	const char *cursor_theme = "default";
+
+	if (!seat->cursor->xcursor_manager) {
+		seat->cursor->xcursor_manager =
+			wlr_xcursor_manager_create("default", 24);
+		if (sway_assert(seat->cursor->xcursor_manager,
+					"Cannot create XCursor manager for theme %s",
+					cursor_theme)) {
+			return;
+		}
+	}
+
+	for (int i = 0; i < root_container.children->length; ++i) {
+		swayc_t *output_container = root_container.children->items[i];
+		struct wlr_output *output =
+			output_container->sway_output->wlr_output;
+		bool result =
+			wlr_xcursor_manager_load(seat->cursor->xcursor_manager,
+				output->scale);
+
+		sway_assert(!result,
+			"Cannot load xcursor theme for output '%s' with scale %f",
+			// TODO: Fractional scaling
+			output->name, (double)output->scale);
+	}
+
+	wlr_xcursor_manager_set_cursor_image(seat->cursor->xcursor_manager,
+		"left_ptr", seat->cursor->cursor);
+	wlr_cursor_warp(seat->cursor->cursor, NULL, seat->cursor->cursor->x,
+		seat->cursor->cursor->y);
+}
+
+static void handle_focus_destroy(struct wl_listener *listener, void *data) {
+	struct sway_seat *seat = wl_container_of(listener, seat, focus_destroy);
+	//swayc_t *container = data;
+
+	// TODO set new focus based on the state of the tree
+	sway_seat_set_focus(seat, NULL);
+}
+
+void sway_seat_set_focus(struct sway_seat *seat, swayc_t *container) {
+	swayc_t *last_focus = seat->focus;
+
+	if (last_focus == container) {
+		return;
+	}
+
+	if (last_focus) {
+		wl_list_remove(&seat->focus_destroy.link);
+	}
+
+	if (container) {
+		struct sway_view *view = container->sway_view;
+		view->iface.set_activated(view, true);
+		wl_signal_add(&container->events.destroy, &seat->focus_destroy);
+		seat->focus_destroy.notify = handle_focus_destroy;
+		wlr_seat_keyboard_notify_enter(seat->wlr_seat, view->surface);
+	}
+
+	seat->focus = container;
+
+	if (last_focus &&
+			!sway_input_manager_has_focus(seat->input, last_focus)) {
+		struct sway_view *view = last_focus->sway_view;
+		view->iface.set_activated(view, false);
+
+	}
+}
+
+void sway_seat_set_config(struct sway_seat *seat,
+		struct seat_config *seat_config) {
+	// clear configs
+	seat->config = NULL;
+
+	struct sway_seat_device *seat_device = NULL;
+	wl_list_for_each(seat_device, &seat->devices, link) {
+		seat_device->attachment_config = NULL;
+	}
+
+	if (!seat_config) {
+		return;
+	}
+
+	// add configs
+	seat->config = seat_config;
+
+	wl_list_for_each(seat_device, &seat->devices, link) {
+		seat_device->attachment_config =
+			seat_config_get_attachment(seat_config,
+				seat_device->input_device->identifier);
+	}
+}
diff --git a/sway/ipc-json.c b/sway/ipc-json.c
index 09a32c1b..bab9a201 100644
--- a/sway/ipc-json.c
+++ b/sway/ipc-json.c
@@ -141,3 +141,40 @@ json_object *ipc_json_describe_container_recursive(swayc_t *c) {
 
 	return object;
 }
+
+static const char *describe_device_type(struct sway_input_device *device) {
+	switch (device->wlr_device->type) {
+	case WLR_INPUT_DEVICE_POINTER:
+		return "pointer";
+	case WLR_INPUT_DEVICE_KEYBOARD:
+		return "keyboard";
+	case WLR_INPUT_DEVICE_TOUCH:
+		return "touch";
+	case WLR_INPUT_DEVICE_TABLET_TOOL:
+		return "tablet_tool";
+	case WLR_INPUT_DEVICE_TABLET_PAD:
+		return "tablet_pad";
+	}
+	return "unknown";
+}
+
+json_object *ipc_json_describe_input(struct sway_input_device *device) {
+	if (!(sway_assert(device, "Device must not be null"))) {
+		return NULL;
+	}
+
+	json_object *object = json_object_new_object();
+
+	json_object_object_add(object, "identifier",
+		json_object_new_string(device->identifier));
+	json_object_object_add(object, "name",
+		json_object_new_string(device->wlr_device->name));
+	json_object_object_add(object, "vendor",
+		json_object_new_int(device->wlr_device->vendor));
+	json_object_object_add(object, "product",
+		json_object_new_int(device->wlr_device->product));
+	json_object_object_add(object, "type",
+		json_object_new_string(describe_device_type(device)));
+
+	return object;
+}
diff --git a/sway/ipc-server.c b/sway/ipc-server.c
index b7cd2d76..046e40a8 100644
--- a/sway/ipc-server.c
+++ b/sway/ipc-server.c
@@ -20,6 +20,7 @@
 #include "sway/ipc-json.h"
 #include "sway/ipc-server.h"
 #include "sway/server.h"
+#include "sway/input/input-manager.h"
 #include "list.h"
 #include "log.h"
 
@@ -359,6 +360,19 @@ void ipc_client_handle_command(struct ipc_client *client) {
 		goto exit_cleanup;
 	}
 
+	case IPC_GET_INPUTS:
+	{
+		json_object *inputs = json_object_new_array();
+		struct sway_input_device *device = NULL;
+		wl_list_for_each(device, &input_manager->devices, link) {
+			json_object_array_add(inputs, ipc_json_describe_input(device));
+		}
+		const char *json_string = json_object_to_json_string(inputs);
+		ipc_send_reply(client, json_string, (uint32_t)strlen(json_string));
+		json_object_put(inputs); // free
+		goto exit_cleanup;
+	}
+
 	case IPC_GET_TREE:
 	{
 		json_object *tree =
diff --git a/sway/main.c b/sway/main.c
index 8952f997..bd69395a 100644
--- a/sway/main.c
+++ b/sway/main.c
@@ -158,6 +158,7 @@ static void log_distro() {
 }
 
 static void log_kernel() {
+	return;
 	FILE *f = popen("uname -a", "r");
 	if (!f) {
 		sway_log(L_INFO, "Unable to determine kernel version");
@@ -381,11 +382,12 @@ int main(int argc, char **argv) {
 
 	sway_log(L_INFO, "Starting sway version " SWAY_VERSION "\n");
 
+	init_layout();
+
 	if (!server_init(&server)) {
 		return 1;
 	}
 
-	init_layout();
 	ipc_init(&server);
 	log_env();
 
diff --git a/sway/meson.build b/sway/meson.build
index e16691a8..fee2ddd2 100644
--- a/sway/meson.build
+++ b/sway/meson.build
@@ -2,13 +2,39 @@ sway_sources = files(
 	'main.c',
 	'server.c',
 	'commands.c',
+	'input/input-manager.c',
+	'input/seat.c',
+	'input/cursor.c',
+	'input/keyboard.c',
 	'commands/exit.c',
 	'commands/exec.c',
 	'commands/exec_always.c',
 	'commands/include.c',
+	'commands/input.c',
+	'commands/seat.c',
+	'commands/seat/attach.c',
+	'commands/seat/fallback.c',
+	'commands/input/accel_profile.c',
+	'commands/input/click_method.c',
+	'commands/input/drag_lock.c',
+	'commands/input/dwt.c',
+	'commands/input/events.c',
+	'commands/input/left_handed.c',
+	'commands/input/middle_emulation.c',
+	'commands/input/natural_scroll.c',
+	'commands/input/pointer_accel.c',
+	'commands/input/scroll_method.c',
+	'commands/input/tap.c',
+	'commands/input/xkb_layout.c',
+	'commands/input/xkb_model.c',
+	'commands/input/xkb_options.c',
+	'commands/input/xkb_rules.c',
+	'commands/input/xkb_variant.c',
 	'commands/output.c',
 	'config.c',
 	'config/output.c',
+	'config/seat.c',
+	'config/input.c',
 	'ipc-json.c',
 	'ipc-server.c',
 	'desktop/output.c',
@@ -28,6 +54,8 @@ sway_deps = [
 	wlroots,
 	libcap,
 	math,
+	libinput,
+	xkbcommon,
 ]
 
 executable(
diff --git a/sway/server.c b/sway/server.c
index 024d8429..32c8f03c 100644
--- a/sway/server.c
+++ b/sway/server.c
@@ -11,6 +11,7 @@
 // TODO WLR: make Xwayland optional
 #include <wlr/xwayland.h>
 #include "sway/server.h"
+#include "sway/input/input-manager.h"
 #include "log.h"
 
 bool server_init(struct sway_server *server) {
@@ -58,6 +59,9 @@ bool server_init(struct sway_server *server) {
 		wlr_backend_destroy(server->backend);
 		return false;
 	}
+
+	input_manager = sway_input_manager_create(server);
+
 	return true;
 }
 
diff --git a/sway/sway-input.5.txt b/sway/sway-input.5.txt
index f0c8f87c..0603616b 100644
--- a/sway/sway-input.5.txt
+++ b/sway/sway-input.5.txt
@@ -1,5 +1,5 @@
 /////
-vim:set ts=4 sw=4 tw=82 noet:
+vim:set ft=asciidoc ts=4 sw=4 tw=82 noet:
 /////
 sway-input (5)
 ==============
@@ -11,12 +11,37 @@ sway-input - input configuration file and commands
 Description
 -----------
 
-Sway allows for configuration of libinput devices within the sway configuration file.
+Sway allows for configuration of devices within the sway configuration file.
 sway-input commands must be used inside an _input { }_ block in the config.
 To obtain a list of available device identifiers, run **swaymsg -t get_inputs**.
 
-Commands
---------
+Input Commands
+--------------
+
+Keyboard Configuration
+~~~~~~~~~~~~~~~~~~~~~~
+
+For more information on these xkb configuration options, see
+**xkeyboard-config**(7).
+
+**input** <identifier> xkb_layout <layout_name>::
+	Sets the layout of the keyboard like _us_ or _de_.
+
+**input** <identifier> xkb_model <model_name>::
+	Sets the model of the keyboard. This has an influence for some extra keys your
+	keyboard might have.
+
+**input** <identifier> xkb_options <options>::
+	Sets extra xkb configuration options for the keyboard.
+
+**input** <identifier> xkb_rules <rules>::
+	Sets files of rules to be used for keyboard mapping composition.
+
+**input** <identifier> xkb_variant <variant>::
+	Sets the variant of the keyboard like _dvorak_ or _colemak_.
+
+Libinput Configuration
+~~~~~~~~~~~~~~~~~~~~~~
 
 **input** <identifier> accel_profile <adaptive|flat>::
 	Sets the pointer acceleration profile for the specified input device.
@@ -53,6 +78,27 @@ Commands
 **input** <identifier> tap <enabled|disabled>::
 	Enables or disables tap for specified input device.
 
+Seat Configuration
+------------------
+
+Configure options for multiseat mode. sway-seat commands must be used inside a
+_seat { }_ block in the config.
+
+A _seat_ is a collection of input devices that act independently of each other.
+Seats are identified by name and the default seat is _seat0_ if no seats are
+configured. Each seat has an independent keyboard focus and a separate cursor that
+is controlled by the pointer devices of the seat. This is useful for multiple
+people using the desktop at the same time with their own devices (each sitting in
+their own "seat").
+
+**seat** <name> attach <input_identifier>::
+	Attach an input device to this seat by its input identifier. A special value
+	of _*_ will attach all devices to the seat.
+
+**seat** <name> fallback <true|false>::
+	Set this seat as the fallback seat. A fallback seat will attach any device not
+	explicitly attached to another seat (similar to a "default" seat).
+
 See Also
 --------
 
diff --git a/sway/sway.1.txt b/sway/sway.1.txt
index 14ab9f49..17fc13da 100644
--- a/sway/sway.1.txt
+++ b/sway/sway.1.txt
@@ -1,5 +1,5 @@
 /////
-vim:set ts=4 sw=4 tw=82 noet:
+vim:set ft=asciidoc ts=4 sw=4 tw=82 noet:
 /////
 :quotes.~:
 
@@ -93,27 +93,6 @@ The following environment variables have an effect on sway:
 *SWAYSOCK*::
 	Specifies the path to the sway IPC socket.
 
-*WLC_DRM_DEVICE*::
-	Specifies the device to use in DRM mode.
-
-*WLC_SHM*::
-	Set 1 to force EGL clients to use shared memory.
-
-*WLC_OUTPUTS*::
-	Number of fake outputs to use when running in X11 mode.
-
-*WLC_XWAYLAND*::
-	Set to 0 to disable Xwayland support.
-
-*WLC_LIBINPUT*::
-	Set to 1 to force libinput (even in X11 mode).
-
-*WLC_REPEAT_DELAY*::
-	Configures the keyboard repeat delay.
-
-*WLC_REPEAT_RATE*::
-	Configures the keyboard repeat rate.
-
 *XKB_DEFAULT_RULES*, *XKB_DEFAULT_MODEL*, *XKB_DEFAULT_LAYOUT*, *XKB_DEFAULT_VARIANT*, *XKB_DEFAULT_OPTIONS*::
 	Configures the xkb keyboard settings. See xkeyboard-config(7).
 
diff --git a/sway/sway.5.txt b/sway/sway.5.txt
index 2a4ef205..afd3c9fa 100644
--- a/sway/sway.5.txt
+++ b/sway/sway.5.txt
@@ -312,7 +312,7 @@ The default colors are:
 **hide_edge_borders** <none|vertical|horizontal|both|smart>::
 	Hide window borders adjacent to the screen edges. Default is _none_.
 
-**input** <input device> <block of commands>::
+**input** <input_device> <block of commands>::
 	Append _{_ to this command, the following lines will be commands to configure
 	the named input device, and _}_ on its own line will close the block.
 	+
@@ -320,6 +320,11 @@ The default colors are:
 	+
 	See **sway-input**(5) for details.
 
+**seat** <seat_name> <block of commands>::
+	Append _{_ to this command, the following lines will be commands to configure
+	the named seat, and _}_ on its own line will close the block.
+	See **sway-input**(5) for details.
+
 **kill**::
 	Kills (force-closes) the currently-focused container and all of its children.
 
diff --git a/sway/tree/container.c b/sway/tree/container.c
index 5df10bcb..6f2a3abf 100644
--- a/sway/tree/container.c
+++ b/sway/tree/container.c
@@ -4,6 +4,7 @@
 #include <string.h>
 #include <strings.h>
 #include <wlr/types/wlr_output_layout.h>
+#include <wlr/types/wlr_wl_shell.h>
 #include "sway/config.h"
 #include "sway/container.h"
 #include "sway/layout.h"
@@ -42,6 +43,9 @@ static swayc_t *new_swayc(enum swayc_types type) {
 	if (type != C_VIEW) {
 		c->children = create_list();
 	}
+
+	wl_signal_init(&c->events.destroy);
+
 	return c;
 }
 
@@ -140,6 +144,9 @@ static void free_swayc(swayc_t *cont) {
 	if (!sway_assert(cont, "free_swayc passed NULL")) {
 		return;
 	}
+
+	wl_signal_emit(&cont->events.destroy, cont);
+
 	if (cont->children) {
 		// remove children until there are no more, free_swayc calls
 		// remove_child, which removes child from this container
@@ -218,3 +225,63 @@ swayc_t *swayc_parent_by_type(swayc_t *container, enum swayc_types type) {
 	} while (container && container->type != type);
 	return container;
 }
+
+swayc_t *swayc_at(swayc_t *parent, double lx, double ly,
+		struct wlr_surface **surface, double *sx, double *sy) {
+	list_t *queue = create_list();
+	list_add(queue, parent);
+
+	swayc_t *swayc = NULL;
+	while (queue->length) {
+		swayc = queue->items[0];
+		list_del(queue, 0);
+		if (swayc->type == C_VIEW) {
+			struct sway_view *sview = swayc->sway_view;
+			swayc_t *soutput = swayc_parent_by_type(swayc, C_OUTPUT);
+			struct wlr_box *output_box =
+				wlr_output_layout_get_box(
+					root_container.sway_root->output_layout,
+					soutput->sway_output->wlr_output);
+			double ox = lx - output_box->x;
+			double oy = ly - output_box->y;
+			double view_sx = ox - swayc->x;
+			double view_sy = oy - swayc->y;
+			int width = swayc->sway_view->surface->current->width;
+			int height = swayc->sway_view->surface->current->height;
+
+			// TODO popups and subsurfaces
+			switch (sview->type) {
+				case SWAY_WL_SHELL_VIEW:
+					break;
+				case SWAY_XDG_SHELL_V6_VIEW:
+					// the top left corner of the sway container is the
+					// coordinate of the top left corner of the window geometry
+					view_sx += sview->wlr_xdg_surface_v6->geometry->x;
+					view_sy += sview->wlr_xdg_surface_v6->geometry->y;
+					break;
+				case SWAY_XWAYLAND_VIEW:
+					break;
+				default:
+					break;
+			}
+
+			if (view_sx > 0 && view_sx < width &&
+					view_sy > 0 && view_sy < height &&
+					pixman_region32_contains_point(
+						&sview->surface->current->input,
+						view_sx, view_sy, NULL)) {
+				*sx = view_sx;
+				*sy = view_sy;
+				*surface = swayc->sway_view->surface;
+				list_free(queue);
+				return swayc;
+			}
+		} else {
+			list_cat(queue, swayc->children);
+		}
+	}
+
+	list_free(queue);
+
+	return NULL;
+}
diff --git a/swaymsg/main.c b/swaymsg/main.c
index 2f9cfb14..b431872a 100644
--- a/swaymsg/main.c
+++ b/swaymsg/main.c
@@ -61,55 +61,49 @@ static void pretty_print_workspace(json_object *w) {
 	);
 }
 
-static void pretty_print_input(json_object *i) {
-	json_object *id, *name, *size, *caps;
-	json_object_object_get_ex(i, "identifier", &id);
-	json_object_object_get_ex(i, "name", &name);
-	json_object_object_get_ex(i, "size", &size);
-	json_object_object_get_ex(i, "capabilities", &caps);
-
-	printf( "Input device %s\n  Type: ", json_object_get_string(name));
-
+static const char *pretty_type_name(const char *name) {
+	// TODO these constants probably belong in the common lib
 	struct {
 		const char *a;
 		const char *b;
-	} cap_names[] = {
+	} type_names[] = {
 		{ "keyboard", "Keyboard" },
 		{ "pointer", "Mouse" },
-		{ "touch", "Touch" },
-		{ "tablet_tool", "Tablet tool" },
 		{ "tablet_pad", "Tablet pad" },
-		{ "gesture", "Gesture" },
-		{ "switch", "Switch" },
+		{ "tablet_tool", "Tablet tool" },
+		{ "touch", "Touch" },
 	};
 
-	size_t len = json_object_array_length(caps);
-	if (len == 0) {
-		printf("Unknown");
+	for (size_t i = 0; i < sizeof(type_names) / sizeof(type_names[0]); ++i) {
+		if (strcmp(type_names[i].a, name) == 0) {
+			return type_names[i].b;
+		}
 	}
 
-	json_object *cap;
-	for (size_t i = 0; i < len; ++i) {
-		cap = json_object_array_get_idx(caps, i);
-		const char *cap_s = json_object_get_string(cap);
-		const char *_name = NULL;
-		for (size_t j = 0; j < sizeof(cap_names) / sizeof(cap_names[0]); ++i) {
-			if (strcmp(cap_names[i].a, cap_s) == 0) {
-				_name = cap_names[i].b;
-				break;
-			}
-		}
-		printf("%s%s", _name ? _name : cap_s, len > 1 && i != len - 1 ? ", " : "");
-	}
-	printf("\n  Sway ID: %s\n", json_object_get_string(id));
-	if (size) {
-		json_object *width, *height;
-		json_object_object_get_ex(size, "width", &width);
-		json_object_object_get_ex(size, "height", &height);
-		printf("  Size: %lfmm x %lfmm\n",
-				json_object_get_double(width), json_object_get_double(height));
-	}
-	printf("\n");
+	return name;
+}
+
+static void pretty_print_input(json_object *i) {
+	json_object *id, *name, *type, *product, *vendor;
+	json_object_object_get_ex(i, "identifier", &id);
+	json_object_object_get_ex(i, "name", &name);
+	json_object_object_get_ex(i, "type", &type);
+	json_object_object_get_ex(i, "product", &product);
+	json_object_object_get_ex(i, "vendor", &vendor);
+
+	const char *fmt =
+		"Input device: %s\n"
+		"  Type: %s\n"
+		"  Identifier: %s\n"
+		"  Product ID: %d\n"
+		"  Vendor ID: %d\n\n";
+
+
+	printf(fmt, json_object_get_string(name),
+		pretty_type_name(json_object_get_string(type)),
+		json_object_get_string(id),
+		json_object_get_int(product),
+		json_object_get_int(vendor));
 }
 
 static void pretty_print_output(json_object *o) {
@@ -314,9 +308,11 @@ int main(int argc, char **argv) {
 	}
 	free(cmdtype);
 
-	char *command = strdup("");
+	char *command = NULL;
 	if (optind < argc) {
 		command = join_args(argv + optind, argc - optind);
+	} else {
+		command = strdup("");
 	}
 
 	int ret = 0;
@@ -341,7 +337,7 @@ int main(int argc, char **argv) {
 			} else {
 				pretty_print(type, obj);
 			}
-			free(obj);
+			json_object_put(obj);
 		}
 	}
 	close(socketfd);