diff --git a/README.de.md b/README.de.md
index 206a1040..a872e888 100644
--- a/README.de.md
+++ b/README.de.md
@@ -64,8 +64,6 @@ Abhängigkeiten:
 * cairo
 * gdk-pixbuf2 *
 * pam **
-* imagemagick (erforderlich für Bildaufnahme mit swaygrab)
-* ffmpeg (erforderlich für Videoaufnahme swaygrab)
 * [scdoc](https://git.sr.ht/~sircmpwn/scdoc) (erforderlich für man pages)
 
 _\*Nur erforderlich für swaybar, swaybg, und swaylock_
diff --git a/README.el.md b/README.el.md
index 5c70beff..6887fe8e 100644
--- a/README.el.md
+++ b/README.el.md
@@ -57,8 +57,6 @@ To username μου στο Freenode είναι kon14 και θα με βρείτ
 * cairo
 * gdk-pixbuf2 *
 * pam **
-* imagemagick (αναγκαίο για καταγραφή εικόνας μέσω του swaygrab)
-* ffmpeg (αναγκαίο για καταγραφή video μέσω του swaygrab)
 * [scdoc](https://git.sr.ht/~sircmpwn/scdoc) (required for man pages)
 
 _\*Απαιτείται μόνο για swaybar, swaybg, and swaylock_
diff --git a/README.fr.md b/README.fr.md
index 0d2573f9..6ea4d14c 100644
--- a/README.fr.md
+++ b/README.fr.md
@@ -59,8 +59,6 @@ Installez les dépendances :
 * cairo
 * gdk-pixbuf2 *
 * pam **
-* imagemagick (requis pour la capture d'image avec swaygrab)
-* ffmpeg (requis pour la capture vidéo avec swaygrab)
 * [scdoc](https://git.sr.ht/~sircmpwn/scdoc) (requis pour les pages man)
 
 _\*Uniquement requis pour swaybar, swaybg, and swaylock_
diff --git a/README.it.md b/README.it.md
index 0d81ea54..3b1b1ebc 100644
--- a/README.it.md
+++ b/README.it.md
@@ -60,8 +60,6 @@ Installa queste dipendenze:
 * cairo
 * gdk-pixbuf2 *
 * pam **
-* imagemagick (richiesto per catturare immagini con swaygrab)
-* ffmpeg (rrichiesto per catturare video con swaygrab)
 * [scdoc](https://git.sr.ht/~sircmpwn/scdoc) (rrichiesto per man pages)
 
 _\*Richiesto solo per swaybar, swaybg, e swaylock_
diff --git a/README.ja.md b/README.ja.md
index 476d7472..7b437966 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -50,8 +50,6 @@ Swayは沢山のディストリビューションで提供されています。"
 * cairo
 * gdk-pixbuf2 *
 * pam **
-* imagemagick (swaygrabでスクリーンショットを撮るのに必要です)
-* ffmpeg (swaygrabで画面を録画するのに必要です)
 * [scdoc](https://git.sr.ht/~sircmpwn/scdoc) (manで必要です)
 
 _\*swaybar,swaybg,swaylockでのみ必要です_
diff --git a/README.md b/README.md
index 49140fb8..ce753265 100644
--- a/README.md
+++ b/README.md
@@ -58,8 +58,6 @@ Install dependencies:
 * gdk-pixbuf2 *
 * pam **
 * dbus >= 1.10 ***
-* imagemagick (required for image capture with swaygrab)
-* ffmpeg (required for video capture with swaygrab)
 * [scdoc](https://git.sr.ht/~sircmpwn/scdoc) (required for man pages)
 
 _\*Only required for swaybar, swaybg, and swaylock_
diff --git a/README.pt.md b/README.pt.md
index d1ef245f..9089c8c6 100644
--- a/README.pt.md
+++ b/README.pt.md
@@ -66,8 +66,6 @@ Antes de iniciar a compilação, instale as dependências:
 * cairo
 * gdk-pixbuf2 *
 * pam **
-* imagemagick (capturar imagem com o swaygrab)
-* ffmpeg (capturar vídeo com o swaygrab)
 * [scdoc](https://git.sr.ht/~sircmpwn/scdoc) (man pages)
 
 _\*Dependência apenas de swaybar, swaybg, e swaylock_
diff --git a/README.ru.md b/README.ru.md
index 3b3de19a..68675db3 100644
--- a/README.ru.md
+++ b/README.ru.md
@@ -62,8 +62,6 @@ Sway доступен во многих дистрибутивах и наход
 * gdk-pixbuf2 *
 * pam **
 * dbus >= 1.10 ***
-* imagemagick (требуется для захвата изображений через swaygrab)
-* ffmpeg (требуется для захвата видео через swaygrab)
 * [scdoc](https://git.sr.ht/~sircmpwn/scdoc) (required for man pages)
 
 _\*Требуется только для swaybar, swaybg и swaylock_
diff --git a/README.uk.md b/README.uk.md
index 55698487..c31a3ea9 100644
--- a/README.uk.md
+++ b/README.uk.md
@@ -66,8 +66,6 @@ Sway доступний у багатьох дистрибутивах Linux (а
 * cairo
 * gdk-pixbuf2 *
 * pam **
-* imagemagick (для захоплення зображень за допомогою swaygrab)
-* ffmpeg (для захоплення відео за допомогою swaygrab)
 * [scdoc](https://git.sr.ht/~sircmpwn/scdoc) (required for man pages)
 
 _\*Лише для swaybar, swaybg та swaylock_
diff --git a/common/util.c b/common/util.c
index e8a88772..467aa4b5 100644
--- a/common/util.c
+++ b/common/util.c
@@ -123,6 +123,22 @@ uint32_t parse_color(const char *color) {
 	return res;
 }
 
+bool parse_boolean(const char *boolean, bool current) {
+	if (strcasecmp(boolean, "1") == 0
+			|| strcasecmp(boolean, "yes") == 0
+			|| strcasecmp(boolean, "on") == 0
+			|| strcasecmp(boolean, "true") == 0
+			|| strcasecmp(boolean, "enable") == 0
+			|| strcasecmp(boolean, "enabled") == 0
+			|| strcasecmp(boolean, "active") == 0) {
+		return true;
+	} else if (strcasecmp(boolean, "toggle") == 0) {
+		return !current;
+	}
+	// All other values are false to match i3
+	return false;
+}
+
 char* resolve_path(const char* path) {
 	struct stat sb;
 	ssize_t r;
diff --git a/completions/bash/sway b/completions/bash/sway
new file mode 100644
index 00000000..edd752cd
--- /dev/null
+++ b/completions/bash/sway
@@ -0,0 +1,46 @@
+# sway(1) completion
+
+_sway()
+{
+  local cur prev
+  _get_comp_words_by_ref cur prev
+
+  short=(
+    -h
+    -c
+    -C
+    -d
+    -v
+    -V
+  )
+
+  long=(
+    --help
+    --config
+    --validate
+    --debug
+    --version
+    --verbose
+    --get-socketpath
+  )
+
+  case $prev in
+    -c|--config)
+      _filedir
+      return
+      ;;
+  esac
+
+  if [[ $cur == --* ]]; then
+    COMPREPLY=($(compgen -W "${long[*]}" -- "$cur"))
+  elif [[ $cur == -* ]]; then
+    COMPREPLY=($(compgen -W "${short[*]}" -- "$cur"))
+    COMPREPLY+=($(compgen -W "${long[*]}" -- "$cur"))
+  else
+    COMPREPLY=($(compgen -W "${short[*]}" -- "$cur"))
+    COMPREPLY+=($(compgen -W "${long[*]}" -- "$cur"))
+    COMPREPLY+=($(compgen -c -- "$cur"))
+  fi
+
+} &&
+complete -F _sway sway
diff --git a/completions/bash/swayidle b/completions/bash/swayidle
new file mode 100644
index 00000000..a0cdc8b2
--- /dev/null
+++ b/completions/bash/swayidle
@@ -0,0 +1,48 @@
+# swaymsg(1) completion
+
+_swayidle()
+{
+  local cur prev
+  _get_comp_words_by_ref -n : cur prev
+  local prev2=${COMP_WORDS[COMP_CWORD-2]}
+  local prev3=${COMP_WORDS[COMP_CWORD-3]}
+
+  events=(
+    'timeout'
+    'before-sleep'
+  )
+
+  short=(
+    -h
+    -d
+  )
+
+  if [ "$prev" = timeout ]; then
+    # timeout <timeout>
+    return
+  elif [ "$prev2" = timeout ]; then
+    # timeout <timeout> <timeout command>
+    COMPREPLY=($(compgen -c -- "$cur"))
+    return
+  elif [ "$prev3" = timeout ]; then
+    # timeout <timeout> <timeout command> [resume <resume command>]
+    COMPREPLY=(resume)
+    # optional argument; no return here as user may skip 'resume'
+  fi
+
+  case "$prev" in
+    resume)
+      COMPREPLY=($(compgen -c -- "$cur"))
+      return
+      ;;
+    before-sleep)
+      COMPREPLY=($(compgen -c -- "$cur"))
+      return
+      ;;
+  esac
+
+  COMPREPLY+=($(compgen -W "${events[*]}" -- "$cur"))
+  COMPREPLY+=($(compgen -W "${short[*]}" -- "$cur"))
+
+} &&
+complete -F _swayidle swayidle
diff --git a/completions/bash/swaylock b/completions/bash/swaylock
new file mode 100644
index 00000000..33925480
--- /dev/null
+++ b/completions/bash/swaylock
@@ -0,0 +1,66 @@
+# swaylock(1) completion
+
+_swaylock()
+{
+  local cur prev
+  _get_comp_words_by_ref -n : cur prev
+
+  short=(
+    -h
+    -c
+    -s
+    -t
+    -v
+    -i
+    -u
+    -f
+  )
+
+  long=(
+    --help
+    --color
+    --scaling
+    --tiling
+    --version
+    --image
+    --no-unlock-indicator
+    --daemonize
+  )
+
+  scaling=(
+    'stretch'
+    'fill'
+    'fit'
+    'center'
+    'tile'
+  )
+
+  case $prev in
+    -c|--color)
+      return
+      ;;
+    --scaling)
+      COMPREPLY=($(compgen -W "${scaling[*]}" -- "$cur"))
+      return
+      ;;
+    -i|--image)
+      if grep -q : <<< "$cur"; then
+        output="${cur%%:*}:"
+        cur="${cur#*:}"
+      else
+        output=
+      fi
+      COMPREPLY=($(compgen -f -- "$cur"))
+      return
+      ;;
+  esac
+
+  if [[ $cur == --* ]]; then
+    COMPREPLY=($(compgen -W "${long[*]}" -- "$cur"))
+  else
+    COMPREPLY=($(compgen -W "${short[*]}" -- "$cur"))
+    COMPREPLY+=($(compgen -W "${long[*]}" -- "$cur"))
+  fi
+
+} &&
+complete -F _swaylock swaylock
diff --git a/completions/bash/swaymsg b/completions/bash/swaymsg
new file mode 100644
index 00000000..20092bdc
--- /dev/null
+++ b/completions/bash/swaymsg
@@ -0,0 +1,59 @@
+# swaymsg(1) completion
+
+_swaymsg()
+{
+  local cur prev
+  _get_comp_words_by_ref cur prev
+
+  types=(
+    'get_workspaces'
+    'get_seats'
+    'get_inputs'
+    'get_outputs'
+    'get_tree'
+    'get_marks'
+    'get_bar_config'
+    'get_version'
+    'get_binding_modes'
+    'get_config'
+    'send_tick'
+  )
+
+  short=(
+    -h
+    -q
+    -r
+    -s
+    -t
+    -v
+  )
+
+  long=(
+    --help
+    --quiet
+    --raw
+    --socket
+    --type
+    --verbose
+  )
+
+  case $prev in
+    -s|--socket)
+      _filedir
+      return
+      ;;
+    -t|--type)
+      COMPREPLY=($(compgen -W "${types[*]}" -- "$cur"))
+      return
+      ;;
+  esac
+
+  if [[ $cur == --* ]]; then
+    COMPREPLY=($(compgen -W "${long[*]}" -- "$cur"))
+  else
+    COMPREPLY=($(compgen -W "${short[*]}" -- "$cur"))
+    COMPREPLY+=($(compgen -W "${long[*]}" -- "$cur"))
+  fi
+
+} &&
+complete -F _swaymsg swaymsg
diff --git a/completions/zsh/_sway b/completions/zsh/_sway
index bab90fbf..05112002 100644
--- a/completions/zsh/_sway
+++ b/completions/zsh/_sway
@@ -18,5 +18,5 @@ _arguments -s \
 	'(-c --config)'{-c,--config}'[Specify a config file]:files:_files' \
 	'(-C --validate)'{-C,--validate}'[Check validity of the config file, then exit]' \
 	'(-d --debug)'{-d,--debug}'[Enables full logging, including debug information]' \
-	'(-v --verbose)'{-v,--verbose}'[Enables more verbose logging]' \
+	'(-V --verbose)'{-V,--verbose}'[Enables more verbose logging]' \
 	'(--get-socketpath)'--get-socketpath'[Gets the IPC socket path and prints it, then exits]'
diff --git a/completions/zsh/_swaygrab b/completions/zsh/_swaygrab
deleted file mode 100644
index 0f9846f4..00000000
--- a/completions/zsh/_swaygrab
+++ /dev/null
@@ -1,23 +0,0 @@
-#compdef swaygrab
-#-----------------
-# Description
-# -----------
-#
-# Completion script for swaygrab in sway wm (http://swaywm.org)
-#
-# -----------------------------------------------------
-# Author
-# ------
-#
-# * Seth Barberee <seth.barberee@gmail.com>
-#
-# ------------------------------------------
-
-_arguments -s \
-	'(-h --help)'{-h,--help}'[Shows help message]' \
-	'(-c --capture)'{-c,--capture}'[Captures multiple frames as video and passes them to ffmpeg]' \
-	'(-o --output)'{-o,--output}'[Use the specified output. If not specified then current focused output will be used]' \
-	'(-v --version)'{-v,--version}'[Print the version (of swaymsg) and quit]' \
-	'(-s --socket)'{-s,--socket}'[Use the specified socket path.]:files:_files' \
-	'(-r --rate)'{-r,--rate}'[Specify a framerate (in fps). Used in combination with -c. Default is 30 and must be an integer]' \
-	'(--raw)--raw[Instead of ImageMagick or ffmpeg, dump raw rgba data to stdout]'
diff --git a/completions/zsh/_swaymsg b/completions/zsh/_swaymsg
index 2e39deb6..a7a1c8e0 100644
--- a/completions/zsh/_swaymsg
+++ b/completions/zsh/_swaymsg
@@ -22,6 +22,9 @@ types=(
 'get_marks'
 'get_bar_config'
 'get_version'
+'get_binding_modes'
+'get_config'
+'send_tick'
 )
 
 _arguments -s \
diff --git a/config.in b/config.in
index 4a11762a..41f53461 100644
--- a/config.in
+++ b/config.in
@@ -16,7 +16,8 @@ set $right l
 # Your preferred terminal emulator
 set $term urxvt
 # Your preferred application launcher
-set $menu dmenu_run
+# Note: it's recommended that you pass the final command to sway
+set $menu dmenu_path | dmenu | xargs swaymsg exec
 
 ### Output configuration
 #
diff --git a/include/ipc.h b/include/ipc.h
index 0010718b..a3f60e19 100644
--- a/include/ipc.h
+++ b/include/ipc.h
@@ -15,6 +15,7 @@ enum ipc_command_type {
 	IPC_GET_VERSION = 7,
 	IPC_GET_BINDING_MODES = 8,
 	IPC_GET_CONFIG = 9,
+	IPC_SEND_TICK = 10,
 
 	// sway-specific command types
 	IPC_GET_INPUTS = 100,
@@ -27,8 +28,8 @@ enum ipc_command_type {
 	IPC_EVENT_WINDOW = ((1<<31) | 3),
 	IPC_EVENT_BARCONFIG_UPDATE = ((1<<31) | 4),
 	IPC_EVENT_BINDING = ((1<<31) | 5),
-	IPC_EVENT_MODIFIER = ((1<<31) | 6),
-	IPC_EVENT_INPUT = ((1<<31) | 7),
+	IPC_EVENT_SHUTDOWN = ((1<<31) | 6),
+	IPC_EVENT_TICK = ((1<<31) | 7),
 };
 
 #endif
diff --git a/include/sway/commands.h b/include/sway/commands.h
index e71a7228..41858ccc 100644
--- a/include/sway/commands.h
+++ b/include/sway/commands.h
@@ -106,7 +106,7 @@ sway_cmd cmd_exit;
 sway_cmd cmd_floating;
 sway_cmd cmd_floating_maximum_size;
 sway_cmd cmd_floating_minimum_size;
-sway_cmd cmd_floating_mod;
+sway_cmd cmd_floating_modifier;
 sway_cmd cmd_floating_scroll;
 sway_cmd cmd_focus;
 sway_cmd cmd_focus_follows_mouse;
@@ -213,8 +213,10 @@ sway_cmd input_cmd_scroll_button;
 sway_cmd input_cmd_scroll_method;
 sway_cmd input_cmd_tap;
 sway_cmd input_cmd_tap_button_map;
+sway_cmd input_cmd_xkb_capslock;
 sway_cmd input_cmd_xkb_layout;
 sway_cmd input_cmd_xkb_model;
+sway_cmd input_cmd_xkb_numlock;
 sway_cmd input_cmd_xkb_options;
 sway_cmd input_cmd_xkb_rules;
 sway_cmd input_cmd_xkb_variant;
diff --git a/include/sway/config.h b/include/sway/config.h
index b8da29c5..909b6827 100644
--- a/include/sway/config.h
+++ b/include/sway/config.h
@@ -1,6 +1,5 @@
 #ifndef _SWAY_CONFIG_H
 #define _SWAY_CONFIG_H
-#define PID_WORKSPACE_TIMEOUT 60
 #include <libinput.h>
 #include <stdint.h>
 #include <string.h>
@@ -22,14 +21,28 @@ struct sway_variable {
 	char *value;
 };
 
+
+enum binding_input_type {
+	BINDING_KEYCODE,
+	BINDING_KEYSYM,
+	BINDING_MOUSE,
+};
+
+enum binding_flags {
+	BINDING_RELEASE=1,
+	BINDING_LOCKED=2, // keyboard only
+	BINDING_BORDER=4, // mouse only; trigger on container border
+	BINDING_CONTENTS=8, // mouse only; trigger on container contents
+	BINDING_TITLEBAR=16 // mouse only; trigger on container titlebar
+};
+
 /**
  * A key binding and an associated command.
  */
 struct sway_binding {
+	enum binding_input_type type;
 	int order;
-	bool release;
-	bool locked;
-	bool bindcode;
+	uint32_t flags;
 	list_t *keys; // sorted in ascending order
 	uint32_t modifiers;
 	char *command;
@@ -50,6 +63,7 @@ struct sway_mode {
 	char *name;
 	list_t *keysym_bindings;
 	list_t *keycode_bindings;
+	list_t *mouse_bindings;
 	bool pango;
 };
 
@@ -87,6 +101,9 @@ struct input_config {
 	char *xkb_rules;
 	char *xkb_variant;
 
+	int xkb_numlock;
+	int xkb_capslock;
+
 	struct input_config_mapped_from_region *mapped_from_region;
 	char *mapped_to_output;
 
@@ -146,12 +163,6 @@ struct workspace_output {
 	char *workspace;
 };
 
-struct pid_workspace {
-	pid_t *pid;
-	char *workspace;
-	time_t *time_added;
-};
-
 struct bar_config {
 	/**
 	 * One of "dock", "hide", "invisible"
@@ -302,7 +313,6 @@ struct sway_config {
 	list_t *bars;
 	list_t *cmd_queue;
 	list_t *workspace_outputs;
-	list_t *pid_workspaces;
 	list_t *output_configs;
 	list_t *input_configs;
 	list_t *seat_configs;
@@ -313,6 +323,7 @@ struct sway_config {
 	struct bar_config *current_bar;
 	char *swaybg_command;
 	uint32_t floating_mod;
+	bool floating_mod_inverse;
 	uint32_t dragging_key;
 	uint32_t resizing_key;
 	char *floating_scroll_up_cmd;
@@ -388,9 +399,6 @@ struct sway_config {
 	} handler_context;
 };
 
-void pid_workspace_add(struct pid_workspace *pw);
-void free_pid_workspace(struct pid_workspace *pw);
-
 /**
  * Loads the main config from the given path. is_active should be true when
  * reloading the config.
@@ -480,7 +488,7 @@ int sway_binding_cmp_keys(const void *a, const void *b);
 
 void free_sway_binding(struct sway_binding *sb);
 
-struct sway_binding *sway_binding_dup(struct sway_binding *sb);
+void seat_execute_command(struct sway_seat *seat, struct sway_binding *binding);
 
 void load_swaybars();
 
diff --git a/include/sway/criteria.h b/include/sway/criteria.h
index 6a8337c5..b4ff7d49 100644
--- a/include/sway/criteria.h
+++ b/include/sway/criteria.h
@@ -2,6 +2,7 @@
 #define _SWAY_CRITERIA_H
 
 #include <pcre.h>
+#include "config.h"
 #include "list.h"
 #include "tree/view.h"
 
@@ -25,7 +26,9 @@ struct criteria {
 	pcre *instance;
 	pcre *con_mark;
 	uint32_t con_id; // internal ID
+#ifdef HAVE_XWAYLAND
 	uint32_t id; // X11 window ID
+#endif
 	pcre *window_role;
 	uint32_t window_type;
 	bool floating;
diff --git a/include/sway/desktop/transaction.h b/include/sway/desktop/transaction.h
index cee4afed..56361d94 100644
--- a/include/sway/desktop/transaction.h
+++ b/include/sway/desktop/transaction.h
@@ -42,17 +42,4 @@ void transaction_notify_view_ready(struct sway_view *view, uint32_t serial);
 void transaction_notify_view_ready_by_size(struct sway_view *view,
 		int width, int height);
 
-/**
- * Get the saved texture that should be rendered for a view.
- *
- * The addresses pointed at by the width and height pointers will be populated
- * with the surface's dimensions, which may be different to the texture's
- * dimensions if output scaling is used.
- *
- * This function should only be called if it is known that the view has
- * instructions.
- */
-struct wlr_texture *transaction_get_saved_texture(struct sway_view *view,
-		int *width, int *height);
-
 #endif
diff --git a/include/sway/input/cursor.h b/include/sway/input/cursor.h
index 5dd109ca..7ec45120 100644
--- a/include/sway/input/cursor.h
+++ b/include/sway/input/cursor.h
@@ -3,6 +3,8 @@
 #include <stdint.h>
 #include "sway/input/seat.h"
 
+#define SWAY_CURSOR_PRESSED_BUTTONS_CAP 32
+
 struct sway_cursor {
 	struct sway_seat *seat;
 	struct wlr_cursor *cursor;
@@ -11,6 +13,7 @@ struct sway_cursor {
 	} previous;
 	struct wlr_xcursor_manager *xcursor_manager;
 
+	const char *image;
 	struct wl_client *image_client;
 
 	struct wl_listener motion;
@@ -28,6 +31,10 @@ struct sway_cursor {
 	uint32_t tool_buttons;
 
 	struct wl_listener request_set_cursor;
+
+	// Mouse binding state
+	uint32_t pressed_buttons[SWAY_CURSOR_PRESSED_BUTTONS_CAP];
+	size_t pressed_button_count;
 };
 
 void sway_cursor_destroy(struct sway_cursor *cursor);
@@ -37,4 +44,7 @@ void cursor_send_pointer_motion(struct sway_cursor *cursor, uint32_t time_msec,
 void dispatch_cursor_button(struct sway_cursor *cursor, uint32_t time_msec,
 	uint32_t button, enum wlr_button_state state);
 
+void cursor_set_image(struct sway_cursor *cursor, const char *image,
+		struct wl_client *client);
+
 #endif
diff --git a/include/sway/input/input-manager.h b/include/sway/input/input-manager.h
index 89a3ac71..aa2f6f19 100644
--- a/include/sway/input/input-manager.h
+++ b/include/sway/input/input-manager.h
@@ -2,6 +2,7 @@
 #define _SWAY_INPUT_INPUT_MANAGER_H
 #include <libinput.h>
 #include <wlr/types/wlr_input_inhibitor.h>
+#include <wlr/types/wlr_virtual_keyboard_v1.h>
 #include "sway/server.h"
 #include "sway/config.h"
 #include "list.h"
@@ -25,10 +26,12 @@ struct sway_input_manager {
 	struct wl_list seats;
 
 	struct wlr_input_inhibit_manager *inhibit;
+	struct wlr_virtual_keyboard_manager_v1 *virtual_keyboard;
 
 	struct wl_listener new_input;
 	struct wl_listener inhibit_activate;
 	struct wl_listener inhibit_deactivate;
+	struct wl_listener virtual_keyboard_new;
 };
 
 struct sway_input_manager *input_manager_create(struct sway_server *server);
diff --git a/include/sway/input/keyboard.h b/include/sway/input/keyboard.h
index 6713398e..6d28454c 100644
--- a/include/sway/input/keyboard.h
+++ b/include/sway/input/keyboard.h
@@ -38,6 +38,9 @@ struct sway_keyboard {
 	struct sway_shortcut_state state_keysyms_raw;
 	struct sway_shortcut_state state_keycodes;
 	struct sway_binding *held_binding;
+
+	struct wl_event_source *key_repeat_source;
+	struct sway_binding *repeat_binding;
 };
 
 struct sway_keyboard *sway_keyboard_create(struct sway_seat *seat,
diff --git a/include/sway/input/seat.h b/include/sway/input/seat.h
index eac1626b..92387601 100644
--- a/include/sway/input/seat.h
+++ b/include/sway/input/seat.h
@@ -3,6 +3,7 @@
 
 #include <wlr/types/wlr_layer_shell.h>
 #include <wlr/types/wlr_seat.h>
+#include <wlr/util/edges.h>
 #include "sway/input/input-manager.h"
 
 struct sway_seat_device {
@@ -52,6 +53,24 @@ struct sway_seat {
 	int32_t touch_id;
 	double touch_x, touch_y;
 
+	// Operations (drag and resize)
+	enum {
+		OP_NONE,
+		OP_MOVE,
+		OP_RESIZE,
+	} operation;
+
+	struct sway_container *op_container;
+	enum wlr_edges op_resize_edge;
+	uint32_t op_button;
+	bool op_resize_preserve_ratio;
+	double op_ref_lx, op_ref_ly;         // cursor's x/y at start of op
+	double op_ref_width, op_ref_height;  // container's size at start of op
+	double op_ref_con_lx, op_ref_con_ly; // container's x/y at start of op
+
+	uint32_t last_button;
+	uint32_t last_button_serial;
+
 	struct wl_listener focus_destroy;
 	struct wl_listener new_container;
 	struct wl_listener new_drag_icon;
@@ -80,7 +99,7 @@ void seat_configure_xcursor(struct sway_seat *seat);
 void seat_set_focus(struct sway_seat *seat, struct sway_container *container);
 
 void seat_set_focus_warp(struct sway_seat *seat,
-		struct sway_container *container, bool warp);
+		struct sway_container *container, bool warp, bool notify);
 
 void seat_set_focus_surface(struct sway_seat *seat,
 		struct wlr_surface *surface, bool unfocus);
@@ -105,6 +124,9 @@ struct sway_container *seat_get_focus(struct sway_seat *seat);
 struct sway_container *seat_get_focus_inactive(struct sway_seat *seat,
 		struct sway_container *container);
 
+struct sway_container *seat_get_focus_inactive_tiling(struct sway_seat *seat,
+		struct sway_container *container);
+
 /**
  * Descend into the focus stack to find the focus-inactive view. Useful for
  * container placement when they change position in the tree.
@@ -134,4 +156,15 @@ bool seat_is_input_allowed(struct sway_seat *seat, struct wlr_surface *surface);
 
 void drag_icon_update_position(struct sway_drag_icon *icon);
 
+void seat_begin_move(struct sway_seat *seat, struct sway_container *con,
+		uint32_t button);
+
+void seat_begin_resize(struct sway_seat *seat, struct sway_container *con,
+		uint32_t button, enum wlr_edges edge);
+
+void seat_end_mouse_operation(struct sway_seat *seat);
+
+void seat_pointer_notify_button(struct sway_seat *seat, uint32_t time_msec,
+		uint32_t button, enum wlr_button_state state);
+
 #endif
diff --git a/include/sway/ipc-server.h b/include/sway/ipc-server.h
index 6469f097..4b6d0e25 100644
--- a/include/sway/ipc-server.h
+++ b/include/sway/ipc-server.h
@@ -16,5 +16,7 @@ void ipc_event_workspace(struct sway_container *old,
 void ipc_event_window(struct sway_container *window, const char *change);
 void ipc_event_barconfig_update(struct bar_config *bar);
 void ipc_event_mode(const char *mode, bool pango);
+void ipc_event_shutdown(const char *reason);
+void ipc_event_binding(struct sway_binding *binding);
 
 #endif
diff --git a/include/sway/output.h b/include/sway/output.h
index b6cda83c..80dcd37b 100644
--- a/include/sway/output.h
+++ b/include/sway/output.h
@@ -5,6 +5,7 @@
 #include <wayland-server.h>
 #include <wlr/types/wlr_box.h>
 #include <wlr/types/wlr_output.h>
+#include "config.h"
 #include "sway/tree/view.h"
 
 struct sway_server;
@@ -38,15 +39,9 @@ struct sway_output {
 	} events;
 };
 
-/**
- * Contains a surface's root geometry information. For instance, when rendering
- * a popup, this will contain the parent view's position and size.
- */
-struct root_geometry {
-	double x, y;
-	int width, height;
-	float rotation;
-};
+typedef void (*sway_surface_iterator_func_t)(struct sway_output *output,
+	struct wlr_surface *surface, struct wlr_box *box, float rotation,
+	void *user_data);
 
 void output_damage_whole(struct sway_output *output);
 
@@ -65,36 +60,37 @@ struct sway_container *output_by_name(const char *name);
 
 void output_enable(struct sway_output *output);
 
-bool output_has_opaque_lockscreen(struct sway_output *output,
-	struct sway_seat *seat);
+bool output_has_opaque_overlay_layer_surface(struct sway_output *output);
 
 struct sway_container *output_get_active_workspace(struct sway_output *output);
 
 void output_render(struct sway_output *output, struct timespec *when,
 	pixman_region32_t *damage);
 
-bool output_get_surface_box(struct root_geometry *geo,
-	struct sway_output *output, struct wlr_surface *surface, int sx, int sy,
-	struct wlr_box *surface_box);
+void output_surface_for_each_surface(struct sway_output *output,
+		struct wlr_surface *surface, double ox, double oy,
+		sway_surface_iterator_func_t iterator, void *user_data);
 
-void output_surface_for_each_surface(struct wlr_surface *surface,
-	double ox, double oy, struct root_geometry *geo,
-	wlr_surface_iterator_func_t iterator, void *user_data);
-
-void output_view_for_each_surface(struct sway_view *view,
-	struct sway_output *output, struct root_geometry *geo,
-	wlr_surface_iterator_func_t iterator, void *user_data);
-
-void output_layer_for_each_surface(struct wl_list *layer_surfaces,
-	struct root_geometry *geo, wlr_surface_iterator_func_t iterator,
+void output_view_for_each_surface(struct sway_output *output,
+	struct sway_view *view, sway_surface_iterator_func_t iterator,
 	void *user_data);
 
-void output_unmanaged_for_each_surface(struct wl_list *unmanaged,
-	struct sway_output *output, struct root_geometry *geo,
-	wlr_surface_iterator_func_t iterator, void *user_data);
+void output_view_for_each_popup(struct sway_output *output,
+		struct sway_view *view, sway_surface_iterator_func_t iterator,
+		void *user_data);
 
-void output_drag_icons_for_each_surface(struct wl_list *drag_icons,
-	struct sway_output *output, struct root_geometry *geo,
-	wlr_surface_iterator_func_t iterator, void *user_data);
+void output_layer_for_each_surface(struct sway_output *output,
+	struct wl_list *layer_surfaces, sway_surface_iterator_func_t iterator,
+	void *user_data);
+
+#ifdef HAVE_XWAYLAND
+void output_unmanaged_for_each_surface(struct sway_output *output,
+	struct wl_list *unmanaged, sway_surface_iterator_func_t iterator,
+	void *user_data);
+#endif
+
+void output_drag_icons_for_each_surface(struct sway_output *output,
+	struct wl_list *drag_icons, sway_surface_iterator_func_t iterator,
+	void *user_data);
 
 #endif
diff --git a/include/sway/scratchpad.h b/include/sway/scratchpad.h
new file mode 100644
index 00000000..5af5256f
--- /dev/null
+++ b/include/sway/scratchpad.h
@@ -0,0 +1,26 @@
+#ifndef _SWAY_SCRATCHPAD_H
+#define _SWAY_SCRATCHPAD_H
+
+#include "tree/container.h"
+
+/**
+ * Move a container to the scratchpad.
+ */
+void scratchpad_add_container(struct sway_container *con);
+
+/**
+ * Remove a container from the scratchpad.
+ */
+void scratchpad_remove_container(struct sway_container *con);
+
+/**
+ * Show or hide the next container on the scratchpad.
+ */
+void scratchpad_toggle_auto(void);
+
+/**
+ * Show or hide a specific container on the scratchpad.
+ */
+void scratchpad_toggle_container(struct sway_container *con);
+
+#endif
diff --git a/include/sway/server.h b/include/sway/server.h
index 70bde6d4..a3782f91 100644
--- a/include/sway/server.h
+++ b/include/sway/server.h
@@ -12,7 +12,10 @@
 #include <wlr/render/wlr_renderer.h>
 // TODO WLR: make Xwayland optional
 #include "list.h"
+#include "config.h"
+#ifdef HAVE_XWAYLAND
 #include "sway/xwayland.h"
+#endif
 
 struct sway_server {
 	struct wl_display *wl_display;
@@ -39,11 +42,11 @@ struct sway_server {
 
 	struct wlr_xdg_shell *xdg_shell;
 	struct wl_listener xdg_shell_surface;
-
+#ifdef HAVE_XWAYLAND
 	struct sway_xwayland xwayland;
 	struct wl_listener xwayland_surface;
 	struct wl_listener xwayland_ready;
-
+#endif
 	bool debug_txn_timings;
 
 	list_t *transactions;
@@ -65,6 +68,7 @@ void handle_idle_inhibitor_v1(struct wl_listener *listener, void *data);
 void handle_layer_shell_surface(struct wl_listener *listener, void *data);
 void handle_xdg_shell_v6_surface(struct wl_listener *listener, void *data);
 void handle_xdg_shell_surface(struct wl_listener *listener, void *data);
+#ifdef HAVE_XWAYLAND
 void handle_xwayland_surface(struct wl_listener *listener, void *data);
-
+#endif
 #endif
diff --git a/include/sway/tree/container.h b/include/sway/tree/container.h
index ca7a3288..c3942e9e 100644
--- a/include/sway/tree/container.h
+++ b/include/sway/tree/container.h
@@ -60,6 +60,8 @@ struct sway_container_state {
 	double swayc_x, swayc_y;
 	double swayc_width, swayc_height;
 
+	bool is_fullscreen;
+
 	bool has_gaps;
 	double current_gaps;
 	double gaps_inner;
@@ -74,7 +76,6 @@ struct sway_container_state {
 	// View properties
 	double view_x, view_y;
 	double view_width, view_height;
-	bool is_fullscreen;
 
 	enum sway_container_border border;
 	int border_thickness;
@@ -84,7 +85,7 @@ struct sway_container_state {
 	bool border_right;
 
 	// Workspace properties
-	struct sway_view *ws_fullscreen;
+	struct sway_container *ws_fullscreen;
 	struct sway_container *ws_floating;
 };
 
@@ -124,6 +125,8 @@ struct sway_container {
 	double saved_x, saved_y;
 	double saved_width, saved_height;
 
+	bool is_fullscreen;
+
 	// The gaps currently applied to the container.
 	double current_gaps;
 
@@ -135,6 +138,11 @@ struct sway_container {
 
 	struct sway_container *parent;
 
+	// Indicates that the container is a scratchpad container.
+	// Both hidden and visible scratchpad containers have scratchpad=true.
+	// Hidden scratchpad containers have a NULL parent.
+	bool scratchpad;
+
 	float alpha;
 
 	struct wlr_texture *title_focused;
@@ -222,16 +230,13 @@ struct sway_container *container_parent(struct sway_container *container,
  * surface-local coordinates of the given layout coordinates if the container
  * is a view and the view contains a surface at those coordinates.
  */
-struct sway_container *container_at(struct sway_container *container,
-		double ox, double oy, struct wlr_surface **surface,
+struct sway_container *container_at(struct sway_container *workspace,
+		double lx, double ly, struct wlr_surface **surface,
 		double *sx, double *sy);
 
-/**
- * Same as container_at, but only checks floating views and expects coordinates
- * to be layout coordinates, as that's what floating views use.
- */
-struct sway_container *floating_container_at(double lx, double ly,
-		struct wlr_surface **surface, double *sx, double *sy);
+struct sway_container *container_at_view(struct sway_container *view,
+		double lx, double ly, struct wlr_surface **surface,
+		double *sx, double *sy);
 
 /**
  * Apply the function for each descendant of the container breadth first.
@@ -262,6 +267,8 @@ int container_count_descendants_of_type(struct sway_container *con,
 
 void container_create_notify(struct sway_container *container);
 
+void container_update_textures_recursive(struct sway_container *con);
+
 void container_damage_whole(struct sway_container *container);
 
 bool container_reap_empty(struct sway_container *con);
@@ -289,6 +296,11 @@ void container_notify_subtree_changed(struct sway_container *container);
  */
 size_t container_titlebar_height(void);
 
+/**
+ * Resize and center the container in its workspace.
+ */
+void container_init_floating(struct sway_container *container);
+
 void container_set_floating(struct sway_container *container, bool enable);
 
 void container_set_geometry_from_floating_view(struct sway_container *con);
@@ -304,6 +316,12 @@ bool container_is_floating(struct sway_container *container);
  */
 void container_get_box(struct sway_container *container, struct wlr_box *box);
 
+/**
+ * Move a floating container by the specified amount.
+ */
+void container_floating_translate(struct sway_container *con,
+		double x_amount, double y_amount);
+
 /**
  * Move a floating container to a new layout-local position.
  */
@@ -318,4 +336,32 @@ void container_set_dirty(struct sway_container *container);
 
 bool container_has_urgent_child(struct sway_container *container);
 
+/**
+ * If the container is involved in a drag or resize operation via a mouse, this
+ * ends the operation.
+ */
+void container_end_mouse_operation(struct sway_container *container);
+
+void container_set_fullscreen(struct sway_container *container, bool enable);
+
+/**
+ * Return true if the container is floating, or a child of a floating split
+ * container.
+ */
+bool container_is_floating_or_child(struct sway_container *container);
+
+/**
+ * Return true if the container is fullscreen, or a child of a fullscreen split
+ * container.
+ */
+bool container_is_fullscreen_or_child(struct sway_container *container);
+
+/**
+ * Wrap the children of parent in a new container. The new container will be the
+ * only child of parent.
+ *
+ * The new container is returned.
+ */
+struct sway_container *container_wrap_children(struct sway_container *parent);
+
 #endif
diff --git a/include/sway/tree/layout.h b/include/sway/tree/layout.h
index ba265623..a4c31bf6 100644
--- a/include/sway/tree/layout.h
+++ b/include/sway/tree/layout.h
@@ -3,6 +3,7 @@
 #include <wlr/types/wlr_output_layout.h>
 #include <wlr/render/wlr_texture.h>
 #include "sway/tree/container.h"
+#include "config.h"
 
 enum movement_direction {
 	MOVE_LEFT,
@@ -14,10 +15,11 @@ enum movement_direction {
 };
 
 enum resize_edge {
-	RESIZE_EDGE_LEFT,
-	RESIZE_EDGE_RIGHT,
-	RESIZE_EDGE_TOP,
-	RESIZE_EDGE_BOTTOM,
+	RESIZE_EDGE_NONE   = 0,
+	RESIZE_EDGE_LEFT   = 1,
+	RESIZE_EDGE_RIGHT  = 2,
+	RESIZE_EDGE_TOP    = 4,
+	RESIZE_EDGE_BOTTOM = 8,
 };
 
 struct sway_container;
@@ -26,14 +28,17 @@ struct sway_root {
 	struct wlr_output_layout *output_layout;
 
 	struct wl_listener output_layout_change;
-
+#ifdef HAVE_XWAYLAND
 	struct wl_list xwayland_unmanaged; // sway_xwayland_unmanaged::link
+#endif
 	struct wl_list drag_icons; // sway_drag_icon::link
 
 	struct wlr_texture *debug_tree;
 
 	struct wl_list outputs; // sway_output::link
 
+	list_t *scratchpad; // struct sway_container
+
 	struct {
 		struct wl_signal new_container;
 	} events;
diff --git a/include/sway/tree/view.h b/include/sway/tree/view.h
index 068d92c6..37fd02bc 100644
--- a/include/sway/tree/view.h
+++ b/include/sway/tree/view.h
@@ -3,7 +3,10 @@
 #include <wayland-server.h>
 #include <wlr/types/wlr_surface.h>
 #include <wlr/types/wlr_xdg_shell_v6.h>
+#include "config.h"
+#ifdef HAVE_XWAYLAND
 #include <wlr/xwayland.h>
+#endif
 #include "sway/input/input-manager.h"
 #include "sway/input/seat.h"
 
@@ -12,7 +15,9 @@ struct sway_container;
 enum sway_view_type {
 	SWAY_VIEW_XDG_SHELL_V6,
 	SWAY_VIEW_XDG_SHELL,
+#ifdef HAVE_XWAYLAND
 	SWAY_VIEW_XWAYLAND,
+#endif
 };
 
 enum sway_view_prop {
@@ -22,10 +27,14 @@ enum sway_view_prop {
 	VIEW_PROP_INSTANCE,
 	VIEW_PROP_WINDOW_TYPE,
 	VIEW_PROP_WINDOW_ROLE,
+#ifdef HAVE_XWAYLAND
 	VIEW_PROP_X11_WINDOW_ID,
+#endif
 };
 
 struct sway_view_impl {
+	void (*get_constraints)(struct sway_view *view, double *min_width,
+			double *max_width, double *min_height, double *max_height);
 	const char *(*get_string_prop)(struct sway_view *view,
 			enum sway_view_prop prop);
 	uint32_t (*get_int_prop)(struct sway_view *view, enum sway_view_prop prop);
@@ -38,7 +47,10 @@ struct sway_view_impl {
 	bool (*has_client_side_decorations)(struct sway_view *view);
 	void (*for_each_surface)(struct sway_view *view,
 		wlr_surface_iterator_func_t iterator, void *user_data);
+	void (*for_each_popup)(struct sway_view *view,
+		wlr_surface_iterator_func_t iterator, void *user_data);
 	void (*close)(struct sway_view *view);
+	void (*close_popups)(struct sway_view *view);
 	void (*destroy)(struct sway_view *view);
 };
 
@@ -60,8 +72,6 @@ struct sway_view {
 	// Used when changing a view from tiled to floating.
 	int natural_width, natural_height;
 
-	bool is_fullscreen;
-
 	char *title_format;
 	enum sway_container_border border;
 	int border_thickness;
@@ -75,6 +85,9 @@ struct sway_view {
 	bool allow_request_urgent;
 	struct wl_event_source *urgent_timer;
 
+	struct wlr_buffer *saved_buffer;
+	int saved_buffer_width, saved_buffer_height;
+
 	bool destroying;
 
 	list_t *executed_criteria; // struct criteria *
@@ -88,7 +101,9 @@ struct sway_view {
 	union {
 		struct wlr_xdg_surface_v6 *wlr_xdg_surface_v6;
 		struct wlr_xdg_surface *wlr_xdg_surface;
+#ifdef HAVE_XWAYLAND
 		struct wlr_xwayland_surface *wlr_xwayland_surface;
+#endif
 		struct wlr_wl_shell_surface *wlr_wl_shell_surface;
 	};
 
@@ -108,6 +123,8 @@ struct sway_xdg_shell_v6_view {
 	struct wl_listener request_resize;
 	struct wl_listener request_maximize;
 	struct wl_listener request_fullscreen;
+	struct wl_listener set_title;
+	struct wl_listener set_app_id;
 	struct wl_listener new_popup;
 	struct wl_listener map;
 	struct wl_listener unmap;
@@ -122,12 +139,14 @@ struct sway_xdg_shell_view {
 	struct wl_listener request_resize;
 	struct wl_listener request_maximize;
 	struct wl_listener request_fullscreen;
+	struct wl_listener set_title;
+	struct wl_listener set_app_id;
 	struct wl_listener new_popup;
 	struct wl_listener map;
 	struct wl_listener unmap;
 	struct wl_listener destroy;
 };
-
+#ifdef HAVE_XWAYLAND
 struct sway_xwayland_view {
 	struct sway_view view;
 
@@ -159,7 +178,7 @@ struct sway_xwayland_unmanaged {
 	struct wl_listener unmap;
 	struct wl_listener destroy;
 };
-
+#endif
 struct sway_view_child;
 
 struct sway_view_child_impl {
@@ -215,14 +234,12 @@ uint32_t view_get_window_type(struct sway_view *view);
 
 const char *view_get_shell(struct sway_view *view);
 
+void view_get_constraints(struct sway_view *view, double *min_width,
+		double *max_width, double *min_height, double *max_height);
+
 uint32_t view_configure(struct sway_view *view, double lx, double ly, int width,
 	int height);
 
-/**
- * Center the view in its workspace and build the swayc decorations around it.
- */
-void view_init_floating(struct sway_view *view);
-
 /**
  * Configure the view's position and size based on the swayc's position and
  * size, taking borders into consideration.
@@ -233,17 +250,24 @@ void view_set_activated(struct sway_view *view, bool activated);
 
 void view_set_tiled(struct sway_view *view, bool tiled);
 
-void view_set_fullscreen_raw(struct sway_view *view, bool fullscreen);
-
-void view_set_fullscreen(struct sway_view *view, bool fullscreen);
-
 void view_close(struct sway_view *view);
 
+void view_close_popups(struct sway_view *view);
+
 void view_damage_from(struct sway_view *view);
 
+/**
+ * Iterate all surfaces of a view (toplevels + popups).
+ */
 void view_for_each_surface(struct sway_view *view,
 	wlr_surface_iterator_func_t iterator, void *user_data);
 
+/**
+ * Iterate all popups recursively.
+ */
+void view_for_each_popup(struct sway_view *view,
+	wlr_surface_iterator_func_t iterator, void *user_data);
+
 // view implementation
 
 void view_init(struct sway_view *view, enum sway_view_type type,
@@ -272,9 +296,10 @@ struct sway_view *view_from_wlr_xdg_surface(
 	struct wlr_xdg_surface *xdg_surface);
 struct sway_view *view_from_wlr_xdg_surface_v6(
 	struct wlr_xdg_surface_v6 *xdg_surface_v6);
+#ifdef HAVE_XWAYLAND
 struct sway_view *view_from_wlr_xwayland_surface(
 	struct wlr_xwayland_surface *xsurface);
-
+#endif
 struct sway_view *view_from_wlr_surface(struct wlr_surface *surface);
 
 /**
@@ -303,6 +328,8 @@ void view_clear_marks(struct sway_view *view);
 
 bool view_has_mark(struct sway_view *view, char *mark);
 
+void view_add_mark(struct sway_view *view, char *mark);
+
 void view_update_marks_textures(struct sway_view *view);
 
 /**
@@ -315,4 +342,8 @@ void view_set_urgent(struct sway_view *view, bool enable);
 
 bool view_is_urgent(struct sway_view *view);
 
+void view_remove_saved_buffer(struct sway_view *view);
+
+void view_save_buffer(struct sway_view *view);
+
 #endif
diff --git a/include/sway/tree/workspace.h b/include/sway/tree/workspace.h
index bc95317a..5ae0ae3a 100644
--- a/include/sway/tree/workspace.h
+++ b/include/sway/tree/workspace.h
@@ -7,7 +7,7 @@ struct sway_view;
 
 struct sway_workspace {
 	struct sway_container *swayc;
-	struct sway_view *fullscreen;
+	struct sway_container *fullscreen;
 	struct sway_container *floating;
 	list_t *output_priority;
 	bool urgent;
@@ -44,6 +44,10 @@ void workspace_output_add_priority(struct sway_container *workspace,
 struct sway_container *workspace_output_get_highest_available(
 		struct sway_container *ws, struct sway_container *exclude);
 
+struct sway_container *workspace_for_pid(pid_t pid);
+
+void workspace_record_pid(pid_t pid);
+
 void workspace_detect_urgent(struct sway_container *workspace);
 
 #endif
diff --git a/include/swaygrab/json.h b/include/swaygrab/json.h
deleted file mode 100644
index c1093ef1..00000000
--- a/include/swaygrab/json.h
+++ /dev/null
@@ -1,10 +0,0 @@
-#include <json-c/json.h>
-#include "wlc/wlc.h"
-
-void init_json_tree(int socketfd);
-void free_json_tree();
-char *get_focused_output();
-char *create_payload(const char *output, struct wlc_geometry *g);
-struct wlc_geometry *get_container_geometry(json_object *container);
-json_object *get_focused_container();
-json_object *get_output_container(const char *output);
diff --git a/include/swaynag/config.h b/include/swaynag/config.h
new file mode 100644
index 00000000..0d8889de
--- /dev/null
+++ b/include/swaynag/config.h
@@ -0,0 +1,13 @@
+#ifndef _SWAYNAG_CONFIG_H
+#define _SWAYNAG_CONFIG_H
+#include "swaynag/swaynag.h"
+#include "list.h"
+
+int swaynag_parse_options(int argc, char **argv, struct swaynag *swaynag,
+		list_t *types, struct swaynag_type *type, char **config, bool *debug);
+
+char *swaynag_get_config_path(void);
+
+int swaynag_load_config(char *path, struct swaynag *swaynag, list_t *types);
+
+#endif
diff --git a/include/swaynag/render.h b/include/swaynag/render.h
new file mode 100644
index 00000000..d09e5929
--- /dev/null
+++ b/include/swaynag/render.h
@@ -0,0 +1,7 @@
+#ifndef _SWAYNAG_RENDER_H
+#define _SWAYNAG_RENDER_H
+#include "swaynag/swaynag.h"
+
+void render_frame(struct swaynag *swaynag);
+
+#endif
diff --git a/include/swaynag/swaynag.h b/include/swaynag/swaynag.h
new file mode 100644
index 00000000..1bf8b640
--- /dev/null
+++ b/include/swaynag/swaynag.h
@@ -0,0 +1,100 @@
+#ifndef _SWAYNAG_SWAYNAG_H
+#define _SWAYNAG_SWAYNAG_H
+#include <stdint.h>
+#include <strings.h>
+#include "list.h"
+#include "pool-buffer.h"
+#include "swaynag/types.h"
+#include "xdg-output-unstable-v1-client-protocol.h"
+
+#define SWAYNAG_MAX_HEIGHT 500
+
+struct swaynag;
+
+enum swaynag_action_type {
+	SWAYNAG_ACTION_DISMISS,
+	SWAYNAG_ACTION_EXPAND,
+	SWAYNAG_ACTION_COMMAND,
+};
+
+struct swaynag_pointer {
+	struct wl_pointer *pointer;
+	uint32_t serial;
+	struct wl_cursor_theme *cursor_theme;
+	struct wl_cursor_image *cursor_image;
+	struct wl_surface *cursor_surface;
+	int x;
+	int y;
+};
+
+struct swaynag_output {
+	char *name;
+	struct wl_output *wl_output;
+	uint32_t wl_name;
+	uint32_t scale;
+	struct swaynag *swaynag;
+	struct wl_list link;
+};
+
+struct swaynag_button {
+	char *text;
+	enum swaynag_action_type type;
+	char *action;
+	int x;
+	int y;
+	int width;
+	int height;
+};
+
+struct swaynag_details {
+	bool visible;
+	char *message;
+
+	int x;
+	int y;
+	int width;
+	int height;
+
+	int offset;
+	int visible_lines;
+	int total_lines;
+	struct swaynag_button button_details;
+	struct swaynag_button button_up;
+	struct swaynag_button button_down;
+};
+
+struct swaynag {
+	bool run_display;
+	int querying_outputs;
+
+	struct wl_display *display;
+	struct wl_compositor *compositor;
+	struct wl_seat *seat;
+	struct wl_shm *shm;
+	struct swaynag_pointer pointer;
+	struct zxdg_output_manager_v1 *xdg_output_manager;
+	struct wl_list outputs;  // swaynag_output::link
+	struct swaynag_output *output;
+	struct zwlr_layer_shell_v1 *layer_shell;
+	struct zwlr_layer_surface_v1 *layer_surface;
+	struct wl_surface *surface;
+
+	uint32_t width;
+	uint32_t height;
+	int32_t scale;
+	struct pool_buffer buffers[2];
+	struct pool_buffer *current_buffer;
+
+	struct swaynag_type *type;
+	char *message;
+	list_t *buttons;
+	struct swaynag_details details;
+};
+
+void swaynag_setup(struct swaynag *swaynag);
+
+void swaynag_run(struct swaynag *swaynag);
+
+void swaynag_destroy(struct swaynag *swaynag);
+
+#endif
diff --git a/include/swaynag/types.h b/include/swaynag/types.h
new file mode 100644
index 00000000..2183ce22
--- /dev/null
+++ b/include/swaynag/types.h
@@ -0,0 +1,39 @@
+#ifndef _SWAYNAG_TYPES_H
+#define _SWAYNAG_TYPES_H
+
+struct swaynag_type {
+	char *name;
+
+	char *font;
+	char *output;
+	uint32_t anchors;
+
+	uint32_t button_background;
+	uint32_t background;
+	uint32_t text;
+	uint32_t border;
+	uint32_t border_bottom;
+
+	uint32_t bar_border_thickness;
+	uint32_t message_padding;
+	uint32_t details_border_thickness;
+	uint32_t button_border_thickness;
+	uint32_t button_gap;
+	uint32_t button_gap_close;
+	uint32_t button_margin_right;
+	uint32_t button_padding;
+};
+
+void swaynag_types_add_default(list_t *types);
+
+struct swaynag_type *swaynag_type_get(list_t *types, char *name);
+
+struct swaynag_type *swaynag_type_clone(struct swaynag_type *type);
+
+void swaynag_type_merge(struct swaynag_type *dest, struct swaynag_type *src);
+
+void swaynag_type_free(struct swaynag_type *type);
+
+void swaynag_types_free(list_t *types);
+
+#endif
diff --git a/include/util.h b/include/util.h
index f68deae8..9277fa6e 100644
--- a/include/util.h
+++ b/include/util.h
@@ -2,6 +2,7 @@
 #define _SWAY_UTIL_H
 
 #include <stdint.h>
+#include <stdbool.h>
 #include <unistd.h>
 #include <sys/types.h> 
 #include <xkbcommon/xkbcommon.h>
@@ -50,6 +51,14 @@ pid_t get_parent_pid(pid_t pid);
  */
 uint32_t parse_color(const char *color);
 
+/**
+ * Given a string that represents a boolean, return the boolean value. This
+ * function also takes in the current boolean value to support toggling. If
+ * toggling is not desired, pass in true for current so that toggling values
+ * get parsed as not true.
+ */
+bool parse_boolean(const char *boolean, bool current);
+
 /**
  * Given a path string, recurseively resolves any symlinks to their targets
  * (which may be a file, directory) and returns the result.
diff --git a/meson.build b/meson.build
index 1d40581a..2a020323 100644
--- a/meson.build
+++ b/meson.build
@@ -12,6 +12,7 @@ project(
 add_project_arguments('-Wno-unused-parameter', language: 'c')
 add_project_arguments('-Wno-unused-function', language: 'c')
 add_project_arguments('-Wno-unused-result', language: 'c')
+add_project_arguments('-DWLR_USE_UNSTABLE', language: 'c')
 
 cc = meson.get_compiler('c')
 
@@ -43,11 +44,17 @@ systemd        = dependency('libsystemd', required: false)
 elogind        = dependency('libelogind', required: false)
 math           = cc.find_library('m')
 rt             = cc.find_library('rt')
-xcb            = dependency('xcb')
 git            = find_program('git', required: false)
 
 conf_data = configuration_data()
 
+if get_option('enable-xwayland')
+	conf_data.set('HAVE_XWAYLAND', true)
+	xcb = dependency('xcb')
+else
+	conf_data.set('HAVE_XWAYLAND', false)
+endif
+
 if gdk_pixbuf.found()
 	conf_data.set('HAVE_GDK_PIXBUF', true)
 endif
@@ -75,6 +82,8 @@ if scdoc.found()
 		'swaylock/swaylock.1.scd',
 		'swaymsg/swaymsg.1.scd',
 		'swayidle/swayidle.1.scd',
+		'swaynag/swaynag.1.scd',
+		'swaynag/swaynag.5.scd',
 	]
 	foreach filename : man_files
 		topic = filename.split('.')[-3].split('/')[-1]
@@ -123,6 +132,7 @@ subdir('swaybg')
 subdir('swaybar')
 subdir('swaylock')
 subdir('swayidle')
+subdir('swaynag')
 
 config = configuration_data()
 config.set('sysconfdir', join_paths(prefix, sysconfdir))
@@ -176,7 +186,6 @@ endif
 if (get_option('zsh_completions'))
 	zsh_files = files(
 		'completions/zsh/_sway',
-		'completions/zsh/_swaygrab',
 		'completions/zsh/_swaylock',
 		'completions/zsh/_swaymsg',
 	)
@@ -184,3 +193,15 @@ if (get_option('zsh_completions'))
 
 	install_data(zsh_files, install_dir: zsh_install_dir)
 endif
+
+if (get_option('bash_completions'))
+	bash_files = files(
+		'completions/bash/sway',
+		'completions/bash/swayidle',
+		'completions/bash/swaylock',
+		'completions/bash/swaymsg',
+	)
+	bash_install_dir = datadir + '/bash-completion/completions'
+
+	install_data(bash_files, install_dir: bash_install_dir)
+endif
diff --git a/meson_options.txt b/meson_options.txt
index 541ccf13..7a23c206 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -1,3 +1,5 @@
 option('sway_version', type : 'string', description: 'The version string reported in `sway --version`.')
 option('default_wallpaper', type: 'boolean', value: true, description: 'Install the default wallpaper.')
 option('zsh_completions', type: 'boolean', value: true, description: 'Install zsh shell completions.')
+option('bash_completions', type: 'boolean', value: true, description: 'Install bash shell completions.')
+option('enable-xwayland', type: 'boolean', value: true, description: 'Enable support for X11 applications')
diff --git a/security.d/00-defaults.in b/security.d/00-defaults.in
index e4626477..be7b9d06 100644
--- a/security.d/00-defaults.in
+++ b/security.d/00-defaults.in
@@ -12,7 +12,6 @@
 permit * fullscreen keyboard mouse
 permit @prefix@/bin/swaylock lock
 permit @prefix@/bin/swaybg background
-permit @prefix@/bin/swaygrab screenshot
 permit @prefix@/bin/swaybar panel
 
 # Configures enabled IPC features for specific programs
@@ -36,11 +35,6 @@ ipc @prefix@/bin/swaybar {
     }
 }
 
-ipc @prefix@/bin/swaygrab {
-    outputs enabled
-    tree enabled
-}
-
 ipc @prefix@/bin/swaylock {
     outputs enabled
 }
diff --git a/sway/commands.c b/sway/commands.c
index f1f03574..fdae1961 100644
--- a/sway/commands.c
+++ b/sway/commands.c
@@ -103,6 +103,7 @@ static struct cmd_handler handlers[] = {
 	{ "exec_always", cmd_exec_always },
 	{ "floating_maximum_size", cmd_floating_maximum_size },
 	{ "floating_minimum_size", cmd_floating_minimum_size },
+	{ "floating_modifier", cmd_floating_modifier },
 	{ "focus", cmd_focus },
 	{ "focus_follows_mouse", cmd_focus_follows_mouse },
 	{ "focus_wrapping", cmd_focus_wrapping },
@@ -148,6 +149,7 @@ static struct cmd_handler command_handlers[] = {
 	{ "reload", cmd_reload },
 	{ "rename", cmd_rename },
 	{ "resize", cmd_resize },
+	{ "scratchpad", cmd_scratchpad },
 	{ "split", cmd_split },
 	{ "splith", cmd_splith },
 	{ "splitt", cmd_splitt },
@@ -325,7 +327,7 @@ struct cmd_results *execute_command(char *_exec, struct sway_seat *seat) {
 	} while(head);
 cleanup:
 	free(exec);
-	free(views);
+	list_free(views);
 	if (!results) {
 		results = cmd_results_new(CMD_SUCCESS, NULL, NULL);
 	}
diff --git a/sway/commands/bind.c b/sway/commands/bind.c
index 83e9e432..8270b958 100644
--- a/sway/commands/bind.c
+++ b/sway/commands/bind.c
@@ -1,3 +1,4 @@
+#define _XOPEN_SOURCE 500
 #ifdef __linux__
 #include <linux/input-event-codes.h>
 #elif __FreeBSD__
@@ -5,9 +6,11 @@
 #endif
 #include <xkbcommon/xkbcommon.h>
 #include <xkbcommon/xkbcommon-names.h>
+#include <string.h>
 #include <strings.h>
 #include "sway/commands.h"
 #include "sway/config.h"
+#include "sway/ipc-server.h"
 #include "list.h"
 #include "log.h"
 #include "stringop.h"
@@ -27,6 +30,33 @@ void free_sway_binding(struct sway_binding *binding) {
 	free(binding);
 }
 
+static struct sway_binding *sway_binding_dup(struct sway_binding *sb) {
+	struct sway_binding *new_sb = calloc(1, sizeof(struct sway_binding));
+	if (!new_sb) {
+		return NULL;
+	}
+
+	new_sb->type = sb->type;
+	new_sb->order = sb->order;
+	new_sb->flags = sb->flags;
+	new_sb->modifiers = sb->modifiers;
+	new_sb->command = strdup(sb->command);
+
+	new_sb->keys = create_list();
+	int i;
+	for (i = 0; i < sb->keys->length; ++i) {
+		xkb_keysym_t *key = malloc(sizeof(xkb_keysym_t));
+		if (!key) {
+			free_sway_binding(new_sb);
+			return NULL;
+		}
+		*key = *(xkb_keysym_t *)sb->keys->items[i];
+		list_add(new_sb->keys, key);
+	}
+
+	return new_sb;
+}
+
 /**
  * Returns true if the bindings have the same key and modifier combinations.
  * Note that keyboard layout is not considered, so the bindings might actually
@@ -34,11 +64,14 @@ void free_sway_binding(struct sway_binding *binding) {
  */
 static bool binding_key_compare(struct sway_binding *binding_a,
 		struct sway_binding *binding_b) {
-	if (binding_a->release != binding_b->release) {
+	if (binding_a->type != binding_b->type) {
 		return false;
 	}
 
-	if (binding_a->bindcode != binding_b->bindcode) {
+	uint32_t conflict_generating_flags = BINDING_RELEASE | BINDING_BORDER
+			| BINDING_CONTENTS | BINDING_TITLEBAR;
+	if ((binding_a->flags & conflict_generating_flags) !=
+			(binding_b->flags & conflict_generating_flags)) {
 		return false;
 	}
 
@@ -69,6 +102,66 @@ static int key_qsort_cmp(const void *keyp_a, const void *keyp_b) {
 	return (key_a < key_b) ? -1 : ((key_a > key_b) ? 1 : 0);
 }
 
+
+/**
+ * From a keycode, bindcode, or bindsym name and the most likely binding type,
+ * identify the appropriate numeric value corresponding to the key. Return NULL
+ * and set *key_val if successful, otherwise return a specific error. Change
+ * the value of *type if the initial type guess was incorrect and if this
+ * was the first identified key.
+ */
+static struct cmd_results *identify_key(const char* name, bool first_key,
+		uint32_t* key_val, enum binding_input_type* type) {
+	if (*type == BINDING_KEYCODE) {
+		// check for keycode
+		xkb_keycode_t keycode = strtol(name, NULL, 10);
+		if (!xkb_keycode_is_legal_ext(keycode)) {
+			return cmd_results_new(CMD_INVALID, "bindcode",
+					"Invalid keycode '%s'", name);
+		}
+		*key_val = keycode;
+	} else {
+		// check for keysym
+		xkb_keysym_t keysym = xkb_keysym_from_name(name,
+				XKB_KEYSYM_CASE_INSENSITIVE);
+
+		// Check for mouse binding
+		uint32_t button = 0;
+		if (strncasecmp(name, "button", strlen("button")) == 0 &&
+				strlen(name) == strlen("button0")) {
+			button = name[strlen("button")] - '1' + BTN_LEFT;
+		}
+
+		if (*type == BINDING_KEYSYM) {
+			if (button) {
+				if (first_key) {
+					*type = BINDING_MOUSE;
+					*key_val = button;
+				} else {
+					return cmd_results_new(CMD_INVALID, "bindsym",
+							"Mixed button '%s' into key sequence", name);
+				}
+			} else if (keysym) {
+				*key_val = keysym;
+			} else {
+				return cmd_results_new(CMD_INVALID, "bindsym",
+						"Unknown key '%s'", name);
+			}
+		} else {
+			if (button) {
+				*key_val = button;
+			} else if (keysym) {
+				return cmd_results_new(CMD_INVALID, "bindsym",
+						"Mixed keysym '%s' into button sequence", name);
+			} else {
+				return cmd_results_new(CMD_INVALID, "bindsym",
+						"Unknown button '%s'", name);
+			}
+		}
+	}
+	return NULL;
+}
+
 static struct cmd_results *cmd_bindsym_or_bindcode(int argc, char **argv,
 		bool bindcode) {
 	const char *bindtype = bindcode ? "bindcode" : "bindsym";
@@ -85,22 +178,34 @@ static struct cmd_results *cmd_bindsym_or_bindcode(int argc, char **argv,
 	}
 	binding->keys = create_list();
 	binding->modifiers = 0;
-	binding->release = false;
-	binding->locked = false;
-	binding->bindcode = bindcode;
+	binding->flags = 0;
+	binding->type = bindcode ? BINDING_KEYCODE : BINDING_KEYSYM;
+
+	bool exclude_titlebar = false;
 
 	// Handle --release and --locked
 	while (argc > 0) {
 		if (strcmp("--release", argv[0]) == 0) {
-			binding->release = true;
+			binding->flags |= BINDING_RELEASE;
 		} else if (strcmp("--locked", argv[0]) == 0) {
-			binding->locked = true;
+			binding->flags |= BINDING_LOCKED;
+		} else if (strcmp("--whole-window", argv[0]) == 0) {
+			binding->flags |= BINDING_BORDER | BINDING_CONTENTS | BINDING_TITLEBAR;
+		} else if (strcmp("--border", argv[0]) == 0) {
+			binding->flags |= BINDING_BORDER;
+		} else if (strcmp("--exclude-titlebar", argv[0]) == 0) {
+			exclude_titlebar = true;
 		} else {
 			break;
 		}
 		argv++;
 		argc--;
 	}
+	if (binding->flags & (BINDING_BORDER | BINDING_CONTENTS | BINDING_TITLEBAR)
+			|| exclude_titlebar) {
+		binding->type = BINDING_MOUSE;
+	}
+
 	if (argc < 2) {
 		free_sway_binding(binding);
 		return cmd_results_new(CMD_FAILURE, bindtype,
@@ -119,64 +224,47 @@ static struct cmd_results *cmd_bindsym_or_bindcode(int argc, char **argv,
 			continue;
 		}
 
-		xkb_keycode_t keycode;
-		xkb_keysym_t keysym;
-		if (bindcode) {
-			// parse keycode
-			keycode = (int)strtol(split->items[i], NULL, 10);
-			if (!xkb_keycode_is_legal_ext(keycode)) {
-				error =
-					cmd_results_new(CMD_INVALID, "bindcode",
-						"Invalid keycode '%s'", (char *)split->items[i]);
-				free_sway_binding(binding);
-				list_free(split);
-				return error;
-			}
-		} else {
-			// Check for xkb key
-			 keysym = xkb_keysym_from_name(split->items[i],
-					XKB_KEYSYM_CASE_INSENSITIVE);
-
-			// Check for mouse binding
-			if (strncasecmp(split->items[i], "button", strlen("button")) == 0 &&
-					strlen(split->items[i]) == strlen("button0")) {
-				keysym = ((char *)split->items[i])[strlen("button")] - '1' + BTN_LEFT;
-			}
-			if (!keysym) {
-				struct cmd_results *ret = cmd_results_new(CMD_INVALID, "bindsym",
-						"Unknown key '%s'", (char *)split->items[i]);
-				free_sway_binding(binding);
-				free_flat_list(split);
-				return ret;
-			}
+		// Identify the key and possibly change binding->type
+		uint32_t key_val = 0;
+		error = identify_key(split->items[i], binding->keys->length == 0,
+				     &key_val, &binding->type);
+		if (error) {
+			free_sway_binding(binding);
+			list_free(split);
+			return error;
 		}
+
 		uint32_t *key = calloc(1, sizeof(uint32_t));
 		if (!key) {
 			free_sway_binding(binding);
 			free_flat_list(split);
 			return cmd_results_new(CMD_FAILURE, bindtype,
-					"Unable to allocate binding");
+					"Unable to allocate binding key");
 		}
-
-		if (bindcode) {
-			*key = (uint32_t)keycode;
-		} else {
-			*key = (uint32_t)keysym;
-		}
-
+		*key = key_val;
 		list_add(binding->keys, key);
 	}
 	free_flat_list(split);
 	binding->order = binding_order++;
 
+	// refine region of interest for mouse binding once we are certain
+	// that this is one
+	if (exclude_titlebar) {
+		binding->flags &= ~BINDING_TITLEBAR;
+	} else if (binding->type == BINDING_MOUSE) {
+		binding->flags |= BINDING_TITLEBAR;
+	}
+
 	// sort ascending
 	list_qsort(binding->keys, key_qsort_cmp);
 
 	list_t *mode_bindings;
-	if (bindcode) {
+	if (binding->type == BINDING_KEYCODE) {
 		mode_bindings = config->current_mode->keycode_bindings;
-	} else {
+	} else if (binding->type == BINDING_KEYSYM) {
 		mode_bindings = config->current_mode->keysym_bindings;
+	} else {
+		mode_bindings = config->current_mode->mouse_bindings;
 	}
 
 	// overwrite the binding if it already exists
@@ -209,3 +297,39 @@ struct cmd_results *cmd_bindsym(int argc, char **argv) {
 struct cmd_results *cmd_bindcode(int argc, char **argv) {
 	return cmd_bindsym_or_bindcode(argc, argv, true);
 }
+
+
+/**
+ * Execute the command associated to a binding
+ */
+void seat_execute_command(struct sway_seat *seat, struct sway_binding *binding) {
+	wlr_log(WLR_DEBUG, "running command for binding: %s",
+		binding->command);
+
+	struct sway_binding *binding_copy = binding;
+	bool reload = false;
+	// if this is a reload command we need to make a duplicate of the
+	// binding since it will be gone after the reload has completed.
+	if (strcasecmp(binding->command, "reload") == 0) {
+		reload = true;
+		binding_copy = sway_binding_dup(binding);
+		if (!binding_copy) {
+			wlr_log(WLR_ERROR, "Failed to duplicate binding during reload");
+			return;
+		}
+	}
+
+	config->handler_context.seat = seat;
+	struct cmd_results *results = execute_command(binding->command, NULL);
+	if (results->status == CMD_SUCCESS) {
+		ipc_event_binding(binding_copy);
+	} else {
+		wlr_log(WLR_DEBUG, "could not run command for binding: %s (%s)",
+			binding->command, results->error);
+	}
+
+	if (reload) { // free the binding if we made a copy
+		free_sway_binding(binding_copy);
+	}
+	free_cmd_results(results);
+}
diff --git a/sway/commands/exec_always.c b/sway/commands/exec_always.c
index c7727857..c730cb8b 100644
--- a/sway/commands/exec_always.c
+++ b/sway/commands/exec_always.c
@@ -4,6 +4,7 @@
 #include <string.h>
 #include <sys/wait.h>
 #include <unistd.h>
+#include <signal.h>
 #include "sway/commands.h"
 #include "sway/config.h"
 #include "sway/tree/container.h"
@@ -47,6 +48,9 @@ struct cmd_results *cmd_exec_always(int argc, char **argv) {
 	if ((pid = fork()) == 0) {
 		// Fork child process again
 		setsid();
+		sigset_t set;
+		sigemptyset(&set);
+		sigprocmask(SIG_SETMASK, &set, NULL);
 		close(fd[0]);
 		if ((child = fork()) == 0) {
 			close(fd[1]);
@@ -74,7 +78,7 @@ struct cmd_results *cmd_exec_always(int argc, char **argv) {
 	waitpid(pid, NULL, 0);
 	if (child > 0) {
 		wlr_log(WLR_DEBUG, "Child process created with pid %d", child);
-		// TODO: add PID to active workspace
+		workspace_record_pid(child);
 	} else {
 		return cmd_results_new(CMD_FAILURE, "exec_always",
 			"Second fork() failed");
diff --git a/sway/commands/floating.c b/sway/commands/floating.c
index 6ab56c3b..31de5ec3 100644
--- a/sway/commands/floating.c
+++ b/sway/commands/floating.c
@@ -17,9 +17,24 @@ struct cmd_results *cmd_floating(int argc, char **argv) {
 	}
 	struct sway_container *container =
 		config->handler_context.current_container;
-	if (container->type != C_VIEW) {
-		// TODO: This doesn't strictly speaking have to be true
-		return cmd_results_new(CMD_INVALID, "float", "Only views can float");
+	if (container->type == C_WORKSPACE && container->children->length == 0) {
+		return cmd_results_new(CMD_INVALID, "floating",
+				"Can't float an empty workspace");
+	}
+	if (container->type == C_WORKSPACE) {
+		// Wrap the workspace's children in a container so we can float it
+		struct sway_container *workspace = container;
+		container = container_wrap_children(container);
+		workspace->layout = L_HORIZ;
+		seat_set_focus(config->handler_context.seat, container);
+	}
+
+	// If the container is in a floating split container,
+	// operate on the split container instead of the child.
+	if (container_is_floating_or_child(container)) {
+		while (container->parent->layout != L_FLOATING) {
+			container = container->parent;
+		}
 	}
 
 	bool wants_floating;
diff --git a/sway/commands/floating_modifier.c b/sway/commands/floating_modifier.c
new file mode 100644
index 00000000..f5d2b3fe
--- /dev/null
+++ b/sway/commands/floating_modifier.c
@@ -0,0 +1,30 @@
+#include "strings.h"
+#include "sway/commands.h"
+#include "sway/config.h"
+#include "util.h"
+
+struct cmd_results *cmd_floating_modifier(int argc, char **argv) {
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "floating_modifier", EXPECTED_AT_LEAST, 1))) {
+		return error;
+	}
+
+	uint32_t mod = get_modifier_mask_by_name(argv[0]);
+	if (!mod) {
+		return cmd_results_new(CMD_INVALID, "floating_modifier",
+				"Invalid modifier");
+	}
+
+	if (argc == 1 || strcasecmp(argv[1], "normal") == 0) {
+		config->floating_mod_inverse = false;
+	} else if (strcasecmp(argv[1], "inverse") == 0) {
+		config->floating_mod_inverse = true;
+	} else {
+		return cmd_results_new(CMD_INVALID, "floating_modifier",
+				"Usage: floating_modifier <mod> [inverse|normal]");
+	}
+
+	config->floating_mod = mod;
+
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
diff --git a/sway/commands/focus.c b/sway/commands/focus.c
index 9cd8bfae..76d3f1dc 100644
--- a/sway/commands/focus.c
+++ b/sway/commands/focus.c
@@ -35,14 +35,25 @@ static struct cmd_results *focus_mode(struct sway_container *con,
 		struct sway_seat *seat, bool floating) {
 	struct sway_container *ws = con->type == C_WORKSPACE ?
 		con : container_parent(con, C_WORKSPACE);
-	struct sway_container *new_focus = ws;
-	if (floating) {
-		new_focus = ws->sway_workspace->floating;
-		if (new_focus->children->length == 0) {
-			return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+
+	// If the container is in a floating split container,
+	// operate on the split container instead of the child.
+	if (container_is_floating_or_child(con)) {
+		while (con->parent->layout != L_FLOATING) {
+			con = con->parent;
 		}
 	}
-	seat_set_focus(seat, seat_get_active_child(seat, new_focus));
+
+	struct sway_container *new_focus = NULL;
+	if (floating) {
+		new_focus = seat_get_focus_inactive(seat, ws->sway_workspace->floating);
+	} else {
+		new_focus = seat_get_focus_inactive_tiling(seat, ws);
+	}
+	if (!new_focus) {
+		new_focus = ws;
+	}
+	seat_set_focus(seat, new_focus);
 	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
 }
 
@@ -97,7 +108,7 @@ struct cmd_results *cmd_focus(int argc, char **argv) {
 	} else if (strcmp(argv[0], "tiling") == 0) {
 		return focus_mode(con, seat, false);
 	} else if (strcmp(argv[0], "mode_toggle") == 0) {
-		return focus_mode(con, seat, !container_is_floating(con));
+		return focus_mode(con, seat, !container_is_floating_or_child(con));
 	}
 
 	if (strcmp(argv[0], "output") == 0) {
diff --git a/sway/commands/focus_follows_mouse.c b/sway/commands/focus_follows_mouse.c
index 661e7852..0b0e334c 100644
--- a/sway/commands/focus_follows_mouse.c
+++ b/sway/commands/focus_follows_mouse.c
@@ -1,12 +1,14 @@
 #include <string.h>
 #include <strings.h>
 #include "sway/commands.h"
+#include "util.h"
 
 struct cmd_results *cmd_focus_follows_mouse(int argc, char **argv) {
 	struct cmd_results *error = NULL;
 	if ((error = checkarg(argc, "focus_follows_mouse", EXPECTED_EQUAL_TO, 1))) {
 		return error;
 	}
-	config->focus_follows_mouse = !strcasecmp(argv[0], "yes");
+	config->focus_follows_mouse =
+		parse_boolean(argv[0], config->focus_follows_mouse);
 	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
 }
diff --git a/sway/commands/focus_wrapping.c b/sway/commands/focus_wrapping.c
index 0a9e0bf2..562ee4f9 100644
--- a/sway/commands/focus_wrapping.c
+++ b/sway/commands/focus_wrapping.c
@@ -1,6 +1,7 @@
 #include <strings.h>
 #include "sway/commands.h"
 #include "sway/config.h"
+#include "util.h"
 
 struct cmd_results *cmd_focus_wrapping(int argc, char **argv) {
 	struct cmd_results *error = NULL;
@@ -8,15 +9,12 @@ struct cmd_results *cmd_focus_wrapping(int argc, char **argv) {
 		return error;
 	}
 
-	if (strcasecmp(argv[0], "no") == 0) {
-		config->focus_wrapping = WRAP_NO;
-	} else if (strcasecmp(argv[0], "yes") == 0) {
-		config->focus_wrapping = WRAP_YES;
-	} else if (strcasecmp(argv[0], "force") == 0) {
+	if (strcasecmp(argv[0], "force") == 0) {
 		config->focus_wrapping = WRAP_FORCE;
+	} else if (parse_boolean(argv[0], config->focus_wrapping == WRAP_YES)) {
+		config->focus_wrapping = WRAP_YES;
 	} else {
-		return cmd_results_new(CMD_INVALID, "focus_wrapping",
-				"Expected 'focus_wrapping yes|no|force'");
+		config->focus_wrapping = WRAP_NO;
 	}
 
 	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
diff --git a/sway/commands/force_focus_wrapping.c b/sway/commands/force_focus_wrapping.c
index bc1d067f..0892d9e9 100644
--- a/sway/commands/force_focus_wrapping.c
+++ b/sway/commands/force_focus_wrapping.c
@@ -1,6 +1,7 @@
 #include <strings.h>
 #include "sway/commands.h"
 #include "sway/config.h"
+#include "util.h"
 
 struct cmd_results *cmd_force_focus_wrapping(int argc, char **argv) {
 	struct cmd_results *error =
@@ -9,13 +10,10 @@ struct cmd_results *cmd_force_focus_wrapping(int argc, char **argv) {
 		return error;
 	}
 
-	if (strcasecmp(argv[0], "no") == 0) {
-		config->focus_wrapping = WRAP_YES;
-	} else if (strcasecmp(argv[0], "yes") == 0) {
+	if (parse_boolean(argv[0], config->focus_wrapping == WRAP_FORCE)) {
 		config->focus_wrapping = WRAP_FORCE;
 	} else {
-		return cmd_results_new(CMD_INVALID, "force_focus_wrapping",
-				"Expected 'force_focus_wrapping yes|no'");
+		config->focus_wrapping = WRAP_YES;
 	}
 
 	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
diff --git a/sway/commands/fullscreen.c b/sway/commands/fullscreen.c
index 0b5beaa2..5ad06e40 100644
--- a/sway/commands/fullscreen.c
+++ b/sway/commands/fullscreen.c
@@ -5,6 +5,7 @@
 #include "sway/tree/container.h"
 #include "sway/tree/view.h"
 #include "sway/tree/layout.h"
+#include "util.h"
 
 struct cmd_results *cmd_fullscreen(int argc, char **argv) {
 	struct cmd_results *error = NULL;
@@ -13,25 +14,24 @@ struct cmd_results *cmd_fullscreen(int argc, char **argv) {
 	}
 	struct sway_container *container =
 		config->handler_context.current_container;
-	if (container->type != C_VIEW) {
+	if (container->type == C_WORKSPACE && container->children->length == 0) {
 		return cmd_results_new(CMD_INVALID, "fullscreen",
-				"Only views can fullscreen");
+				"Can't fullscreen an empty workspace");
 	}
-	struct sway_view *view = container->sway_view;
-	bool wants_fullscreen;
+	if (container->type == C_WORKSPACE) {
+		// Wrap the workspace's children in a container so we can fullscreen it
+		struct sway_container *workspace = container;
+		container = container_wrap_children(container);
+		workspace->layout = L_HORIZ;
+		seat_set_focus(config->handler_context.seat, container);
+	}
+	bool enable = !container->is_fullscreen;
 
-	if (argc == 0 || strcmp(argv[0], "toggle") == 0) {
-		wants_fullscreen = !view->is_fullscreen;
-	} else if (strcmp(argv[0], "enable") == 0) {
-		wants_fullscreen = true;
-	} else if (strcmp(argv[0], "disable") == 0) {
-		wants_fullscreen = false;
-	} else {
-		return cmd_results_new(CMD_INVALID, "fullscreen",
-				"Expected 'fullscreen' or 'fullscreen <enable|disable|toggle>'");
+	if (argc) {
+		enable = parse_boolean(argv[0], container->is_fullscreen);
 	}
 
-	view_set_fullscreen(view, wants_fullscreen);
+	container_set_fullscreen(container, enable);
 
 	struct sway_container *workspace = container_parent(container, C_WORKSPACE);
 	arrange_windows(workspace->parent);
diff --git a/sway/commands/input.c b/sway/commands/input.c
index 5b203ea0..84888fbb 100644
--- a/sway/commands/input.c
+++ b/sway/commands/input.c
@@ -31,6 +31,12 @@ static struct cmd_handler input_handlers[] = {
 	{ "xkb_variant", input_cmd_xkb_variant },
 };
 
+// must be in order for the bsearch
+static struct cmd_handler input_config_handlers[] = {
+	{ "xkb_capslock", input_cmd_xkb_capslock },
+	{ "xkb_numlock", input_cmd_xkb_numlock },
+};
+
 struct cmd_results *cmd_input(int argc, char **argv) {
 	struct cmd_results *error = NULL;
 	if ((error = checkarg(argc, "input", EXPECTED_AT_LEAST, 2))) {
@@ -44,8 +50,21 @@ struct cmd_results *cmd_input(int argc, char **argv) {
 		return cmd_results_new(CMD_FAILURE, NULL, "Couldn't allocate config");
 	}
 
-	struct cmd_results *res = config_subcommand(argv + 1, argc - 1,
+	struct cmd_results *res;
+
+	if (find_handler(argv[1], input_config_handlers,
+			sizeof(input_config_handlers))) {
+		if (config->reading) {
+			res = config_subcommand(argv + 1, argc - 1,
+				input_config_handlers, sizeof(input_config_handlers));
+		} else {
+			res = cmd_results_new(CMD_FAILURE, "input",
+				"Can only be used in config file.");
+		}
+	} else {
+		res = config_subcommand(argv + 1, argc - 1,
 			input_handlers, sizeof(input_handlers));
+	}
 
 	free_input_config(config->handler_context.input_config);
 	config->handler_context.input_config = NULL;
diff --git a/sway/commands/input/drag_lock.c b/sway/commands/input/drag_lock.c
index 9e32816f..f9ddeef2 100644
--- a/sway/commands/input/drag_lock.c
+++ b/sway/commands/input/drag_lock.c
@@ -3,6 +3,7 @@
 #include "sway/config.h"
 #include "sway/commands.h"
 #include "sway/input/input-manager.h"
+#include "util.h"
 
 struct cmd_results *input_cmd_drag_lock(int argc, char **argv) {
 	struct cmd_results *error = NULL;
@@ -18,14 +19,10 @@ struct cmd_results *input_cmd_drag_lock(int argc, char **argv) {
 	struct input_config *new_config =
 		new_input_config(current_input_config->identifier);
 
-	if (strcasecmp(argv[0], "enabled") == 0) {
+	if (parse_boolean(argv[0], true)) {
 		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 {
-		free_input_config(new_config);
-		return cmd_results_new(CMD_INVALID, "drag_lock",
-			"Expected 'drag_lock <enabled|disabled>'");
+		new_config->drag_lock = LIBINPUT_CONFIG_DRAG_LOCK_DISABLED;
 	}
 
 	apply_input_config(new_config);
diff --git a/sway/commands/input/dwt.c b/sway/commands/input/dwt.c
index 73937507..15134268 100644
--- a/sway/commands/input/dwt.c
+++ b/sway/commands/input/dwt.c
@@ -3,6 +3,7 @@
 #include "sway/config.h"
 #include "sway/commands.h"
 #include "sway/input/input-manager.h"
+#include "util.h"
 
 struct cmd_results *input_cmd_dwt(int argc, char **argv) {
 	struct cmd_results *error = NULL;
@@ -17,14 +18,10 @@ struct cmd_results *input_cmd_dwt(int argc, char **argv) {
 	struct input_config *new_config =
 		new_input_config(current_input_config->identifier);
 
-	if (strcasecmp(argv[0], "enabled") == 0) {
+	if (parse_boolean(argv[0], true)) {
 		new_config->dwt = LIBINPUT_CONFIG_DWT_ENABLED;
-	} else if (strcasecmp(argv[0], "disabled") == 0) {
-		new_config->dwt = LIBINPUT_CONFIG_DWT_DISABLED;
 	} else {
-		free_input_config(new_config);
-		return cmd_results_new(CMD_INVALID, "dwt",
-			"Expected 'dwt <enabled|disabled>'");
+		new_config->dwt = LIBINPUT_CONFIG_DWT_DISABLED;
 	}
 
 	apply_input_config(new_config);
diff --git a/sway/commands/input/left_handed.c b/sway/commands/input/left_handed.c
index 769ce98c..e770043a 100644
--- a/sway/commands/input/left_handed.c
+++ b/sway/commands/input/left_handed.c
@@ -3,6 +3,7 @@
 #include "sway/config.h"
 #include "sway/commands.h"
 #include "sway/input/input-manager.h"
+#include "util.h"
 
 struct cmd_results *input_cmd_left_handed(int argc, char **argv) {
 	struct cmd_results *error = NULL;
@@ -18,15 +19,7 @@ struct cmd_results *input_cmd_left_handed(int argc, char **argv) {
 	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 {
-		free_input_config(new_config);
-		return cmd_results_new(CMD_INVALID, "left_handed",
-			"Expected 'left_handed <enabled|disabled>'");
-	}
+	new_config->left_handed = parse_boolean(argv[0], true);
 
 	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
index 7ca01629..414d4d2b 100644
--- a/sway/commands/input/middle_emulation.c
+++ b/sway/commands/input/middle_emulation.c
@@ -3,6 +3,7 @@
 #include "sway/config.h"
 #include "sway/commands.h"
 #include "sway/input/input-manager.h"
+#include "util.h"
 
 struct cmd_results *input_cmd_middle_emulation(int argc, char **argv) {
 	struct cmd_results *error = NULL;
@@ -18,15 +19,11 @@ struct cmd_results *input_cmd_middle_emulation(int argc, char **argv) {
 	struct input_config *new_config =
 		new_input_config(current_input_config->identifier);
 
-	if (strcasecmp(argv[0], "enabled") == 0) {
+	if (parse_boolean(argv[0], true)) {
 		new_config->middle_emulation = LIBINPUT_CONFIG_MIDDLE_EMULATION_ENABLED;
-	} else if (strcasecmp(argv[0], "disabled") == 0) {
+	} else {
 		new_config->middle_emulation =
 			LIBINPUT_CONFIG_MIDDLE_EMULATION_DISABLED;
-	} else {
-		free_input_config(new_config);
-		return cmd_results_new(CMD_INVALID, "middle_emulation",
-			"Expected 'middle_emulation <enabled|disabled>'");
 	}
 
 	apply_input_config(new_config);
diff --git a/sway/commands/input/natural_scroll.c b/sway/commands/input/natural_scroll.c
index 55236790..77c3ff00 100644
--- a/sway/commands/input/natural_scroll.c
+++ b/sway/commands/input/natural_scroll.c
@@ -3,6 +3,7 @@
 #include "sway/config.h"
 #include "sway/commands.h"
 #include "sway/input/input-manager.h"
+#include "util.h"
 
 struct cmd_results *input_cmd_natural_scroll(int argc, char **argv) {
 	struct cmd_results *error = NULL;
@@ -18,15 +19,7 @@ struct cmd_results *input_cmd_natural_scroll(int argc, char **argv) {
 	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 {
-		free_input_config(new_config);
-		return cmd_results_new(CMD_INVALID, "natural_scroll",
-			"Expected 'natural_scroll <enabled|disabled>'");
-	}
+	new_config->natural_scroll = parse_boolean(argv[0], true);
 
 	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
index a8d1a10c..ac3b8237 100644
--- a/sway/commands/input/tap.c
+++ b/sway/commands/input/tap.c
@@ -4,6 +4,7 @@
 #include "sway/commands.h"
 #include "sway/input/input-manager.h"
 #include "log.h"
+#include "util.h"
 
 struct cmd_results *input_cmd_tap(int argc, char **argv) {
 	struct cmd_results *error = NULL;
@@ -18,14 +19,10 @@ struct cmd_results *input_cmd_tap(int argc, char **argv) {
 	struct input_config *new_config =
 		new_input_config(current_input_config->identifier);
 
-	if (strcasecmp(argv[0], "enabled") == 0) {
+	if (parse_boolean(argv[0], true)) {
 		new_config->tap = LIBINPUT_CONFIG_TAP_ENABLED;
-	} else if (strcasecmp(argv[0], "disabled") == 0) {
-		new_config->tap = LIBINPUT_CONFIG_TAP_DISABLED;
 	} else {
-		free_input_config(new_config);
-		return cmd_results_new(CMD_INVALID, "tap",
-			"Expected 'tap <enabled|disabled>'");
+		new_config->tap = LIBINPUT_CONFIG_TAP_DISABLED;
 	}
 
 	wlr_log(WLR_DEBUG, "apply-tap for device: %s",
diff --git a/sway/commands/input/xkb_capslock.c b/sway/commands/input/xkb_capslock.c
new file mode 100644
index 00000000..5442c463
--- /dev/null
+++ b/sway/commands/input/xkb_capslock.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"
+
+struct cmd_results *input_cmd_xkb_capslock(int argc, char **argv) {
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "xkb_capslock", EXPECTED_AT_LEAST, 1))) {
+		return error;
+	}
+	struct input_config *current_input_config =
+		config->handler_context.input_config;
+	if (!current_input_config) {
+		return cmd_results_new(CMD_FAILURE, "xkb_capslock", 
+			"No input device defined.");
+	}
+	struct input_config *new_config =
+		new_input_config(current_input_config->identifier);
+
+	if (strcasecmp(argv[0], "enabled") == 0) {
+		new_config->xkb_capslock = 1;
+	} else if (strcasecmp(argv[0], "disabled") == 0) {
+		new_config->xkb_capslock = 0; 
+	} else {
+		free_input_config(new_config);
+		return cmd_results_new(CMD_INVALID, "xkb_capslock",
+			"Expected 'xkb_capslock <enabled|disabled>'");
+	}
+
+	apply_input_config(new_config);
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
diff --git a/sway/commands/input/xkb_numlock.c b/sway/commands/input/xkb_numlock.c
new file mode 100644
index 00000000..39675366
--- /dev/null
+++ b/sway/commands/input/xkb_numlock.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"
+
+struct cmd_results *input_cmd_xkb_numlock(int argc, char **argv) {
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "xkb_numlock", EXPECTED_AT_LEAST, 1))) {
+		return error;
+	}
+	struct input_config *current_input_config =
+		config->handler_context.input_config;
+	if (!current_input_config) {
+		return cmd_results_new(CMD_FAILURE, "xkb_numlock", 
+			"No input device defined.");
+	}
+	struct input_config *new_config =
+		new_input_config(current_input_config->identifier);
+
+	if (strcasecmp(argv[0], "enabled") == 0) {
+		new_config->xkb_numlock = 1;
+	} else if (strcasecmp(argv[0], "disabled") == 0) {
+		new_config->xkb_numlock = 0; 
+	} else {
+		free_input_config(new_config);
+		return cmd_results_new(CMD_INVALID, "xkb_numlock",
+			"Expected 'xkb_numlock <enabled|disabled>'");
+	}
+
+	apply_input_config(new_config);
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
diff --git a/sway/commands/mark.c b/sway/commands/mark.c
index 5a897e69..9ea8c301 100644
--- a/sway/commands/mark.c
+++ b/sway/commands/mark.c
@@ -58,7 +58,7 @@ struct cmd_results *cmd_mark(int argc, char **argv) {
 	view_find_and_unmark(mark);
 
 	if (!toggle || !had_mark) {
-		list_add(view->marks, strdup(mark));
+		view_add_mark(view, mark);
 	}
 
 	free(mark);
diff --git a/sway/commands/mode.c b/sway/commands/mode.c
index b460fcb5..637ca45e 100644
--- a/sway/commands/mode.c
+++ b/sway/commands/mode.c
@@ -56,6 +56,7 @@ struct cmd_results *cmd_mode(int argc, char **argv) {
 		mode->name = strdup(mode_name);
 		mode->keysym_bindings = create_list();
 		mode->keycode_bindings = create_list();
+		mode->mouse_bindings = create_list();
 		mode->pango = pango;
 		list_add(config->modes, mode);
 	}
diff --git a/sway/commands/move.c b/sway/commands/move.c
index 6ec050a8..702b42d9 100644
--- a/sway/commands/move.c
+++ b/sway/commands/move.c
@@ -9,6 +9,7 @@
 #include "sway/input/cursor.h"
 #include "sway/input/seat.h"
 #include "sway/output.h"
+#include "sway/scratchpad.h"
 #include "sway/tree/arrange.h"
 #include "sway/tree/container.h"
 #include "sway/tree/layout.h"
@@ -58,8 +59,7 @@ static struct cmd_results *cmd_move_container(struct sway_container *current,
 			&& strcasecmp(argv[2], "workspace") == 0) {
 		// move container to workspace x
 		if (current->type == C_WORKSPACE) {
-			// TODO: Wrap children in a container and move that
-			return cmd_results_new(CMD_FAILURE, "move", "Unimplemented");
+			current = container_wrap_children(current);
 		} else if (current->type != C_CONTAINER && current->type != C_VIEW) {
 			return cmd_results_new(CMD_FAILURE, "move",
 					"Can only move containers and views.");
@@ -97,7 +97,7 @@ static struct cmd_results *cmd_move_container(struct sway_container *current,
 		container_move_to(current, destination);
 		struct sway_container *focus = seat_get_focus_inactive(
 				config->handler_context.seat, old_parent);
-		seat_set_focus(config->handler_context.seat, focus);
+		seat_set_focus_warp(config->handler_context.seat, focus, true, false);
 		container_reap_empty(old_parent);
 		container_reap_empty(destination->parent);
 
@@ -134,7 +134,7 @@ static struct cmd_results *cmd_move_container(struct sway_container *current,
 		struct sway_container *old_parent = current->parent;
 		struct sway_container *old_ws = container_parent(current, C_WORKSPACE);
 		container_move_to(current, focus);
-		seat_set_focus(config->handler_context.seat, old_parent);
+		seat_set_focus_warp(config->handler_context.seat, old_parent, true, false);
 		container_reap_empty(old_parent);
 		container_reap_empty(focus->parent);
 
@@ -195,7 +195,7 @@ static struct cmd_results *move_in_direction(struct sway_container *container,
 				"Cannot move workspaces in a direction");
 	}
 	if (container_is_floating(container)) {
-		if (container->type == C_VIEW && container->sway_view->is_fullscreen) {
+		if (container->is_fullscreen) {
 			return cmd_results_new(CMD_FAILURE, "move",
 					"Cannot move fullscreen floating container");
 		}
@@ -296,6 +296,34 @@ static struct cmd_results *move_to_position(struct sway_container *container,
 	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
 }
 
+static struct cmd_results *move_to_scratchpad(struct sway_container *con) {
+	if (con->type == C_WORKSPACE && con->children->length == 0) {
+		return cmd_results_new(CMD_INVALID, "move",
+				"Can't move an empty workspace to the scratchpad");
+	}
+	if (con->type == C_WORKSPACE) {
+		// Wrap the workspace's children in a container
+		struct sway_container *workspace = con;
+		con = container_wrap_children(con);
+		workspace->layout = L_HORIZ;
+	}
+
+	// If the container is in a floating split container,
+	// operate on the split container instead of the child.
+	if (container_is_floating_or_child(con)) {
+		while (con->parent->layout != L_FLOATING) {
+			con = con->parent;
+		}
+	}
+
+	if (con->scratchpad) {
+		return cmd_results_new(CMD_INVALID, "move",
+				"Container is already in the scratchpad");
+	}
+	scratchpad_add_container(con);
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
+
 struct cmd_results *cmd_move(int argc, char **argv) {
 	struct cmd_results *error = NULL;
 	if ((error = checkarg(argc, "move", EXPECTED_AT_LEAST, 1))) {
@@ -317,10 +345,9 @@ struct cmd_results *cmd_move(int argc, char **argv) {
 	} else if (strcasecmp(argv[0], "workspace") == 0) {
 		return cmd_move_workspace(current, argc, argv);
 	} else if (strcasecmp(argv[0], "scratchpad") == 0
-			|| (strcasecmp(argv[0], "to") == 0
+			|| (strcasecmp(argv[0], "to") == 0 && argc == 2
 				&& strcasecmp(argv[1], "scratchpad") == 0)) {
-		// TODO: scratchpad
-		return cmd_results_new(CMD_FAILURE, "move", "Unimplemented");
+		return move_to_scratchpad(current);
 	} else if (strcasecmp(argv[0], "position") == 0) {
 		return move_to_position(current, argc, argv);
 	} else if (strcasecmp(argv[0], "absolute") == 0) {
diff --git a/sway/commands/output/dpms.c b/sway/commands/output/dpms.c
index 0959ea6b..3492061e 100644
--- a/sway/commands/output/dpms.c
+++ b/sway/commands/output/dpms.c
@@ -1,5 +1,6 @@
 #include "sway/commands.h"
 #include "sway/config.h"
+#include "util.h"
 
 struct cmd_results *output_cmd_dpms(int argc, char **argv) {
 	if (!config->handler_context.output_config) {
@@ -9,13 +10,10 @@ struct cmd_results *output_cmd_dpms(int argc, char **argv) {
 		return cmd_results_new(CMD_INVALID, "output", "Missing dpms argument.");
 	}
 
-	if (strcmp(*argv, "on") == 0) {
+	if (parse_boolean(argv[0], true)) {
 		config->handler_context.output_config->dpms_state = DPMS_ON;
-	} else if (strcmp(*argv, "off") == 0) {
-		config->handler_context.output_config->dpms_state = DPMS_OFF;
 	} else {
-		return cmd_results_new(CMD_INVALID, "output",
-				"Invalid dpms state, valid states are on/off.");
+		config->handler_context.output_config->dpms_state = DPMS_OFF;
 	}
 
 	config->handler_context.leftovers.argc = argc - 1;
diff --git a/sway/commands/reload.c b/sway/commands/reload.c
index cea6a94b..5c1b19b4 100644
--- a/sway/commands/reload.c
+++ b/sway/commands/reload.c
@@ -1,17 +1,46 @@
+#define _XOPEN_SOURCE 500
+#include <string.h>
 #include "sway/commands.h"
 #include "sway/config.h"
+#include "sway/ipc-server.h"
 #include "sway/tree/arrange.h"
+#include "list.h"
 
 struct cmd_results *cmd_reload(int argc, char **argv) {
 	struct cmd_results *error = NULL;
 	if ((error = checkarg(argc, "reload", EXPECTED_EQUAL_TO, 0))) {
 		return error;
 	}
+
+	// store bar ids to check against new bars for barconfig_update events
+	list_t *bar_ids = create_list();
+	for (int i = 0; i < config->bars->length; ++i) {
+		struct bar_config *bar = config->bars->items[i];
+		list_add(bar_ids, strdup(bar->id));
+	}
+
 	if (!load_main_config(config->current_config_path, true)) {
 		return cmd_results_new(CMD_FAILURE, "reload", "Error(s) reloading config.");
 	}
+	ipc_event_workspace(NULL, NULL, "reload");
 
 	load_swaybars();
+
+	for (int i = 0; i < config->bars->length; ++i) {
+		struct bar_config *bar = config->bars->items[i];
+		for (int j = 0; j < bar_ids->length; ++j) {
+			if (strcmp(bar->id, bar_ids->items[j]) == 0) {
+				ipc_event_barconfig_update(bar);
+				break;
+			}
+		}
+	}
+
+	for (int i = 0; i < bar_ids->length; ++i) {
+		free(bar_ids->items[i]);
+	}
+	list_free(bar_ids);
+
 	arrange_windows(&root_container);
 	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
 }
diff --git a/sway/commands/scratchpad.c b/sway/commands/scratchpad.c
new file mode 100644
index 00000000..01a91d65
--- /dev/null
+++ b/sway/commands/scratchpad.c
@@ -0,0 +1,44 @@
+#include "log.h"
+#include "sway/commands.h"
+#include "sway/config.h"
+#include "sway/scratchpad.h"
+#include "sway/tree/container.h"
+
+struct cmd_results *cmd_scratchpad(int argc, char **argv) {
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "scratchpad", EXPECTED_EQUAL_TO, 1))) {
+		return error;
+	}
+	if (strcmp(argv[0], "show") != 0) {
+		return cmd_results_new(CMD_INVALID, "scratchpad",
+				"Expected 'scratchpad show'");
+	}
+	if (!root_container.sway_root->scratchpad->length) {
+		return cmd_results_new(CMD_INVALID, "scratchpad",
+				"Scratchpad is empty");
+	}
+
+	if (config->handler_context.using_criteria) {
+		struct sway_container *con = config->handler_context.current_container;
+
+		// If the container is in a floating split container,
+		// operate on the split container instead of the child.
+		if (container_is_floating_or_child(con)) {
+			while (con->parent->layout != L_FLOATING) {
+				con = con->parent;
+			}
+		}
+
+		// If using criteria, this command is executed for every container which
+		// matches the criteria. If this container isn't in the scratchpad,
+		// we'll just silently return a success.
+		if (!con->scratchpad) {
+			return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+		}
+		scratchpad_toggle_container(con);
+	} else {
+		scratchpad_toggle_auto();
+	}
+
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
diff --git a/sway/commands/show_marks.c b/sway/commands/show_marks.c
index c7fdc538..434a0e27 100644
--- a/sway/commands/show_marks.c
+++ b/sway/commands/show_marks.c
@@ -7,6 +7,7 @@
 #include "list.h"
 #include "log.h"
 #include "stringop.h"
+#include "util.h"
 
 static void rebuild_marks_iterator(struct sway_container *con, void *data) {
 	if (con->type == C_VIEW) {
@@ -20,14 +21,7 @@ struct cmd_results *cmd_show_marks(int argc, char **argv) {
 		return error;
 	}
 
-	if (strcmp(*argv, "yes") == 0) {
-		config->show_marks = true;
-	} else if (strcmp(*argv, "no") == 0) {
-		config->show_marks = false;
-	} else {
-		return cmd_results_new(CMD_INVALID, "show_marks",
-				"Expected 'show_marks <yes|no>'");
-	}
+	config->show_marks = parse_boolean(argv[0], config->show_marks);
 
 	if (config->show_marks) {
 		container_for_each_descendant_dfs(&root_container,
diff --git a/sway/commands/split.c b/sway/commands/split.c
index 313799da..a8eddf54 100644
--- a/sway/commands/split.c
+++ b/sway/commands/split.c
@@ -10,10 +10,6 @@
 
 static struct cmd_results *do_split(int layout) {
 	struct sway_container *con = config->handler_context.current_container;
-	if (container_is_floating(con)) {
-		return cmd_results_new(CMD_FAILURE, "split",
-			"Can't split a floating view");
-	}
 	struct sway_container *parent = container_split(con, layout);
 	container_create_notify(parent);
 	arrange_windows(parent->parent);
diff --git a/sway/commands/swap.c b/sway/commands/swap.c
index 2fc88308..4e3a9cce 100644
--- a/sway/commands/swap.c
+++ b/sway/commands/swap.c
@@ -1,5 +1,6 @@
 #include <strings.h>
 #include <wlr/util/log.h>
+#include "config.h"
 #include "sway/commands.h"
 #include "sway/tree/arrange.h"
 #include "sway/tree/layout.h"
@@ -14,10 +15,14 @@ static bool test_con_id(struct sway_container *container, void *con_id) {
 }
 
 static bool test_id(struct sway_container *container, void *id) {
+#ifdef HAVE_XWAYLAND
 	xcb_window_t *wid = id;
 	return (container->type == C_VIEW
 			&& container->sway_view->type == SWAY_VIEW_XWAYLAND
 			&& container->sway_view->wlr_xwayland_surface->window_id == *wid);
+#else
+	return false;
+#endif
 }
 
 static bool test_mark(struct sway_container *container, void *mark) {
@@ -43,8 +48,10 @@ struct cmd_results *cmd_swap(int argc, char **argv) {
 
 	char *value = join_args(argv + 3, argc - 3);
 	if (strcasecmp(argv[2], "id") == 0) {
+#ifdef HAVE_XWAYLAND
 		xcb_window_t id = strtol(value, NULL, 0);
 		other = container_find(&root_container, test_id, (void *)&id);
+#endif
 	} else if (strcasecmp(argv[2], "con_id") == 0) {
 		size_t con_id = atoi(value);
 		other = container_find(&root_container, test_con_id, (void *)con_id);
diff --git a/sway/commands/urgent.c b/sway/commands/urgent.c
index d199858a..51c497c4 100644
--- a/sway/commands/urgent.c
+++ b/sway/commands/urgent.c
@@ -5,6 +5,7 @@
 #include "sway/tree/container.h"
 #include "sway/tree/view.h"
 #include "sway/tree/layout.h"
+#include "util.h"
 
 struct cmd_results *cmd_urgent(int argc, char **argv) {
 	struct cmd_results *error = NULL;
@@ -19,17 +20,12 @@ struct cmd_results *cmd_urgent(int argc, char **argv) {
 	}
 	struct sway_view *view = container->sway_view;
 
-	if (strcmp(argv[0], "enable") == 0) {
-		view_set_urgent(view, true);
-	} else if (strcmp(argv[0], "disable") == 0) {
-		view_set_urgent(view, false);
-	} else if (strcmp(argv[0], "allow") == 0) {
+	if (strcmp(argv[0], "allow") == 0) {
 		view->allow_request_urgent = true;
 	} else if (strcmp(argv[0], "deny") == 0) {
 		view->allow_request_urgent = false;
 	} else {
-		return cmd_results_new(CMD_INVALID, "urgent",
-				"Expected 'urgent <enable|disable|allow|deny>'");
+		view_set_urgent(view, parse_boolean(argv[0], view_is_urgent(view)));
 	}
 
 	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
diff --git a/sway/config.c b/sway/config.c
index ed624bfa..2afffab1 100644
--- a/sway/config.c
+++ b/sway/config.c
@@ -56,6 +56,12 @@ static void free_mode(struct sway_mode *mode) {
 		}
 		list_free(mode->keycode_bindings);
 	}
+	if (mode->mouse_bindings) {
+		for (i = 0; i < mode->mouse_bindings->length; i++) {
+			free_sway_binding(mode->mouse_bindings->items[i]);
+		}
+		list_free(mode->mouse_bindings);
+	}
 	free(mode);
 }
 
@@ -87,7 +93,6 @@ void free_config(struct sway_config *config) {
 	}
 	list_free(config->cmd_queue);
 	list_free(config->workspace_outputs);
-	list_free(config->pid_workspaces);
 	if (config->output_configs) {
 		for (int i = 0; i < config->output_configs->length; i++) {
 			free_output_config(config->output_configs->items[i]);
@@ -157,7 +162,6 @@ static void config_defaults(struct sway_config *config) {
 	if (!(config->modes = create_list())) goto cleanup;
 	if (!(config->bars = create_list())) goto cleanup;
 	if (!(config->workspace_outputs = create_list())) goto cleanup;
-	if (!(config->pid_workspaces = create_list())) goto cleanup;
 	if (!(config->criteria = create_list())) goto cleanup;
 	if (!(config->no_focus = create_list())) goto cleanup;
 	if (!(config->input_configs = create_list())) goto cleanup;
@@ -172,9 +176,11 @@ static void config_defaults(struct sway_config *config) {
 	strcpy(config->current_mode->name, "default");
 	if (!(config->current_mode->keysym_bindings = create_list())) goto cleanup;
 	if (!(config->current_mode->keycode_bindings = create_list())) goto cleanup;
+	if (!(config->current_mode->mouse_bindings = create_list())) goto cleanup;
 	list_add(config->modes, config->current_mode);
 
 	config->floating_mod = 0;
+	config->floating_mod_inverse = false;
 	config->dragging_key = BTN_LEFT;
 	config->resizing_key = BTN_RIGHT;
 
diff --git a/sway/config/bar.c b/sway/config/bar.c
index 3a74331e..ae9383d6 100644
--- a/sway/config/bar.c
+++ b/sway/config/bar.c
@@ -10,6 +10,7 @@
 #include <sys/stat.h>
 #include <signal.h>
 #include <strings.h>
+#include <signal.h>
 #include "sway/config.h"
 #include "stringop.h"
 #include "list.h"
@@ -175,6 +176,9 @@ void invoke_swaybar(struct bar_config *bar) {
 	if (bar->pid == 0) {
 		setpgid(0, 0);
 		close(filedes[0]);
+		sigset_t set;
+		sigemptyset(&set);
+		sigprocmask(SIG_SETMASK, &set, NULL);
 
 		// run custom swaybar
 		size_t len = snprintf(NULL, 0, "%s -b %s",
diff --git a/sway/config/input.c b/sway/config/input.c
index 8d687a6d..9885e85c 100644
--- a/sway/config/input.c
+++ b/sway/config/input.c
@@ -33,6 +33,8 @@ struct input_config *new_input_config(const char* identifier) {
 	input->left_handed = INT_MIN;
 	input->repeat_delay = INT_MIN;
 	input->repeat_rate = INT_MIN;
+	input->xkb_numlock = INT_MIN;
+	input->xkb_capslock = INT_MIN;
 
 	return input;
 }
@@ -104,6 +106,12 @@ void merge_input_config(struct input_config *dst, struct input_config *src) {
 		free(dst->xkb_variant);
 		dst->xkb_variant = strdup(src->xkb_variant);
 	}
+	if (src->xkb_numlock != INT_MIN) {
+		dst->xkb_numlock = src->xkb_numlock;
+	}
+	if (src->xkb_capslock != INT_MIN) {
+		dst->xkb_capslock = src->xkb_capslock;
+	}
 	if (src->mapped_from_region) {
 		free(dst->mapped_from_region);
 		dst->mapped_from_region =
diff --git a/sway/criteria.c b/sway/criteria.c
index e2b248de..39d300ea 100644
--- a/sway/criteria.c
+++ b/sway/criteria.c
@@ -10,6 +10,7 @@
 #include "stringop.h"
 #include "list.h"
 #include "log.h"
+#include "config.h"
 
 bool criteria_is_empty(struct criteria *criteria) {
 	return !criteria->title
@@ -19,7 +20,9 @@ bool criteria_is_empty(struct criteria *criteria) {
 		&& !criteria->instance
 		&& !criteria->con_mark
 		&& !criteria->con_id
+#ifdef HAVE_XWAYLAND
 		&& !criteria->id
+#endif
 		&& !criteria->window_role
 		&& !criteria->window_type
 		&& !criteria->floating
@@ -127,12 +130,14 @@ static bool criteria_matches_view(struct criteria *criteria,
 		}
 	}
 
+#ifdef HAVE_XWAYLAND
 	if (criteria->id) { // X11 window ID
 		uint32_t x11_window_id = view_get_x11_window_id(view);
 		if (!x11_window_id || x11_window_id != criteria->id) {
 			return false;
 		}
 	}
+#endif
 
 	if (criteria->window_role) {
 		// TODO
@@ -225,6 +230,15 @@ list_t *criteria_get_views(struct criteria *criteria) {
 	};
 	container_for_each_descendant_dfs(&root_container,
 		criteria_get_views_iterator, &data);
+
+	// Scratchpad items which are hidden are not in the tree.
+	for (int i = 0; i < root_container.sway_root->scratchpad->length; ++i) {
+		struct sway_container *con =
+			root_container.sway_root->scratchpad->items[i];
+		if (!con->parent) {
+			criteria_get_views_iterator(con, &data);
+		}
+	}
 	return matches;
 }
 
@@ -256,7 +270,9 @@ enum criteria_token {
 	T_CON_ID,
 	T_CON_MARK,
 	T_FLOATING,
+#ifdef HAVE_XWAYLAND
 	T_ID,
+#endif
 	T_INSTANCE,
 	T_SHELL,
 	T_TILING,
@@ -278,8 +294,10 @@ static enum criteria_token token_from_name(char *name) {
 		return T_CON_ID;
 	} else if (strcmp(name, "con_mark") == 0) {
 		return T_CON_MARK;
+#ifdef HAVE_XWAYLAND
 	} else if (strcmp(name, "id") == 0) {
 		return T_ID;
+#endif
 	} else if (strcmp(name, "instance") == 0) {
 		return T_INSTANCE;
 	} else if (strcmp(name, "shell") == 0) {
@@ -346,7 +364,9 @@ static char *get_focused_prop(enum criteria_token token) {
 	case T_CON_ID: // These do not support __focused__
 	case T_CON_MARK:
 	case T_FLOATING:
+#ifdef HAVE_XWAYLAND
 	case T_ID:
+#endif
 	case T_TILING:
 	case T_URGENT:
 	case T_WINDOW_TYPE:
@@ -417,12 +437,14 @@ static bool parse_token(struct criteria *criteria, char *name, char *value) {
 	case T_WINDOW_TYPE:
 		// TODO: This is a string but will be stored as an enum or integer
 		break;
+#ifdef HAVE_XWAYLAND
 	case T_ID:
 		criteria->id = strtoul(effective_value, &endptr, 10);
 		if (*endptr != 0) {
 			error = strdup("The value for 'id' should be numeric");
 		}
 		break;
+#endif
 	case T_FLOATING:
 		criteria->floating = true;
 		break;
diff --git a/sway/desktop/layer_shell.c b/sway/desktop/layer_shell.c
index a7d96717..a2935883 100644
--- a/sway/desktop/layer_shell.c
+++ b/sway/desktop/layer_shell.c
@@ -7,11 +7,13 @@
 #include <wlr/types/wlr_output_damage.h>
 #include <wlr/types/wlr_output.h>
 #include <wlr/util/log.h>
+#include "sway/desktop/transaction.h"
 #include "sway/input/input-manager.h"
 #include "sway/input/seat.h"
 #include "sway/layers.h"
 #include "sway/output.h"
 #include "sway/server.h"
+#include "sway/tree/arrange.h"
 #include "sway/tree/layout.h"
 #include "log.h"
 
@@ -245,6 +247,9 @@ static void handle_surface_commit(struct wl_listener *listener, void *data) {
 		output_damage_surface(output, layer->geo.x, layer->geo.y,
 			layer_surface->surface, false);
 	}
+
+	arrange_windows(output->swayc);
+	transaction_commit_dirty();
 }
 
 static void unmap(struct sway_layer_surface *sway_layer) {
@@ -282,6 +287,8 @@ static void handle_destroy(struct wl_listener *listener, void *data) {
 		struct sway_output *output = sway_layer->layer_surface->output->data;
 		if (output != NULL && output->swayc != NULL) {
 			arrange_layers(output);
+			arrange_windows(output->swayc);
+			transaction_commit_dirty();
 		}
 		wl_list_remove(&sway_layer->output_destroy.link);
 		sway_layer->layer_surface->output = NULL;
diff --git a/sway/desktop/output.c b/sway/desktop/output.c
index a9808406..66747a3f 100644
--- a/sway/desktop/output.c
+++ b/sway/desktop/output.c
@@ -14,6 +14,7 @@
 #include <wlr/types/wlr_surface.h>
 #include <wlr/util/region.h>
 #include "log.h"
+#include "config.h"
 #include "sway/config.h"
 #include "sway/input/input-manager.h"
 #include "sway/input/seat.h"
@@ -56,9 +57,21 @@ static void rotate_child_position(double *sx, double *sy, double sw, double sh,
 	*sy = ry + ph/2 - sh/2;
 }
 
-bool output_get_surface_box(struct root_geometry *geo,
-		struct sway_output *output, struct wlr_surface *surface, int sx, int sy,
+struct surface_iterator_data {
+	sway_surface_iterator_func_t user_iterator;
+	void *user_data;
+
+	struct sway_output *output;
+	double ox, oy;
+	int width, height;
+	float rotation;
+};
+
+static bool get_surface_box(struct surface_iterator_data *data,
+		struct wlr_surface *surface, int sx, int sy,
 		struct wlr_box *surface_box) {
+	struct sway_output *output = data->output;
+
 	if (!wlr_surface_has_buffer(surface)) {
 		return false;
 	}
@@ -67,12 +80,12 @@ bool output_get_surface_box(struct root_geometry *geo,
 	int sh = surface->current.height;
 
 	double _sx = sx, _sy = sy;
-	rotate_child_position(&_sx, &_sy, sw, sh, geo->width, geo->height,
-		geo->rotation);
+	rotate_child_position(&_sx, &_sy, sw, sh, data->width, data->height,
+		data->rotation);
 
 	struct wlr_box box = {
-		.x = geo->x + _sx,
-		.y = geo->y + _sy,
+		.x = data->ox + _sx,
+		.y = data->oy + _sy,
 		.width = sw,
 		.height = sh,
 	};
@@ -81,7 +94,7 @@ bool output_get_surface_box(struct root_geometry *geo,
 	}
 
 	struct wlr_box rotated_box;
-	wlr_box_rotated_bounds(&box, geo->rotation, &rotated_box);
+	wlr_box_rotated_bounds(&box, data->rotation, &rotated_box);
 
 	struct wlr_box output_box = {
 		.width = output->swayc->current.swayc_width,
@@ -92,46 +105,90 @@ bool output_get_surface_box(struct root_geometry *geo,
 	return wlr_box_intersection(&output_box, &rotated_box, &intersection);
 }
 
-void output_surface_for_each_surface(struct wlr_surface *surface,
-		double ox, double oy, struct root_geometry *geo,
-		wlr_surface_iterator_func_t iterator, void *user_data) {
-	geo->x = ox;
-	geo->y = oy;
-	geo->width = surface->current.width;
-	geo->height = surface->current.height;
-	geo->rotation = 0;
+static void output_for_each_surface_iterator(struct wlr_surface *surface,
+		int sx, int sy, void *_data) {
+	struct surface_iterator_data *data = _data;
 
-	wlr_surface_for_each_surface(surface, iterator, user_data);
+	struct wlr_box box;
+	bool intersects = get_surface_box(data, surface, sx, sy, &box);
+	if (!intersects) {
+		return;
+	}
+
+	data->user_iterator(data->output, surface, &box, data->rotation,
+		data->user_data);
 }
 
-void output_view_for_each_surface(struct sway_view *view,
-		struct sway_output *output, struct root_geometry *geo,
-		wlr_surface_iterator_func_t iterator, void *user_data) {
-	geo->x = view->swayc->current.view_x - output->swayc->current.swayc_x;
-	geo->y = view->swayc->current.view_y - output->swayc->current.swayc_y;
-	geo->width = view->swayc->current.view_width;
-	geo->height = view->swayc->current.view_height;
-	geo->rotation = 0; // TODO
+void output_surface_for_each_surface(struct sway_output *output,
+		struct wlr_surface *surface, double ox, double oy,
+		sway_surface_iterator_func_t iterator, void *user_data) {
+	struct surface_iterator_data data = {
+		.user_iterator = iterator,
+		.user_data = user_data,
+		.output = output,
+		.ox = ox,
+		.oy = oy,
+		.width = surface->current.width,
+		.height = surface->current.height,
+		.rotation = 0,
+	};
 
-	view_for_each_surface(view, iterator, user_data);
+	wlr_surface_for_each_surface(surface,
+		output_for_each_surface_iterator, &data);
 }
 
-void output_layer_for_each_surface(struct wl_list *layer_surfaces,
-		struct root_geometry *geo, wlr_surface_iterator_func_t iterator,
+void output_view_for_each_surface(struct sway_output *output,
+		struct sway_view *view, sway_surface_iterator_func_t iterator,
+		void *user_data) {
+	struct surface_iterator_data data = {
+		.user_iterator = iterator,
+		.user_data = user_data,
+		.output = output,
+		.ox = view->swayc->current.view_x - output->swayc->current.swayc_x,
+		.oy = view->swayc->current.view_y - output->swayc->current.swayc_y,
+		.width = view->swayc->current.view_width,
+		.height = view->swayc->current.view_height,
+		.rotation = 0, // TODO
+	};
+
+	view_for_each_surface(view,
+		output_for_each_surface_iterator, &data);
+}
+
+void output_view_for_each_popup(struct sway_output *output,
+		struct sway_view *view, sway_surface_iterator_func_t iterator,
+		void *user_data) {
+	struct surface_iterator_data data = {
+		.user_iterator = iterator,
+		.user_data = user_data,
+		.output = output,
+		.ox = view->swayc->current.view_x - output->swayc->current.swayc_x,
+		.oy = view->swayc->current.view_y - output->swayc->current.swayc_y,
+		.width = view->swayc->current.view_width,
+		.height = view->swayc->current.view_height,
+		.rotation = 0, // TODO
+	};
+
+	view_for_each_popup(view, output_for_each_surface_iterator, &data);
+}
+
+void output_layer_for_each_surface(struct sway_output *output,
+		struct wl_list *layer_surfaces, sway_surface_iterator_func_t iterator,
 		void *user_data) {
 	struct sway_layer_surface *layer_surface;
 	wl_list_for_each(layer_surface, layer_surfaces, link) {
 		struct wlr_layer_surface *wlr_layer_surface =
 			layer_surface->layer_surface;
-		output_surface_for_each_surface(wlr_layer_surface->surface,
-			layer_surface->geo.x, layer_surface->geo.y, geo, iterator,
+		output_surface_for_each_surface(output, wlr_layer_surface->surface,
+			layer_surface->geo.x, layer_surface->geo.y, iterator,
 			user_data);
 	}
 }
 
-void output_unmanaged_for_each_surface(struct wl_list *unmanaged,
-		struct sway_output *output, struct root_geometry *geo,
-		wlr_surface_iterator_func_t iterator, void *user_data) {
+#ifdef HAVE_XWAYLAND
+void output_unmanaged_for_each_surface(struct sway_output *output,
+		struct wl_list *unmanaged, sway_surface_iterator_func_t iterator,
+		void *user_data) {
 	struct sway_xwayland_unmanaged *unmanaged_surface;
 	wl_list_for_each(unmanaged_surface, unmanaged, link) {
 		struct wlr_xwayland_surface *xsurface =
@@ -139,22 +196,24 @@ void output_unmanaged_for_each_surface(struct wl_list *unmanaged,
 		double ox = unmanaged_surface->lx - output->swayc->current.swayc_x;
 		double oy = unmanaged_surface->ly - output->swayc->current.swayc_y;
 
-		output_surface_for_each_surface(xsurface->surface, ox, oy, geo,
+		output_surface_for_each_surface(output, xsurface->surface, ox, oy,
 			iterator, user_data);
 	}
 }
+#endif
 
-void output_drag_icons_for_each_surface(struct wl_list *drag_icons,
-		struct sway_output *output, struct root_geometry *geo,
-		wlr_surface_iterator_func_t iterator, void *user_data) {
+void output_drag_icons_for_each_surface(struct sway_output *output,
+		struct wl_list *drag_icons, sway_surface_iterator_func_t iterator,
+		void *user_data) {
 	struct sway_drag_icon *drag_icon;
 	wl_list_for_each(drag_icon, drag_icons, link) {
 		double ox = drag_icon->x - output->swayc->x;
 		double oy = drag_icon->y - output->swayc->y;
 
 		if (drag_icon->wlr_drag_icon->mapped) {
-			output_surface_for_each_surface(drag_icon->wlr_drag_icon->surface,
-				ox, oy, geo, iterator, user_data);
+			output_surface_for_each_surface(output,
+				drag_icon->wlr_drag_icon->surface, ox, oy,
+				iterator, user_data);
 		}
 	}
 }
@@ -181,21 +240,14 @@ struct sway_container *output_get_active_workspace(struct sway_output *output) {
 	return workspace;
 }
 
-bool output_has_opaque_lockscreen(struct sway_output *output,
-		struct sway_seat *seat) {
-	if (!seat->exclusive_client) {
-		return false;
-	}
-
+bool output_has_opaque_overlay_layer_surface(struct sway_output *output) {
 	struct wlr_layer_surface *wlr_layer_surface;
 	wl_list_for_each(wlr_layer_surface, &server.layer_shell->surfaces, link) {
-		if (wlr_layer_surface->output != output->wlr_output) {
+		if (wlr_layer_surface->output != output->wlr_output ||
+				wlr_layer_surface->layer != ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY) {
 			continue;
 		}
 		struct wlr_surface *wlr_surface = wlr_layer_surface->surface;
-		if (wlr_surface->resource->client != seat->exclusive_client) {
-			continue;
-		}
 		struct sway_layer_surface *sway_layer_surface =
 			layer_from_wlr_layer_surface(wlr_layer_surface);
 		pixman_box32_t output_box = {
@@ -217,46 +269,38 @@ bool output_has_opaque_lockscreen(struct sway_output *output,
 	return false;
 }
 
+static void send_frame_done_iterator(struct sway_output *output,
+		struct wlr_surface *surface, struct wlr_box *box, float rotation,
+		void *_data) {
+	struct timespec *when = _data;
+	wlr_surface_send_frame_done(surface, when);
+}
+
+static void send_frame_done_layer(struct sway_output *output,
+		struct wl_list *layer_surfaces, struct timespec *when) {
+	output_layer_for_each_surface(output, layer_surfaces,
+		send_frame_done_iterator, when);
+}
+
+#ifdef HAVE_XWAYLAND
+static void send_frame_done_unmanaged(struct sway_output *output,
+		struct wl_list *unmanaged, struct timespec *when) {
+	output_unmanaged_for_each_surface(output, unmanaged,
+		send_frame_done_iterator, when);
+}
+#endif
+
+static void send_frame_done_drag_icons(struct sway_output *output,
+		struct wl_list *drag_icons, struct timespec *when) {
+	output_drag_icons_for_each_surface(output, drag_icons,
+		send_frame_done_iterator, when);
+}
+
 struct send_frame_done_data {
-	struct root_geometry root_geo;
 	struct sway_output *output;
 	struct timespec *when;
-	struct wl_client *exclusive_client;
 };
 
-static void send_frame_done_iterator(struct wlr_surface *surface,
-		int sx, int sy, void *_data) {
-	struct send_frame_done_data *data = _data;
-	if (data->exclusive_client &&
-			data->exclusive_client != surface->resource->client) {
-		return;
-	}
-
-	bool intersects = output_get_surface_box(&data->root_geo, data->output, surface,
-		sx, sy, NULL);
-	if (intersects) {
-		wlr_surface_send_frame_done(surface, data->when);
-	}
-}
-
-static void send_frame_done_layer(struct send_frame_done_data *data,
-		struct wl_list *layer_surfaces) {
-	output_layer_for_each_surface(layer_surfaces, &data->root_geo,
-		send_frame_done_iterator, data);
-}
-
-static void send_frame_done_unmanaged(struct send_frame_done_data *data,
-		struct wl_list *unmanaged) {
-	output_unmanaged_for_each_surface(unmanaged, data->output, &data->root_geo,
-		send_frame_done_iterator, data);
-}
-
-static void send_frame_done_drag_icons(struct send_frame_done_data *data,
-		struct wl_list *drag_icons) {
-	output_drag_icons_for_each_surface(drag_icons, data->output, &data->root_geo,
-		send_frame_done_iterator, data);
-}
-
 static void send_frame_done_container_iterator(struct sway_container *con,
 		void *_data) {
 	struct send_frame_done_data *data = _data;
@@ -268,52 +312,62 @@ static void send_frame_done_container_iterator(struct sway_container *con,
 		return;
 	}
 
-	output_view_for_each_surface(con->sway_view, data->output, &data->root_geo,
-		send_frame_done_iterator, data);
+	output_view_for_each_surface(data->output, con->sway_view,
+		send_frame_done_iterator, data->when);
 }
 
-static void send_frame_done_container(struct send_frame_done_data *data,
-		struct sway_container *con) {
-	container_descendants(con, C_VIEW,
-		send_frame_done_container_iterator, data);
-}
-
-static void send_frame_done(struct sway_output *output, struct timespec *when) {
-	struct sway_seat *seat = input_manager_current_seat(input_manager);
+static void send_frame_done_container(struct sway_output *output,
+		struct sway_container *con, struct timespec *when) {
 	struct send_frame_done_data data = {
 		.output = output,
 		.when = when,
-		.exclusive_client = output_has_opaque_lockscreen(output, seat) ?
-			seat->exclusive_client : NULL,
 	};
+	container_descendants(con, C_VIEW,
+		send_frame_done_container_iterator, &data);
+}
+
+static void send_frame_done(struct sway_output *output, struct timespec *when) {
+	if (output_has_opaque_overlay_layer_surface(output)) {
+		goto send_frame_overlay;
+	}
 
 	struct sway_container *workspace = output_get_active_workspace(output);
 	if (workspace->current.ws_fullscreen) {
-		send_frame_done_container_iterator(
-			workspace->current.ws_fullscreen->swayc, &data);
-
-		if (workspace->current.ws_fullscreen->type == SWAY_VIEW_XWAYLAND) {
-			send_frame_done_unmanaged(&data,
-				&root_container.sway_root->xwayland_unmanaged);
+		if (workspace->current.ws_fullscreen->type == C_VIEW) {
+			output_view_for_each_surface(output,
+				workspace->current.ws_fullscreen->sway_view,
+				send_frame_done_iterator, when);
+		} else {
+			send_frame_done_container(output, workspace->current.ws_fullscreen,
+				when);
 		}
+#ifdef HAVE_XWAYLAND
+		send_frame_done_unmanaged(output,
+			&root_container.sway_root->xwayland_unmanaged, when);
+#endif
 	} else {
-		send_frame_done_layer(&data,
-			&output->layers[ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND]);
-		send_frame_done_layer(&data,
-			&output->layers[ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM]);
+		send_frame_done_layer(output,
+			&output->layers[ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND], when);
+		send_frame_done_layer(output,
+			&output->layers[ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM], when);
 
-		send_frame_done_container(&data, workspace);
-		send_frame_done_container(&data, workspace->sway_workspace->floating);
+		send_frame_done_container(output, workspace, when);
+		send_frame_done_container(output, workspace->sway_workspace->floating,
+			when);
 
-		send_frame_done_unmanaged(&data,
-			&root_container.sway_root->xwayland_unmanaged);
-		send_frame_done_layer(&data,
-			&output->layers[ZWLR_LAYER_SHELL_V1_LAYER_TOP]);
+#ifdef HAVE_XWAYLAND
+		send_frame_done_unmanaged(output,
+			&root_container.sway_root->xwayland_unmanaged, when);
+#endif
+		send_frame_done_layer(output,
+			&output->layers[ZWLR_LAYER_SHELL_V1_LAYER_TOP], when);
 	}
 
-	send_frame_done_layer(&data,
-		&output->layers[ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY]);
-	send_frame_done_drag_icons(&data, &root_container.sway_root->drag_icons);
+send_frame_overlay:
+	send_frame_done_layer(output,
+		&output->layers[ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY], when);
+	send_frame_done_drag_icons(output, &root_container.sway_root->drag_icons,
+		when);
 }
 
 static void damage_handle_frame(struct wl_listener *listener, void *data) {
@@ -348,26 +402,13 @@ void output_damage_whole(struct sway_output *output) {
 	wlr_output_damage_add_whole(output->damage);
 }
 
-struct damage_data {
-	struct root_geometry root_geo;
-	struct sway_output *output;
-	bool whole;
-};
-
-static void damage_surface_iterator(struct wlr_surface *surface, int sx, int sy,
+static void damage_surface_iterator(struct sway_output *output,
+		struct wlr_surface *surface, struct wlr_box *_box, float rotation,
 		void *_data) {
-	struct damage_data *data = _data;
-	struct sway_output *output = data->output;
-	float rotation = data->root_geo.rotation;
-	bool whole = data->whole;
-
-	struct wlr_box box;
-	bool intersects = output_get_surface_box(&data->root_geo, data->output, surface,
-		sx, sy, &box);
-	if (!intersects) {
-		return;
-	}
+	bool *data = _data;
+	bool whole = *data;
 
+	struct wlr_box box = *_box;
 	scale_box(&box, output->wlr_output->scale);
 
 	int center_x = box.x + box.width/2;
@@ -407,13 +448,8 @@ static void damage_surface_iterator(struct wlr_surface *surface, int sx, int sy,
 
 void output_damage_surface(struct sway_output *output, double ox, double oy,
 		struct wlr_surface *surface, bool whole) {
-	struct damage_data data = {
-		.output = output,
-		.whole = whole,
-	};
-
-	output_surface_for_each_surface(surface, ox, oy, &data.root_geo,
-		damage_surface_iterator, &data);
+	output_surface_for_each_surface(output, surface, ox, oy,
+		damage_surface_iterator, &whole);
 }
 
 static void output_damage_view(struct sway_output *output,
@@ -426,13 +462,7 @@ static void output_damage_view(struct sway_output *output,
 		return;
 	}
 
-	struct damage_data data = {
-		.output = output,
-		.whole = whole,
-	};
-
-	output_view_for_each_surface(view, output, &data.root_geo,
-		damage_surface_iterator, &data);
+	output_view_for_each_surface(output, view, damage_surface_iterator, &whole);
 }
 
 void output_damage_from_view(struct sway_output *output,
@@ -463,11 +493,12 @@ static void output_damage_whole_container_iterator(struct sway_container *con,
 
 void output_damage_whole_container(struct sway_output *output,
 		struct sway_container *con) {
+	// Pad the box by 1px, because the width is a double and might be a fraction
 	struct wlr_box box = {
-		.x = con->current.swayc_x - output->wlr_output->lx,
-		.y = con->current.swayc_y - output->wlr_output->ly,
-		.width = con->current.swayc_width,
-		.height = con->current.swayc_height,
+		.x = con->current.swayc_x - output->wlr_output->lx - 1,
+		.y = con->current.swayc_y - output->wlr_output->ly - 1,
+		.width = con->current.swayc_width + 2,
+		.height = con->current.swayc_height + 2,
 	};
 	scale_box(&box, output->wlr_output->scale);
 	wlr_output_damage_add_box(output->damage, &box);
@@ -509,22 +540,14 @@ static void handle_transform(struct wl_listener *listener, void *data) {
 	transaction_commit_dirty();
 }
 
-static void handle_scale_iterator(struct sway_container *view, void *data) {
-	view_update_marks_textures(view->sway_view);
-}
-
 static void handle_scale(struct wl_listener *listener, void *data) {
 	struct sway_output *output = wl_container_of(listener, output, scale);
 	arrange_layers(output);
-	container_descendants(output->swayc, C_VIEW, handle_scale_iterator, NULL);
+	container_update_textures_recursive(output->swayc);
 	arrange_windows(output->swayc);
 	transaction_commit_dirty();
 }
 
-struct sway_output *output_from_wlr_output(struct wlr_output *wlr_output) {
-	return wlr_output->data;
-}
-
 void handle_new_output(struct wl_listener *listener, void *data) {
 	struct sway_server *server = wl_container_of(listener, server, new_output);
 	struct wlr_output *wlr_output = data;
diff --git a/sway/desktop/render.c b/sway/desktop/render.c
index 4c85e516..cdac9c72 100644
--- a/sway/desktop/render.c
+++ b/sway/desktop/render.c
@@ -14,6 +14,7 @@
 #include <wlr/types/wlr_surface.h>
 #include <wlr/util/region.h>
 #include "log.h"
+#include "config.h"
 #include "sway/config.h"
 #include "sway/debug.h"
 #include "sway/input/input-manager.h"
@@ -28,10 +29,7 @@
 #include "sway/tree/workspace.h"
 
 struct render_data {
-	struct root_geometry root_geo;
-	struct sway_output *output;
 	pixman_region32_t *damage;
-	struct sway_view *view;
 	float alpha;
 };
 
@@ -91,11 +89,11 @@ damage_finish:
 	pixman_region32_fini(&damage);
 }
 
-static void render_surface_iterator(struct wlr_surface *surface, int sx, int sy,
+static void render_surface_iterator(struct sway_output *output,
+		struct wlr_surface *surface, struct wlr_box *_box, float rotation,
 		void *_data) {
 	struct render_data *data = _data;
-	struct wlr_output *wlr_output = data->output->wlr_output;
-	float rotation = data->root_geo.rotation;
+	struct wlr_output *wlr_output = output->wlr_output;
 	pixman_region32_t *output_damage = data->damage;
 	float alpha = data->alpha;
 
@@ -104,13 +102,7 @@ static void render_surface_iterator(struct wlr_surface *surface, int sx, int sy,
 		return;
 	}
 
-	struct wlr_box box;
-	bool intersects = output_get_surface_box(&data->root_geo, data->output,
-		surface, sx, sy, &box);
-	if (!intersects) {
-		return;
-	}
-
+	struct wlr_box box = *_box;
 	scale_box(&box, wlr_output->scale);
 
 	float matrix[9];
@@ -125,33 +117,32 @@ static void render_surface_iterator(struct wlr_surface *surface, int sx, int sy,
 static void render_layer(struct sway_output *output,
 		pixman_region32_t *damage, struct wl_list *layer_surfaces) {
 	struct render_data data = {
-		.output = output,
 		.damage = damage,
 		.alpha = 1.0f,
 	};
-	output_layer_for_each_surface(layer_surfaces, &data.root_geo,
+	output_layer_for_each_surface(output, layer_surfaces,
 		render_surface_iterator, &data);
 }
 
+#ifdef HAVE_XWAYLAND
 static void render_unmanaged(struct sway_output *output,
 		pixman_region32_t *damage, struct wl_list *unmanaged) {
 	struct render_data data = {
-		.output = output,
 		.damage = damage,
 		.alpha = 1.0f,
 	};
-	output_unmanaged_for_each_surface(unmanaged, output, &data.root_geo,
+	output_unmanaged_for_each_surface(output, unmanaged,
 		render_surface_iterator, &data);
 }
+#endif
 
 static void render_drag_icons(struct sway_output *output,
 		pixman_region32_t *damage, struct wl_list *drag_icons) {
 	struct render_data data = {
-		.output = output,
 		.damage = damage,
 		.alpha = 1.0f,
 	};
-	output_drag_icons_for_each_surface(drag_icons, output, &data.root_geo,
+	output_drag_icons_for_each_surface(output, drag_icons,
 		render_surface_iterator, &data);
 }
 
@@ -195,33 +186,51 @@ static void premultiply_alpha(float color[4], float opacity) {
 	color[2] *= color[3];
 }
 
-static void render_view_surfaces(struct sway_view *view,
+static void render_view_toplevels(struct sway_view *view,
 		struct sway_output *output, pixman_region32_t *damage, float alpha) {
 	struct render_data data = {
-		.output = output,
 		.damage = damage,
-		.view = view,
 		.alpha = alpha,
 	};
-	output_view_for_each_surface(view, output, &data.root_geo,
-		render_surface_iterator, &data);
+	// Render all toplevels without descending into popups
+	output_surface_for_each_surface(output, view->surface,
+			view->swayc->current.view_x - output->wlr_output->lx,
+			view->swayc->current.view_y - output->wlr_output->ly,
+			render_surface_iterator, &data);
+}
+
+static void render_popup_iterator(struct sway_output *output,
+		struct wlr_surface *surface, struct wlr_box *box, float rotation,
+		void *data) {
+	// Render this popup's surface
+	render_surface_iterator(output, surface, box, rotation, data);
+
+	// Render this popup's child toplevels
+	output_surface_for_each_surface(output, surface, box->x, box->y,
+			render_surface_iterator, data);
+}
+
+static void render_view_popups(struct sway_view *view,
+		struct sway_output *output, pixman_region32_t *damage, float alpha) {
+	struct render_data data = {
+		.damage = damage,
+		.alpha = alpha,
+	};
+	output_view_for_each_popup(output, view, render_popup_iterator, &data);
 }
 
 static void render_saved_view(struct sway_view *view,
 		struct sway_output *output, pixman_region32_t *damage, float alpha) {
 	struct wlr_output *wlr_output = output->wlr_output;
 
-	int width, height;
-	struct wlr_texture *texture =
-		transaction_get_saved_texture(view, &width, &height);
-	if (!texture) {
+	if (!view->saved_buffer || !view->saved_buffer->texture) {
 		return;
 	}
 	struct wlr_box box = {
 		.x = view->swayc->current.view_x - output->swayc->current.swayc_x,
 		.y = view->swayc->current.view_y - output->swayc->current.swayc_y,
-		.width = width,
-		.height = height,
+		.width = view->saved_buffer_width,
+		.height = view->saved_buffer_height,
 	};
 
 	struct wlr_box output_box = {
@@ -241,7 +250,8 @@ static void render_saved_view(struct sway_view *view,
 	wlr_matrix_project_box(matrix, &box, WL_OUTPUT_TRANSFORM_NORMAL, 0,
 		wlr_output->transform_matrix);
 
-	render_texture(wlr_output, damage, texture, &box, matrix, alpha);
+	render_texture(wlr_output, damage, view->saved_buffer->texture,
+			&box, matrix, alpha);
 }
 
 /**
@@ -250,10 +260,10 @@ static void render_saved_view(struct sway_view *view,
 static void render_view(struct sway_output *output, pixman_region32_t *damage,
 		struct sway_container *con, struct border_colors *colors) {
 	struct sway_view *view = con->sway_view;
-	if (view->swayc->instructions->length) {
+	if (view->saved_buffer) {
 		render_saved_view(view, output, damage, view->swayc->alpha);
 	} else {
-		render_view_surfaces(view, output, damage, view->swayc->alpha);
+		render_view_toplevels(view, output, damage, view->swayc->alpha);
 	}
 
 	if (view->using_csd) {
@@ -778,7 +788,7 @@ static void render_floating_container(struct sway_output *soutput,
 		}
 		render_view(soutput, damage, con, colors);
 	} else {
-		render_container(soutput, damage, con, false);
+		render_container(soutput, damage, con, con->current.focused);
 	}
 }
 
@@ -835,22 +845,13 @@ void output_render(struct sway_output *output, struct timespec *when,
 	}
 
 	struct sway_container *workspace = output_get_active_workspace(output);
-	struct sway_view *fullscreen_view = workspace->current.ws_fullscreen;
-	struct sway_seat *seat = input_manager_current_seat(input_manager);
+	struct sway_container *fullscreen_con = workspace->current.ws_fullscreen;
 
-	if (output_has_opaque_lockscreen(output, seat)) {
-		struct wlr_layer_surface *wlr_layer_surface = seat->focused_layer;
-		struct sway_layer_surface *sway_layer_surface =
-			layer_from_wlr_layer_surface(seat->focused_layer);
-		struct render_data data = {
-			.output = output,
-			.damage = damage,
-			.alpha = 1.0f,
-		};
-		output_surface_for_each_surface(wlr_layer_surface->surface,
-			sway_layer_surface->geo.x, sway_layer_surface->geo.y,
-			&data.root_geo, render_surface_iterator, &data);
-	} else if (fullscreen_view) {
+	if (output_has_opaque_overlay_layer_surface(output)) {
+		goto render_overlay;
+	}
+
+	if (fullscreen_con) {
 		float clear_color[] = {0.0f, 0.0f, 0.0f, 1.0f};
 
 		int nrects;
@@ -861,16 +862,22 @@ void output_render(struct sway_output *output, struct timespec *when,
 		}
 
 		// TODO: handle views smaller than the output
-		if (fullscreen_view->swayc->instructions->length) {
-			render_saved_view(fullscreen_view, output, damage, 1.0f);
+		if (fullscreen_con->type == C_VIEW) {
+			if (fullscreen_con->sway_view->saved_buffer) {
+				render_saved_view(fullscreen_con->sway_view,
+						output, damage, 1.0f);
+			} else {
+				render_view_toplevels(fullscreen_con->sway_view,
+						output, damage, 1.0f);
+			}
 		} else {
-			render_view_surfaces(fullscreen_view, output, damage, 1.0f);
-		}
-
-		if (fullscreen_view->type == SWAY_VIEW_XWAYLAND) {
-			render_unmanaged(output, damage,
-				&root_container.sway_root->xwayland_unmanaged);
+			render_container(output, damage, fullscreen_con,
+					fullscreen_con->current.focused);
 		}
+#ifdef HAVE_XWAYLAND
+		render_unmanaged(output, damage,
+			&root_container.sway_root->xwayland_unmanaged);
+#endif
 	} else {
 		float clear_color[] = {0.25f, 0.25f, 0.25f, 1.0f};
 
@@ -888,12 +895,21 @@ void output_render(struct sway_output *output, struct timespec *when,
 
 		render_container(output, damage, workspace, workspace->current.focused);
 		render_floating(output, damage);
-
+#ifdef HAVE_XWAYLAND
 		render_unmanaged(output, damage,
 			&root_container.sway_root->xwayland_unmanaged);
+#endif
 		render_layer(output, damage,
 			&output->layers[ZWLR_LAYER_SHELL_V1_LAYER_TOP]);
 	}
+
+	struct sway_seat *seat = input_manager_current_seat(input_manager);
+	struct sway_container *focus = seat_get_focus(seat);
+	if (focus && focus->type == C_VIEW) {
+		render_view_popups(focus->sway_view, output, damage, focus->alpha);
+	}
+
+render_overlay:
 	render_layer(output, damage,
 		&output->layers[ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY]);
 	render_drag_icons(output, damage, &root_container.sway_root->drag_icons);
diff --git a/sway/desktop/transaction.c b/sway/desktop/transaction.c
index 19f41efc..4e6af86a 100644
--- a/sway/desktop/transaction.c
+++ b/sway/desktop/transaction.c
@@ -1,4 +1,5 @@
 #define _POSIX_C_SOURCE 200809L
+#include <errno.h>
 #include <stdbool.h>
 #include <stdlib.h>
 #include <string.h>
@@ -40,8 +41,6 @@ struct sway_transaction_instruction {
 	struct sway_transaction *transaction;
 	struct sway_container *container;
 	struct sway_container_state state;
-	struct wlr_buffer *saved_buffer;
-	int saved_buffer_width, saved_buffer_height;
 	uint32_t serial;
 	bool ready;
 };
@@ -56,27 +55,6 @@ static struct sway_transaction *transaction_create() {
 	return transaction;
 }
 
-static void remove_saved_view_buffer(
-		struct sway_transaction_instruction *instruction) {
-	if (instruction->saved_buffer) {
-		wlr_buffer_unref(instruction->saved_buffer);
-		instruction->saved_buffer = NULL;
-	}
-}
-
-static void save_view_buffer(struct sway_view *view,
-		struct sway_transaction_instruction *instruction) {
-	if (!sway_assert(instruction->saved_buffer == NULL,
-				"Didn't expect instruction to have a saved buffer already")) {
-		remove_saved_view_buffer(instruction);
-	}
-	if (view->surface && wlr_surface_has_buffer(view->surface)) {
-		instruction->saved_buffer = wlr_buffer_ref(view->surface->buffer);
-		instruction->saved_buffer_width = view->surface->current.width;
-		instruction->saved_buffer_height = view->surface->current.height;
-	}
-}
-
 static void transaction_destroy(struct sway_transaction *transaction) {
 	// Free instructions
 	for (int i = 0; i < transaction->instructions->length; ++i) {
@@ -92,7 +70,6 @@ static void transaction_destroy(struct sway_transaction *transaction) {
 		if (con->destroying && !con->instructions->length) {
 			container_free(con);
 		}
-		remove_saved_view_buffer(instruction);
 		free(instruction);
 	}
 	list_free(transaction->instructions);
@@ -110,6 +87,7 @@ static void copy_pending_state(struct sway_container *container,
 	state->swayc_y = container->y;
 	state->swayc_width = container->width;
 	state->swayc_height = container->height;
+	state->is_fullscreen = container->is_fullscreen;
 	state->has_gaps = container->has_gaps;
 	state->current_gaps = container->current_gaps;
 	state->gaps_inner = container->gaps_inner;
@@ -122,7 +100,6 @@ static void copy_pending_state(struct sway_container *container,
 		state->view_y = view->y;
 		state->view_width = view->width;
 		state->view_height = view->height;
-		state->is_fullscreen = view->is_fullscreen;
 		state->border = view->border;
 		state->border_thickness = view->border_thickness;
 		state->border_top = view->border_top;
@@ -157,9 +134,6 @@ static void transaction_add_container(struct sway_transaction *transaction,
 
 	copy_pending_state(container, &instruction->state);
 
-	if (container->type == C_VIEW) {
-		save_view_buffer(container->sway_view, instruction);
-	}
 	list_add(transaction->instructions, instruction);
 }
 
@@ -219,27 +193,35 @@ static void transaction_apply(struct sway_transaction *transaction) {
 
 		memcpy(&container->current, &instruction->state,
 				sizeof(struct sway_container_state));
+
+		if (container->type == C_VIEW) {
+			if (container->destroying) {
+				if (container->instructions->length == 1 &&
+						container->sway_view->saved_buffer) {
+					view_remove_saved_buffer(container->sway_view);
+				}
+			} else {
+				if (container->sway_view->saved_buffer) {
+					view_remove_saved_buffer(container->sway_view);
+				}
+				if (container->instructions->length > 1) {
+					view_save_buffer(container->sway_view);
+				}
+			}
+		}
 	}
 }
 
-/**
- * For simplicity, we only progress the queue if it can be completely flushed.
- */
 static void transaction_progress_queue() {
-	// We iterate this list in reverse because we're more likely to find a
-	// waiting transactions at the end of the list.
-	for (int i = server.transactions->length - 1; i >= 0; --i) {
-		struct sway_transaction *transaction = server.transactions->items[i];
+	while (server.transactions->length) {
+		struct sway_transaction *transaction = server.transactions->items[0];
 		if (transaction->num_waiting) {
 			return;
 		}
-	}
-	for (int i = 0; i < server.transactions->length; ++i) {
-		struct sway_transaction *transaction = server.transactions->items[i];
 		transaction_apply(transaction);
 		transaction_destroy(transaction);
+		list_del(server.transactions, 0);
 	}
-	server.transactions->length = 0;
 	idle_inhibit_v1_check_active(server.idle_inhibit_manager_v1);
 }
 
@@ -302,6 +284,9 @@ static void transaction_commit(struct sway_transaction *transaction) {
 			struct timespec when;
 			wlr_surface_send_frame_done(con->sway_view->surface, &when);
 		}
+		if (con->type == C_VIEW && !con->sway_view->saved_buffer) {
+			view_save_buffer(con->sway_view);
+		}
 		list_add(con->instructions, instruction);
 	}
 	transaction->num_configures = transaction->num_waiting;
@@ -324,7 +309,14 @@ static void transaction_commit(struct sway_transaction *transaction) {
 		// Set up a timer which the views must respond within
 		transaction->timer = wl_event_loop_add_timer(server.wl_event_loop,
 				handle_timeout, transaction);
-		wl_event_source_timer_update(transaction->timer, txn_timeout_ms);
+		if (transaction->timer) {
+			wl_event_source_timer_update(transaction->timer, txn_timeout_ms);
+		} else {
+			wlr_log(WLR_ERROR, "Unable to create transaction timer (%s). "
+					"Some imperfect frames might be rendered.",
+					strerror(errno));
+			handle_timeout(transaction);
+		}
 	}
 
 	// The debug tree shows the pending/live tree. Here is a good place to
@@ -352,13 +344,11 @@ static void set_instruction_ready(
 
 	}
 
-	// If all views are ready, apply the transaction.
 	// If the transaction has timed out then its num_waiting will be 0 already.
 	if (transaction->num_waiting > 0 && --transaction->num_waiting == 0) {
 		if (!txn_debug) {
 			wlr_log(WLR_DEBUG, "Transaction %p is ready", transaction);
 			wl_event_source_timer_update(transaction->timer, 0);
-			transaction_progress_queue();
 		}
 	}
 }
@@ -375,6 +365,7 @@ static void set_instructions_ready(struct sway_view *view, int index) {
 			set_instruction_ready(instruction);
 		}
 	}
+	transaction_progress_queue();
 }
 
 void transaction_notify_view_ready(struct sway_view *view, uint32_t serial) {
@@ -401,18 +392,6 @@ void transaction_notify_view_ready_by_size(struct sway_view *view,
 	}
 }
 
-struct wlr_texture *transaction_get_saved_texture(struct sway_view *view,
-		int *width, int *height) {
-	struct sway_transaction_instruction *instruction =
-		view->swayc->instructions->items[0];
-	if (!instruction->saved_buffer) {
-		return NULL;
-	}
-	*width = instruction->saved_buffer_width;
-	*height = instruction->saved_buffer_height;
-	return instruction->saved_buffer->texture;
-}
-
 void transaction_commit_dirty(void) {
 	if (!server.dirty_containers->length) {
 		return;
diff --git a/sway/desktop/xdg_shell.c b/sway/desktop/xdg_shell.c
index 98c16faf..b364663d 100644
--- a/sway/desktop/xdg_shell.c
+++ b/sway/desktop/xdg_shell.c
@@ -1,4 +1,5 @@
 #define _POSIX_C_SOURCE 199309L
+#include <float.h>
 #include <stdbool.h>
 #include <stdlib.h>
 #include <wayland-server.h>
@@ -95,6 +96,16 @@ static struct sway_xdg_shell_view *xdg_shell_view_from_view(
 	return (struct sway_xdg_shell_view *)view;
 }
 
+static void get_constraints(struct sway_view *view, double *min_width,
+		double *max_width, double *min_height, double *max_height) {
+	struct wlr_xdg_toplevel_state *state =
+		&view->wlr_xdg_surface->toplevel->current;
+	*min_width = state->min_width > 0 ? state->min_width : DBL_MIN;
+	*max_width = state->max_width > 0 ? state->max_width : DBL_MAX;
+	*min_height = state->min_height > 0 ? state->min_height : DBL_MIN;
+	*max_height = state->max_height > 0 ? state->max_height : DBL_MAX;
+}
+
 static const char *get_string_prop(struct sway_view *view, enum sway_view_prop prop) {
 	if (xdg_shell_view_from_view(view) == NULL) {
 		return NULL;
@@ -168,6 +179,14 @@ static void for_each_surface(struct sway_view *view,
 		user_data);
 }
 
+static void for_each_popup(struct sway_view *view,
+		wlr_surface_iterator_func_t iterator, void *user_data) {
+	if (xdg_shell_view_from_view(view) == NULL) {
+		return;
+	}
+	wlr_xdg_surface_for_each_popup(view->wlr_xdg_surface, iterator, user_data);
+}
+
 static void _close(struct sway_view *view) {
 	if (xdg_shell_view_from_view(view) == NULL) {
 		return;
@@ -178,6 +197,18 @@ static void _close(struct sway_view *view) {
 	}
 }
 
+static void close_popups_iterator(struct wlr_surface *surface,
+		int sx, int sy, void *data) {
+	struct wlr_xdg_surface *xdg_surface =
+		wlr_xdg_surface_from_wlr_surface(surface);
+	wlr_xdg_surface_send_close(xdg_surface);
+}
+
+static void close_popups(struct sway_view *view) {
+	wlr_xdg_surface_for_each_popup(view->wlr_xdg_surface,
+			close_popups_iterator, NULL);
+}
+
 static void destroy(struct sway_view *view) {
 	struct sway_xdg_shell_view *xdg_shell_view =
 		xdg_shell_view_from_view(view);
@@ -188,6 +219,7 @@ static void destroy(struct sway_view *view) {
 }
 
 static const struct sway_view_impl view_impl = {
+	.get_constraints = get_constraints,
 	.get_string_prop = get_string_prop,
 	.configure = configure,
 	.set_activated = set_activated,
@@ -195,7 +227,9 @@ static const struct sway_view_impl view_impl = {
 	.set_fullscreen = set_fullscreen,
 	.wants_floating = wants_floating,
 	.for_each_surface = for_each_surface,
+	.for_each_popup = for_each_popup,
 	.close = _close,
+	.close_popups = close_popups,
 	.destroy = destroy,
 };
 
@@ -213,10 +247,24 @@ static void handle_commit(struct wl_listener *listener, void *data) {
 		transaction_notify_view_ready(view, xdg_surface->configure_serial);
 	}
 
-	view_update_title(view, false);
 	view_damage_from(view);
 }
 
+static void handle_set_title(struct wl_listener *listener, void *data) {
+	struct sway_xdg_shell_view *xdg_shell_view =
+		wl_container_of(listener, xdg_shell_view, set_title);
+	struct sway_view *view = &xdg_shell_view->view;
+	view_update_title(view, false);
+	view_execute_criteria(view);
+}
+
+static void handle_set_app_id(struct wl_listener *listener, void *data) {
+	struct sway_xdg_shell_view *xdg_shell_view =
+		wl_container_of(listener, xdg_shell_view, set_app_id);
+	struct sway_view *view = &xdg_shell_view->view;
+	view_execute_criteria(view);
+}
+
 static void handle_new_popup(struct wl_listener *listener, void *data) {
 	struct sway_xdg_shell_view *xdg_shell_view =
 		wl_container_of(listener, xdg_shell_view, new_popup);
@@ -241,13 +289,41 @@ static void handle_request_fullscreen(struct wl_listener *listener, void *data)
 		return;
 	}
 
-	view_set_fullscreen(view, e->fullscreen);
+	container_set_fullscreen(view->swayc, e->fullscreen);
 
 	struct sway_container *output = container_parent(view->swayc, C_OUTPUT);
 	arrange_windows(output);
 	transaction_commit_dirty();
 }
 
+static void handle_request_move(struct wl_listener *listener, void *data) {
+	struct sway_xdg_shell_view *xdg_shell_view =
+		wl_container_of(listener, xdg_shell_view, request_move);
+	struct sway_view *view = &xdg_shell_view->view;
+	if (!container_is_floating(view->swayc)) {
+		return;
+	}
+	struct wlr_xdg_toplevel_move_event *e = data;
+	struct sway_seat *seat = e->seat->seat->data;
+	if (e->serial == seat->last_button_serial) {
+		seat_begin_move(seat, view->swayc, seat->last_button);
+	}
+}
+
+static void handle_request_resize(struct wl_listener *listener, void *data) {
+	struct sway_xdg_shell_view *xdg_shell_view =
+		wl_container_of(listener, xdg_shell_view, request_resize);
+	struct sway_view *view = &xdg_shell_view->view;
+	if (!container_is_floating(view->swayc)) {
+		return;
+	}
+	struct wlr_xdg_toplevel_resize_event *e = data;
+	struct sway_seat *seat = e->seat->seat->data;
+	if (e->serial == seat->last_button_serial) {
+		seat_begin_resize(seat, view->swayc, seat->last_button, e->edges);
+	}
+}
+
 static void handle_unmap(struct wl_listener *listener, void *data) {
 	struct sway_xdg_shell_view *xdg_shell_view =
 		wl_container_of(listener, xdg_shell_view, unmap);
@@ -262,6 +338,10 @@ static void handle_unmap(struct wl_listener *listener, void *data) {
 	wl_list_remove(&xdg_shell_view->commit.link);
 	wl_list_remove(&xdg_shell_view->new_popup.link);
 	wl_list_remove(&xdg_shell_view->request_fullscreen.link);
+	wl_list_remove(&xdg_shell_view->request_move.link);
+	wl_list_remove(&xdg_shell_view->request_resize.link);
+	wl_list_remove(&xdg_shell_view->set_title.link);
+	wl_list_remove(&xdg_shell_view->set_app_id.link);
 }
 
 static void handle_map(struct wl_listener *listener, void *data) {
@@ -280,7 +360,7 @@ static void handle_map(struct wl_listener *listener, void *data) {
 	view_map(view, view->wlr_xdg_surface->surface);
 
 	if (xdg_surface->toplevel->client_pending.fullscreen) {
-		view_set_fullscreen(view, true);
+		container_set_fullscreen(view->swayc, true);
 		struct sway_container *ws = container_parent(view->swayc, C_WORKSPACE);
 		arrange_windows(ws);
 	} else {
@@ -299,6 +379,22 @@ static void handle_map(struct wl_listener *listener, void *data) {
 	xdg_shell_view->request_fullscreen.notify = handle_request_fullscreen;
 	wl_signal_add(&xdg_surface->toplevel->events.request_fullscreen,
 			&xdg_shell_view->request_fullscreen);
+
+	xdg_shell_view->request_move.notify = handle_request_move;
+	wl_signal_add(&xdg_surface->toplevel->events.request_move,
+			&xdg_shell_view->request_move);
+
+	xdg_shell_view->request_resize.notify = handle_request_resize;
+	wl_signal_add(&xdg_surface->toplevel->events.request_resize,
+			&xdg_shell_view->request_resize);
+
+	xdg_shell_view->set_title.notify = handle_set_title;
+	wl_signal_add(&xdg_surface->toplevel->events.set_title,
+			&xdg_shell_view->set_title);
+
+	xdg_shell_view->set_app_id.notify = handle_set_app_id;
+	wl_signal_add(&xdg_surface->toplevel->events.set_app_id,
+			&xdg_shell_view->set_app_id);
 }
 
 static void handle_destroy(struct wl_listener *listener, void *data) {
@@ -344,9 +440,6 @@ void handle_xdg_shell_surface(struct wl_listener *listener, void *data) {
 	view_init(&xdg_shell_view->view, SWAY_VIEW_XDG_SHELL, &view_impl);
 	xdg_shell_view->view.wlr_xdg_surface = xdg_surface;
 
-	// TODO:
-	// - Look up pid and open on appropriate workspace
-
 	xdg_shell_view->map.notify = handle_map;
 	wl_signal_add(&xdg_surface->events.map, &xdg_shell_view->map);
 
diff --git a/sway/desktop/xdg_shell_v6.c b/sway/desktop/xdg_shell_v6.c
index 4d76f0a7..ffea03ad 100644
--- a/sway/desktop/xdg_shell_v6.c
+++ b/sway/desktop/xdg_shell_v6.c
@@ -1,4 +1,5 @@
 #define _POSIX_C_SOURCE 199309L
+#include <float.h>
 #include <stdbool.h>
 #include <stdlib.h>
 #include <wayland-server.h>
@@ -94,6 +95,16 @@ static struct sway_xdg_shell_v6_view *xdg_shell_v6_view_from_view(
 	return (struct sway_xdg_shell_v6_view *)view;
 }
 
+static void get_constraints(struct sway_view *view, double *min_width,
+		double *max_width, double *min_height, double *max_height) {
+	struct wlr_xdg_toplevel_v6_state *state =
+		&view->wlr_xdg_surface_v6->toplevel->current;
+	*min_width = state->min_width > 0 ? state->min_width : DBL_MIN;
+	*max_width = state->max_width > 0 ? state->max_width : DBL_MAX;
+	*min_height = state->min_height > 0 ? state->min_height : DBL_MIN;
+	*max_height = state->max_height > 0 ? state->max_height : DBL_MAX;
+}
+
 static const char *get_string_prop(struct sway_view *view, enum sway_view_prop prop) {
 	if (xdg_shell_v6_view_from_view(view) == NULL) {
 		return NULL;
@@ -164,6 +175,15 @@ static void for_each_surface(struct sway_view *view,
 		user_data);
 }
 
+static void for_each_popup(struct sway_view *view,
+		wlr_surface_iterator_func_t iterator, void *user_data) {
+	if (xdg_shell_v6_view_from_view(view) == NULL) {
+		return;
+	}
+	wlr_xdg_surface_v6_for_each_popup(view->wlr_xdg_surface_v6, iterator,
+		user_data);
+}
+
 static void _close(struct sway_view *view) {
 	if (xdg_shell_v6_view_from_view(view) == NULL) {
 		return;
@@ -174,6 +194,18 @@ static void _close(struct sway_view *view) {
 	}
 }
 
+static void close_popups_iterator(struct wlr_surface *surface,
+		int sx, int sy, void *data) {
+	struct wlr_xdg_surface_v6 *xdg_surface_v6 =
+		wlr_xdg_surface_v6_from_wlr_surface(surface);
+	wlr_xdg_surface_v6_send_close(xdg_surface_v6);
+}
+
+static void close_popups(struct sway_view *view) {
+	wlr_xdg_surface_v6_for_each_popup(view->wlr_xdg_surface_v6,
+			close_popups_iterator, NULL);
+}
+
 static void destroy(struct sway_view *view) {
 	struct sway_xdg_shell_v6_view *xdg_shell_v6_view =
 		xdg_shell_v6_view_from_view(view);
@@ -184,6 +216,7 @@ static void destroy(struct sway_view *view) {
 }
 
 static const struct sway_view_impl view_impl = {
+	.get_constraints = get_constraints,
 	.get_string_prop = get_string_prop,
 	.configure = configure,
 	.set_activated = set_activated,
@@ -191,7 +224,9 @@ static const struct sway_view_impl view_impl = {
 	.set_fullscreen = set_fullscreen,
 	.wants_floating = wants_floating,
 	.for_each_surface = for_each_surface,
+	.for_each_popup = for_each_popup,
 	.close = _close,
+	.close_popups = close_popups,
 	.destroy = destroy,
 };
 
@@ -208,10 +243,24 @@ static void handle_commit(struct wl_listener *listener, void *data) {
 		transaction_notify_view_ready(view, xdg_surface_v6->configure_serial);
 	}
 
-	view_update_title(view, false);
 	view_damage_from(view);
 }
 
+static void handle_set_title(struct wl_listener *listener, void *data) {
+	struct sway_xdg_shell_v6_view *xdg_shell_v6_view =
+		wl_container_of(listener, xdg_shell_v6_view, set_title);
+	struct sway_view *view = &xdg_shell_v6_view->view;
+	view_update_title(view, false);
+	view_execute_criteria(view);
+}
+
+static void handle_set_app_id(struct wl_listener *listener, void *data) {
+	struct sway_xdg_shell_v6_view *xdg_shell_v6_view =
+		wl_container_of(listener, xdg_shell_v6_view, set_app_id);
+	struct sway_view *view = &xdg_shell_v6_view->view;
+	view_execute_criteria(view);
+}
+
 static void handle_new_popup(struct wl_listener *listener, void *data) {
 	struct sway_xdg_shell_v6_view *xdg_shell_v6_view =
 		wl_container_of(listener, xdg_shell_v6_view, new_popup);
@@ -236,13 +285,41 @@ static void handle_request_fullscreen(struct wl_listener *listener, void *data)
 		return;
 	}
 
-	view_set_fullscreen(view, e->fullscreen);
+	container_set_fullscreen(view->swayc, e->fullscreen);
 
 	struct sway_container *output = container_parent(view->swayc, C_OUTPUT);
 	arrange_windows(output);
 	transaction_commit_dirty();
 }
 
+static void handle_request_move(struct wl_listener *listener, void *data) {
+	struct sway_xdg_shell_v6_view *xdg_shell_v6_view =
+		wl_container_of(listener, xdg_shell_v6_view, request_move);
+	struct sway_view *view = &xdg_shell_v6_view->view;
+	if (!container_is_floating(view->swayc)) {
+		return;
+	}
+	struct wlr_xdg_toplevel_v6_move_event *e = data;
+	struct sway_seat *seat = e->seat->seat->data;
+	if (e->serial == seat->last_button_serial) {
+		seat_begin_move(seat, view->swayc, seat->last_button);
+	}
+}
+
+static void handle_request_resize(struct wl_listener *listener, void *data) {
+	struct sway_xdg_shell_v6_view *xdg_shell_v6_view =
+		wl_container_of(listener, xdg_shell_v6_view, request_resize);
+	struct sway_view *view = &xdg_shell_v6_view->view;
+	if (!container_is_floating(view->swayc)) {
+		return;
+	}
+	struct wlr_xdg_toplevel_v6_resize_event *e = data;
+	struct sway_seat *seat = e->seat->seat->data;
+	if (e->serial == seat->last_button_serial) {
+		seat_begin_resize(seat, view->swayc, seat->last_button, e->edges);
+	}
+}
+
 static void handle_unmap(struct wl_listener *listener, void *data) {
 	struct sway_xdg_shell_v6_view *xdg_shell_v6_view =
 		wl_container_of(listener, xdg_shell_v6_view, unmap);
@@ -257,6 +334,10 @@ static void handle_unmap(struct wl_listener *listener, void *data) {
 	wl_list_remove(&xdg_shell_v6_view->commit.link);
 	wl_list_remove(&xdg_shell_v6_view->new_popup.link);
 	wl_list_remove(&xdg_shell_v6_view->request_fullscreen.link);
+	wl_list_remove(&xdg_shell_v6_view->request_move.link);
+	wl_list_remove(&xdg_shell_v6_view->request_resize.link);
+	wl_list_remove(&xdg_shell_v6_view->set_title.link);
+	wl_list_remove(&xdg_shell_v6_view->set_app_id.link);
 }
 
 static void handle_map(struct wl_listener *listener, void *data) {
@@ -275,7 +356,7 @@ static void handle_map(struct wl_listener *listener, void *data) {
 	view_map(view, view->wlr_xdg_surface_v6->surface);
 
 	if (xdg_surface->toplevel->client_pending.fullscreen) {
-		view_set_fullscreen(view, true);
+		container_set_fullscreen(view->swayc, true);
 		struct sway_container *ws = container_parent(view->swayc, C_WORKSPACE);
 		arrange_windows(ws);
 	} else {
@@ -294,6 +375,22 @@ static void handle_map(struct wl_listener *listener, void *data) {
 	xdg_shell_v6_view->request_fullscreen.notify = handle_request_fullscreen;
 	wl_signal_add(&xdg_surface->toplevel->events.request_fullscreen,
 			&xdg_shell_v6_view->request_fullscreen);
+
+	xdg_shell_v6_view->request_move.notify = handle_request_move;
+	wl_signal_add(&xdg_surface->toplevel->events.request_move,
+			&xdg_shell_v6_view->request_move);
+
+	xdg_shell_v6_view->request_resize.notify = handle_request_resize;
+	wl_signal_add(&xdg_surface->toplevel->events.request_resize,
+			&xdg_shell_v6_view->request_resize);
+
+	xdg_shell_v6_view->set_title.notify = handle_set_title;
+	wl_signal_add(&xdg_surface->toplevel->events.set_title,
+			&xdg_shell_v6_view->set_title);
+
+	xdg_shell_v6_view->set_app_id.notify = handle_set_app_id;
+	wl_signal_add(&xdg_surface->toplevel->events.set_app_id,
+			&xdg_shell_v6_view->set_app_id);
 }
 
 static void handle_destroy(struct wl_listener *listener, void *data) {
@@ -335,9 +432,6 @@ void handle_xdg_shell_v6_surface(struct wl_listener *listener, void *data) {
 	view_init(&xdg_shell_v6_view->view, SWAY_VIEW_XDG_SHELL_V6, &view_impl);
 	xdg_shell_v6_view->view.wlr_xdg_surface_v6 = xdg_surface;
 
-	// TODO:
-	// - Look up pid and open on appropriate workspace
-
 	xdg_shell_v6_view->map.notify = handle_map;
 	wl_signal_add(&xdg_surface->events.map, &xdg_shell_v6_view->map);
 
diff --git a/sway/desktop/xwayland.c b/sway/desktop/xwayland.c
index bce0a37b..398446f8 100644
--- a/sway/desktop/xwayland.c
+++ b/sway/desktop/xwayland.c
@@ -69,11 +69,13 @@ static void unmanaged_handle_map(struct wl_listener *listener, void *data) {
 	surface->ly = xsurface->y;
 	desktop_damage_surface(xsurface->surface, surface->lx, surface->ly, true);
 
-	struct sway_seat *seat = input_manager_current_seat(input_manager);
-	struct wlr_xwayland *xwayland =
-		seat->input->server->xwayland.wlr_xwayland;
-	wlr_xwayland_set_seat(xwayland, seat->wlr_seat);
-	seat_set_focus_surface(seat, xsurface->surface, false);
+	if (wlr_xwayland_or_surface_wants_focus(xsurface)) {
+		struct sway_seat *seat = input_manager_current_seat(input_manager);
+		struct wlr_xwayland *xwayland =
+			seat->input->server->xwayland.wlr_xwayland;
+		wlr_xwayland_set_seat(xwayland, seat->wlr_seat);
+		seat_set_focus_surface(seat, xsurface->surface, false);
+	}
 }
 
 static void unmanaged_handle_unmap(struct wl_listener *listener, void *data) {
@@ -305,6 +307,8 @@ static void handle_destroy(struct wl_listener *listener, void *data) {
 	wl_list_remove(&xwayland_view->destroy.link);
 	wl_list_remove(&xwayland_view->request_configure.link);
 	wl_list_remove(&xwayland_view->request_fullscreen.link);
+	wl_list_remove(&xwayland_view->request_move.link);
+	wl_list_remove(&xwayland_view->request_resize.link);
 	wl_list_remove(&xwayland_view->set_title.link);
 	wl_list_remove(&xwayland_view->set_class.link);
 	wl_list_remove(&xwayland_view->set_window_type.link);
@@ -355,7 +359,7 @@ static void handle_map(struct wl_listener *listener, void *data) {
 	view_map(view, xsurface->surface);
 
 	if (xsurface->fullscreen) {
-		view_set_fullscreen(view, true);
+		container_set_fullscreen(view->swayc, true);
 		struct sway_container *ws = container_parent(view->swayc, C_WORKSPACE);
 		arrange_windows(ws);
 	} else {
@@ -393,13 +397,44 @@ static void handle_request_fullscreen(struct wl_listener *listener, void *data)
 	if (!xsurface->mapped) {
 		return;
 	}
-	view_set_fullscreen(view, xsurface->fullscreen);
+	container_set_fullscreen(view->swayc, xsurface->fullscreen);
 
 	struct sway_container *output = container_parent(view->swayc, C_OUTPUT);
 	arrange_windows(output);
 	transaction_commit_dirty();
 }
 
+static void handle_request_move(struct wl_listener *listener, void *data) {
+	struct sway_xwayland_view *xwayland_view =
+		wl_container_of(listener, xwayland_view, request_move);
+	struct sway_view *view = &xwayland_view->view;
+	struct wlr_xwayland_surface *xsurface = view->wlr_xwayland_surface;
+	if (!xsurface->mapped) {
+		return;
+	}
+	if (!container_is_floating(view->swayc)) {
+		return;
+	}
+	struct sway_seat *seat = input_manager_current_seat(input_manager);
+	seat_begin_move(seat, view->swayc, seat->last_button);
+}
+
+static void handle_request_resize(struct wl_listener *listener, void *data) {
+	struct sway_xwayland_view *xwayland_view =
+		wl_container_of(listener, xwayland_view, request_resize);
+	struct sway_view *view = &xwayland_view->view;
+	struct wlr_xwayland_surface *xsurface = view->wlr_xwayland_surface;
+	if (!xsurface->mapped) {
+		return;
+	}
+	if (!container_is_floating(view->swayc)) {
+		return;
+	}
+	struct wlr_xwayland_resize_event *e = data;
+	struct sway_seat *seat = input_manager_current_seat(input_manager);
+	seat_begin_resize(seat, view->swayc, seat->last_button, e->edges);
+}
+
 static void handle_set_title(struct wl_listener *listener, void *data) {
 	struct sway_xwayland_view *xwayland_view =
 		wl_container_of(listener, xwayland_view, set_title);
@@ -481,9 +516,6 @@ void handle_xwayland_surface(struct wl_listener *listener, void *data) {
 	view_init(&xwayland_view->view, SWAY_VIEW_XWAYLAND, &view_impl);
 	xwayland_view->view.wlr_xwayland_surface = xsurface;
 
-	// TODO:
-	// - Look up pid and open on appropriate workspace
-
 	wl_signal_add(&xsurface->events.destroy, &xwayland_view->destroy);
 	xwayland_view->destroy.notify = handle_destroy;
 
@@ -495,6 +527,14 @@ void handle_xwayland_surface(struct wl_listener *listener, void *data) {
 		&xwayland_view->request_fullscreen);
 	xwayland_view->request_fullscreen.notify = handle_request_fullscreen;
 
+	wl_signal_add(&xsurface->events.request_move,
+		&xwayland_view->request_move);
+	xwayland_view->request_move.notify = handle_request_move;
+
+	wl_signal_add(&xsurface->events.request_resize,
+		&xwayland_view->request_resize);
+	xwayland_view->request_resize.notify = handle_request_resize;
+
 	wl_signal_add(&xsurface->events.set_title, &xwayland_view->set_title);
 	xwayland_view->set_title.notify = handle_set_title;
 
diff --git a/sway/input/cursor.c b/sway/input/cursor.c
index c76c20b3..c2fc4e9e 100644
--- a/sway/input/cursor.c
+++ b/sway/input/cursor.c
@@ -5,15 +5,20 @@
 #elif __FreeBSD__
 #include <dev/evdev/input-event-codes.h>
 #endif
+#include <limits.h>
 #include <wlr/types/wlr_cursor.h>
 #include <wlr/types/wlr_xcursor_manager.h>
 #include <wlr/types/wlr_idle.h>
 #include "list.h"
 #include "log.h"
+#include "config.h"
+#include "sway/desktop.h"
 #include "sway/desktop/transaction.h"
 #include "sway/input/cursor.h"
+#include "sway/input/keyboard.h"
 #include "sway/layers.h"
 #include "sway/output.h"
+#include "sway/tree/arrange.h"
 #include "sway/tree/view.h"
 #include "sway/tree/workspace.h"
 #include "wlr-layer-shell-unstable-v1-protocol.h"
@@ -50,6 +55,7 @@ static struct sway_container *container_at_coords(
 		struct sway_seat *seat, double lx, double ly,
 		struct wlr_surface **surface, double *sx, double *sy) {
 	// check for unmanaged views first
+#ifdef HAVE_XWAYLAND
 	struct wl_list *unmanaged = &root_container.sway_root->xwayland_unmanaged;
 	struct sway_xwayland_unmanaged *unmanaged_surface;
 	wl_list_for_each_reverse(unmanaged_surface, unmanaged, link) {
@@ -65,7 +71,7 @@ static struct sway_container *container_at_coords(
 			return NULL;
 		}
 	}
-
+#endif
 	// find the output the cursor is on
 	struct wlr_output_layout *output_layout =
 		root_container.sway_root->output_layout;
@@ -93,14 +99,8 @@ static struct sway_container *container_at_coords(
 		return ws;
 	}
 	if (ws->sway_workspace->fullscreen) {
-		struct wlr_surface *wlr_surface = ws->sway_workspace->fullscreen->surface;
-		if (wlr_surface_point_accepts_input(wlr_surface, ox, oy)) {
-			*sx = ox;
-			*sy = oy;
-			*surface = wlr_surface;
-			return ws->sway_workspace->fullscreen->swayc;
-		}
-		return NULL;
+		return container_at_view(ws->sway_workspace->fullscreen, lx, ly,
+				surface, sx, sy);
 	}
 	if ((*surface = layer_surface_at(output,
 				&output->layers[ZWLR_LAYER_SHELL_V1_LAYER_TOP],
@@ -109,9 +109,6 @@ static struct sway_container *container_at_coords(
 	}
 
 	struct sway_container *c;
-	if ((c = floating_container_at(lx, ly, surface, sx, sy))) {
-		return c;
-	}
 	if ((c = container_at(ws, lx, ly, surface, sx, sy))) {
 		return c;
 	}
@@ -127,7 +124,7 @@ static struct sway_container *container_at_coords(
 		return ws;
 	}
 
-	c = seat_get_focus_inactive(seat, output->swayc);
+	c = seat_get_active_child(seat, output->swayc);
 	if (c) {
 		return c;
 	}
@@ -139,6 +136,171 @@ static struct sway_container *container_at_coords(
 	return output->swayc;
 }
 
+static enum wlr_edges find_resize_edge(struct sway_container *cont,
+		struct sway_cursor *cursor) {
+	if (cont->type != C_VIEW) {
+		return WLR_EDGE_NONE;
+	}
+	struct sway_view *view = cont->sway_view;
+	if (view->border == B_NONE || !view->border_thickness || view->using_csd) {
+		return WLR_EDGE_NONE;
+	}
+
+	enum wlr_edges edge = 0;
+	if (cursor->cursor->x < cont->x + view->border_thickness) {
+		edge |= WLR_EDGE_LEFT;
+	}
+	if (cursor->cursor->y < cont->y + view->border_thickness) {
+		edge |= WLR_EDGE_TOP;
+	}
+	if (cursor->cursor->x >= cont->x + cont->width - view->border_thickness) {
+		edge |= WLR_EDGE_RIGHT;
+	}
+	if (cursor->cursor->y >= cont->y + cont->height - view->border_thickness) {
+		edge |= WLR_EDGE_BOTTOM;
+	}
+	return edge;
+}
+
+static void handle_move_motion(struct sway_seat *seat,
+		struct sway_cursor *cursor) {
+	struct sway_container *con = seat->op_container;
+	desktop_damage_whole_container(con);
+	container_floating_translate(con,
+			cursor->cursor->x - cursor->previous.x,
+			cursor->cursor->y - cursor->previous.y);
+	desktop_damage_whole_container(con);
+}
+
+static void calculate_floating_constraints(struct sway_container *con,
+		int *min_width, int *max_width, int *min_height, int *max_height) {
+	if (config->floating_minimum_width == -1) { // no minimum
+		*min_width = 0;
+	} else if (config->floating_minimum_width == 0) { // automatic
+		*min_width = 75;
+	} else {
+		*min_width = config->floating_minimum_width;
+	}
+
+	if (config->floating_minimum_height == -1) { // no minimum
+		*min_height = 0;
+	} else if (config->floating_minimum_height == 0) { // automatic
+		*min_height = 50;
+	} else {
+		*min_height = config->floating_minimum_height;
+	}
+
+	if (config->floating_maximum_width == -1) { // no maximum
+		*max_width = INT_MAX;
+	} else if (config->floating_maximum_width == 0) { // automatic
+		struct sway_container *ws = container_parent(con, C_WORKSPACE);
+		*max_width = ws->width;
+	} else {
+		*max_width = config->floating_maximum_width;
+	}
+
+	if (config->floating_maximum_height == -1) { // no maximum
+		*max_height = INT_MAX;
+	} else if (config->floating_maximum_height == 0) { // automatic
+		struct sway_container *ws = container_parent(con, C_WORKSPACE);
+		*max_height = ws->height;
+	} else {
+		*max_height = config->floating_maximum_height;
+	}
+}
+
+static void handle_resize_motion(struct sway_seat *seat,
+		struct sway_cursor *cursor) {
+	struct sway_container *con = seat->op_container;
+	enum wlr_edges edge = seat->op_resize_edge;
+
+	// The amount the mouse has moved since the start of the resize operation
+	// Positive is down/right
+	double mouse_move_x = cursor->cursor->x - seat->op_ref_lx;
+	double mouse_move_y = cursor->cursor->y - seat->op_ref_ly;
+
+	if (edge == WLR_EDGE_TOP || edge == WLR_EDGE_BOTTOM) {
+		mouse_move_x = 0;
+	}
+	if (edge == WLR_EDGE_LEFT || edge == WLR_EDGE_RIGHT) {
+		mouse_move_y = 0;
+	}
+
+	double grow_width = edge & WLR_EDGE_LEFT ? -mouse_move_x : mouse_move_x;
+	double grow_height = edge & WLR_EDGE_TOP ? -mouse_move_y : mouse_move_y;
+
+	if (seat->op_resize_preserve_ratio) {
+		double x_multiplier = grow_width / seat->op_ref_width;
+		double y_multiplier = grow_height / seat->op_ref_height;
+		double max_multiplier = fmax(x_multiplier, y_multiplier);
+		grow_width = seat->op_ref_width * max_multiplier;
+		grow_height = seat->op_ref_height * max_multiplier;
+	}
+
+	// Determine new width/height, and accommodate for floating min/max values
+	double width = seat->op_ref_width + grow_width;
+	double height = seat->op_ref_height + grow_height;
+	int min_width, max_width, min_height, max_height;
+	calculate_floating_constraints(con, &min_width, &max_width,
+			&min_height, &max_height);
+	width = fmax(min_width, fmin(width, max_width));
+	height = fmax(min_height, fmin(height, max_height));
+
+	// Apply the view's min/max size
+	if (con->type == C_VIEW) {
+		double view_min_width, view_max_width, view_min_height, view_max_height;
+		view_get_constraints(con->sway_view, &view_min_width, &view_max_width,
+				&view_min_height, &view_max_height);
+		width = fmax(view_min_width, fmin(width, view_max_width));
+		height = fmax(view_min_height, fmin(height, view_max_height));
+	}
+
+	// Recalculate these, in case we hit a min/max limit
+	grow_width = width - seat->op_ref_width;
+	grow_height = height - seat->op_ref_height;
+
+	// Determine grow x/y values - these are relative to the container's x/y at
+	// the start of the resize operation.
+	double grow_x = 0, grow_y = 0;
+	if (edge & WLR_EDGE_LEFT) {
+		grow_x = -grow_width;
+	} else if (edge & WLR_EDGE_RIGHT) {
+		grow_x = 0;
+	} else {
+		grow_x = -grow_width / 2;
+	}
+	if (edge & WLR_EDGE_TOP) {
+		grow_y = -grow_height;
+	} else if (edge & WLR_EDGE_BOTTOM) {
+		grow_y = 0;
+	} else {
+		grow_y = -grow_height / 2;
+	}
+
+	// Determine the amounts we need to bump everything relative to the current
+	// size.
+	int relative_grow_width = width - con->width;
+	int relative_grow_height = height - con->height;
+	int relative_grow_x = (seat->op_ref_con_lx + grow_x) - con->x;
+	int relative_grow_y = (seat->op_ref_con_ly + grow_y) - con->y;
+
+	// Actually resize stuff
+	con->x += relative_grow_x;
+	con->y += relative_grow_y;
+	con->width += relative_grow_width;
+	con->height += relative_grow_height;
+
+	if (con->type == C_VIEW) {
+		struct sway_view *view = con->sway_view;
+		view->x += relative_grow_x;
+		view->y += relative_grow_y;
+		view->width += relative_grow_width;
+		view->height += relative_grow_height;
+	}
+
+	arrange_windows(con);
+}
+
 void cursor_send_pointer_motion(struct sway_cursor *cursor, uint32_t time_msec,
 		bool allow_refocusing) {
 	if (time_msec == 0) {
@@ -146,6 +308,18 @@ void cursor_send_pointer_motion(struct sway_cursor *cursor, uint32_t time_msec,
 	}
 
 	struct sway_seat *seat = cursor->seat;
+
+	if (seat->operation != OP_NONE) {
+		if (seat->operation == OP_MOVE) {
+			handle_move_motion(seat, cursor);
+		} else {
+			handle_resize_motion(seat, cursor);
+		}
+		cursor->previous.x = cursor->cursor->x;
+		cursor->previous.y = cursor->cursor->y;
+		return;
+	}
+
 	struct wlr_seat *wlr_seat = seat->wlr_seat;
 	struct wlr_surface *surface = NULL;
 	double sx, sy;
@@ -172,7 +346,7 @@ void cursor_send_pointer_motion(struct sway_cursor *cursor, uint32_t time_msec,
 				output = container_parent(c, C_OUTPUT);
 			}
 			if (output != focus) {
-				seat_set_focus_warp(seat, c, false);
+				seat_set_focus_warp(seat, c, false, true);
 			}
 		} else if (c->type == C_VIEW) {
 			// Focus c if the following are true:
@@ -182,27 +356,33 @@ void cursor_send_pointer_motion(struct sway_cursor *cursor, uint32_t time_msec,
 			if (!wlr_seat_keyboard_has_grab(cursor->seat->wlr_seat) &&
 					c != prev_c &&
 					view_is_visible(c->sway_view)) {
-				seat_set_focus_warp(seat, c, false);
+				seat_set_focus_warp(seat, c, false, true);
 			} else {
 				struct sway_container *next_focus =
 					seat_get_focus_inactive(seat, &root_container);
 				if (next_focus && next_focus->type == C_VIEW &&
 						view_is_visible(next_focus->sway_view)) {
-					seat_set_focus_warp(seat, next_focus, false);
+					seat_set_focus_warp(seat, next_focus, false, true);
 				}
 			}
 		}
 	}
 
-	// reset cursor if switching between clients
-	struct wl_client *client = NULL;
-	if (surface != NULL) {
-		client = wl_resource_get_client(surface->resource);
-	}
-	if (client != cursor->image_client) {
-		wlr_xcursor_manager_set_cursor_image(cursor->xcursor_manager,
-			"left_ptr", cursor->cursor);
-		cursor->image_client = client;
+	// Handle cursor image
+	if (surface) {
+		// Reset cursor if switching between clients
+		struct wl_client *client = wl_resource_get_client(surface->resource);
+		if (client != cursor->image_client) {
+			cursor_set_image(cursor, "left_ptr", client);
+		}
+	} else if (c && container_is_floating(c)) {
+		// Try a floating container's resize edge
+		enum wlr_edges edge = find_resize_edge(c, cursor);
+		const char *image = edge == WLR_EDGE_NONE ?
+			"left_ptr" : wlr_xcursor_get_resize_name(edge);
+		cursor_set_image(cursor, image, NULL);
+	} else {
+		cursor_set_image(cursor, "left_ptr", NULL);
 	}
 
 	// send pointer enter/leave
@@ -243,8 +423,142 @@ static void handle_cursor_motion_absolute(
 	transaction_commit_dirty();
 }
 
+static void dispatch_cursor_button_floating(struct sway_cursor *cursor,
+		uint32_t time_msec, uint32_t button, enum wlr_button_state state,
+		struct wlr_surface *surface, double sx, double sy,
+		struct sway_container *cont) {
+	struct sway_seat *seat = cursor->seat;
+
+	// Deny moving or resizing a fullscreen container
+	if (container_is_fullscreen_or_child(cont)) {
+		seat_pointer_notify_button(seat, time_msec, button, state);
+		return;
+	}
+	struct sway_container *floater = cont;
+	while (floater->parent->layout != L_FLOATING) {
+		floater = floater->parent;
+	}
+
+	struct wlr_keyboard *keyboard = wlr_seat_get_keyboard(seat->wlr_seat);
+	bool mod_pressed = keyboard &&
+		(wlr_keyboard_get_modifiers(keyboard) & config->floating_mod);
+	enum wlr_edges edge = find_resize_edge(floater, cursor);
+	bool over_title = edge == WLR_EDGE_NONE && !surface;
+
+	// Check for beginning move
+	uint32_t btn_move = config->floating_mod_inverse ? BTN_RIGHT : BTN_LEFT;
+	if (button == btn_move && state == WLR_BUTTON_PRESSED &&
+			(mod_pressed || over_title)) {
+		seat_begin_move(seat, floater, button);
+		return;
+	}
+
+	// Check for beginning resize
+	bool resizing_via_border = button == BTN_LEFT && edge != WLR_EDGE_NONE;
+	uint32_t btn_resize = config->floating_mod_inverse ? BTN_LEFT : BTN_RIGHT;
+	bool resizing_via_mod = button == btn_resize && mod_pressed;
+	if ((resizing_via_border || resizing_via_mod) &&
+			state == WLR_BUTTON_PRESSED) {
+		if (edge == WLR_EDGE_NONE) {
+			edge |= cursor->cursor->x > floater->x + floater->width / 2 ?
+				WLR_EDGE_RIGHT : WLR_EDGE_LEFT;
+			edge |= cursor->cursor->y > floater->y + floater->height / 2 ?
+				WLR_EDGE_BOTTOM : WLR_EDGE_TOP;
+		}
+		seat_begin_resize(seat, floater, button, edge);
+		return;
+	}
+
+	// Send event to surface
+	seat_set_focus(seat, cont);
+	seat_pointer_notify_button(seat, time_msec, button, state);
+}
+
+/**
+ * Remove a button (and duplicates) to the sorted list of currently pressed buttons
+ */
+static void state_erase_button(struct sway_cursor *cursor, uint32_t button) {
+	size_t j = 0;
+	for (size_t i = 0; i < cursor->pressed_button_count; ++i) {
+		if (i > j) {
+			cursor->pressed_buttons[j] = cursor->pressed_buttons[i];
+		}
+		if (cursor->pressed_buttons[i] != button) {
+			++j;
+		}
+	}
+	while (cursor->pressed_button_count > j) {
+		--cursor->pressed_button_count;
+		cursor->pressed_buttons[cursor->pressed_button_count] = 0;
+	}
+}
+
+/**
+ * Add a button to the sorted list of currently pressed buttons, if there
+ * is space.
+ */
+static void state_add_button(struct sway_cursor *cursor, uint32_t button) {
+	if (cursor->pressed_button_count >= SWAY_CURSOR_PRESSED_BUTTONS_CAP) {
+		return;
+	}
+	size_t i = 0;
+	while (i < cursor->pressed_button_count && cursor->pressed_buttons[i] < button) {
+		++i;
+	}
+	size_t j = cursor->pressed_button_count;
+	while (j > i) {
+		cursor->pressed_buttons[j] = cursor->pressed_buttons[j - 1];
+		--j;
+	}
+	cursor->pressed_buttons[i] = button;
+	cursor->pressed_button_count++;
+}
+
+/**
+ * Return the mouse binding which matches modifier, click location, release,
+ * and pressed button state, otherwise return null.
+ */
+static struct sway_binding* get_active_mouse_binding(const struct sway_cursor *cursor,
+		list_t *bindings, uint32_t modifiers, bool release, bool on_titlebar,
+				     bool on_border, bool on_content) {
+	uint32_t click_region = (on_titlebar ? BINDING_TITLEBAR : 0) |
+			(on_border ? BINDING_BORDER : 0) |
+			(on_content ? BINDING_CONTENTS : 0);
+
+	for (int i = 0; i < bindings->length; ++i) {
+		struct sway_binding *binding = bindings->items[i];
+		if (modifiers ^ binding->modifiers ||
+				cursor->pressed_button_count != (size_t)binding->keys->length ||
+				release != (binding->flags & BINDING_RELEASE) ||
+				!(click_region & binding->flags)) {
+			continue;
+		}
+
+		bool match = true;
+		for (size_t j = 0; j < cursor->pressed_button_count; j++) {
+			uint32_t key = *(uint32_t *)binding->keys->items[j];
+			if (key != cursor->pressed_buttons[j]) {
+				match = false;
+				break;
+			}
+		}
+		if (!match) {
+			continue;
+		}
+
+		return binding;
+	}
+	return NULL;
+}
+
 void dispatch_cursor_button(struct sway_cursor *cursor,
 		uint32_t time_msec, uint32_t button, enum wlr_button_state state) {
+	if (cursor->seat->operation != OP_NONE &&
+			button == cursor->seat->op_button && state == WLR_BUTTON_RELEASED) {
+		seat_end_mouse_operation(cursor->seat);
+		seat_pointer_notify_button(cursor->seat, time_msec, button, state);
+		return;
+	}
 	if (time_msec == 0) {
 		time_msec = get_current_time_msec();
 	}
@@ -253,12 +567,41 @@ void dispatch_cursor_button(struct sway_cursor *cursor,
 	double sx, sy;
 	struct sway_container *cont = container_at_coords(cursor->seat,
 			cursor->cursor->x, cursor->cursor->y, &surface, &sx, &sy);
+
+	// Handle mouse bindings
+	bool on_border = cont && (find_resize_edge(cont, cursor) != WLR_EDGE_NONE);
+	bool on_contents = !on_border && surface;
+	bool on_titlebar = !on_border && !surface;
+	struct wlr_keyboard *keyboard = wlr_seat_get_keyboard(cursor->seat->wlr_seat);
+	uint32_t modifiers = keyboard ? wlr_keyboard_get_modifiers(keyboard) : 0;
+
+	struct sway_binding *binding = NULL;
+	if (state == WLR_BUTTON_PRESSED) {
+		state_add_button(cursor, button);
+		binding = get_active_mouse_binding(cursor,
+			config->current_mode->mouse_bindings, modifiers, false,
+			on_titlebar, on_border, on_contents);
+	} else {
+		binding = get_active_mouse_binding(cursor,
+			config->current_mode->mouse_bindings, modifiers, true,
+			on_titlebar, on_border, on_contents);
+		state_erase_button(cursor, button);
+	}
+	if (binding) {
+		seat_execute_command(cursor->seat, binding);
+		// TODO: do we want to pass on the event?
+	}
+
 	if (surface && wlr_surface_is_layer_surface(surface)) {
 		struct wlr_layer_surface *layer =
 			wlr_layer_surface_from_wlr_surface(surface);
 		if (layer->current.keyboard_interactive) {
 			seat_set_focus_layer(cursor->seat, layer);
 		}
+		seat_pointer_notify_button(cursor->seat, time_msec, button, state);
+	} else if (cont && container_is_floating_or_child(cont)) {
+		dispatch_cursor_button_floating(cursor, time_msec, button, state,
+				surface, sx, sy, cont);
 	} else if (surface && cont && cont->type != C_VIEW) {
 		// Avoid moving keyboard focus from a surface that accepts it to one
 		// that does not unless the change would move us to a new workspace.
@@ -275,12 +618,14 @@ void dispatch_cursor_button(struct sway_cursor *cursor,
 		if (new_ws != old_ws) {
 			seat_set_focus(cursor->seat, cont);
 		}
+		seat_pointer_notify_button(cursor->seat, time_msec, button, state);
 	} else if (cont) {
 		seat_set_focus(cursor->seat, cont);
+		seat_pointer_notify_button(cursor->seat, time_msec, button, state);
+	} else {
+		seat_pointer_notify_button(cursor->seat, time_msec, button, state);
 	}
 
-	wlr_seat_pointer_notify_button(cursor->seat->wlr_seat,
-			time_msec, button, state);
 	transaction_commit_dirty();
 }
 
@@ -467,6 +812,9 @@ static void handle_request_set_cursor(struct wl_listener *listener,
 		void *data) {
 	struct sway_cursor *cursor =
 		wl_container_of(listener, cursor, request_set_cursor);
+	if (cursor->seat->operation != OP_NONE) {
+		return;
+	}
 	struct wlr_seat_pointer_request_set_cursor_event *event = data;
 
 	struct wl_client *focused_client = NULL;
@@ -485,9 +833,20 @@ static void handle_request_set_cursor(struct wl_listener *listener,
 
 	wlr_cursor_set_surface(cursor->cursor, event->surface, event->hotspot_x,
 		event->hotspot_y);
+	cursor->image = NULL;
 	cursor->image_client = focused_client;
 }
 
+void cursor_set_image(struct sway_cursor *cursor, const char *image,
+		struct wl_client *client) {
+	if (!cursor->image || strcmp(cursor->image, image) != 0) {
+		wlr_xcursor_manager_set_cursor_image(cursor->xcursor_manager, image,
+				cursor->cursor);
+		cursor->image = image;
+	}
+	cursor->image_client = client;
+}
+
 void sway_cursor_destroy(struct sway_cursor *cursor) {
 	if (!cursor) {
 		return;
diff --git a/sway/input/input-manager.c b/sway/input/input-manager.c
index 0b7cb766..c820e032 100644
--- a/sway/input/input-manager.c
+++ b/sway/input/input-manager.c
@@ -8,6 +8,7 @@
 #include <math.h>
 #include <wlr/backend/libinput.h>
 #include <wlr/types/wlr_input_inhibitor.h>
+#include <wlr/types/wlr_virtual_keyboard_v1.h>
 #include "sway/config.h"
 #include "sway/input/input-manager.h"
 #include "sway/input/seat.h"
@@ -303,6 +304,35 @@ static void handle_inhibit_deactivate(struct wl_listener *listener, void *data)
 	}
 }
 
+void handle_virtual_keyboard(struct wl_listener *listener, void *data) {
+	struct sway_input_manager *input_manager =
+		wl_container_of(listener, input_manager, virtual_keyboard_new);
+	struct wlr_virtual_keyboard_v1 *keyboard = data;
+	struct wlr_input_device *device = &keyboard->input_device;
+
+	struct sway_seat *seat = input_manager_get_default_seat(input_manager);
+
+	// TODO: The user might want this on a different seat
+	struct sway_input_device *input_device =
+		calloc(1, sizeof(struct sway_input_device));
+	if (!sway_assert(input_device, "could not allocate input device")) {
+		return;
+	}
+	device->data = input_device;
+
+	input_device->wlr_device = device;
+	input_device->identifier = get_device_identifier(device);
+	wl_list_insert(&input_manager->devices, &input_device->link);
+
+	wlr_log(WLR_DEBUG, "adding virtual keyboard: '%s'",
+		input_device->identifier);
+
+	wl_signal_add(&device->events.destroy, &input_device->device_destroy);
+	input_device->device_destroy.notify = handle_device_destroy;
+
+	seat_add_device(seat, input_device);
+}
+
 struct sway_input_manager *input_manager_create(
 		struct sway_server *server) {
 	struct sway_input_manager *input =
@@ -321,6 +351,12 @@ struct sway_input_manager *input_manager_create(
 	input->new_input.notify = handle_new_input;
 	wl_signal_add(&server->backend->events.new_input, &input->new_input);
 
+	input->virtual_keyboard = wlr_virtual_keyboard_manager_v1_create(
+		server->wl_display);
+	wl_signal_add(&input->virtual_keyboard->events.new_virtual_keyboard,
+		&input->virtual_keyboard_new);
+	input->virtual_keyboard_new.notify = handle_virtual_keyboard;
+
 	input->inhibit = wlr_input_inhibit_manager_create(server->wl_display);
 	input->inhibit_activate.notify = handle_inhibit_activate;
 	wl_signal_add(&input->inhibit->events.activate,
diff --git a/sway/input/keyboard.c b/sway/input/keyboard.c
index ede38519..160ef10b 100644
--- a/sway/input/keyboard.c
+++ b/sway/input/keyboard.c
@@ -3,11 +3,12 @@
 #include <wlr/backend/multi.h>
 #include <wlr/backend/session.h>
 #include <wlr/types/wlr_idle.h>
-#include "sway/desktop/transaction.h"
-#include "sway/input/seat.h"
-#include "sway/input/keyboard.h"
-#include "sway/input/input-manager.h"
+#include <wlr/interfaces/wlr_keyboard.h>
 #include "sway/commands.h"
+#include "sway/desktop/transaction.h"
+#include "sway/input/input-manager.h"
+#include "sway/input/keyboard.h"
+#include "sway/input/seat.h"
 #include "log.h"
 
 /**
@@ -88,11 +89,13 @@ static void get_active_binding(const struct sway_shortcut_state *state,
 		uint32_t modifiers, bool release, bool locked) {
 	for (int i = 0; i < bindings->length; ++i) {
 		struct sway_binding *binding = bindings->items[i];
+		bool binding_locked = binding->flags & BINDING_LOCKED;
+		bool binding_release = binding->flags & BINDING_RELEASE;
 
 		if (modifiers ^ binding->modifiers ||
 				state->npressed != (size_t)binding->keys->length ||
-				locked > binding->locked ||
-				release != binding->release) {
+				release != binding_release ||
+				locked > binding_locked) {
 			continue;
 		}
 
@@ -118,23 +121,6 @@ static void get_active_binding(const struct sway_shortcut_state *state,
 	}
 }
 
-/**
- * Execute the command associated to a binding
- */
-static void keyboard_execute_command(struct sway_keyboard *keyboard,
-		struct sway_binding *binding) {
-	wlr_log(WLR_DEBUG, "running command for binding: %s",
-		binding->command);
-	config->handler_context.seat = keyboard->seat_device->sway_seat;
-	struct cmd_results *results = execute_command(binding->command, NULL);
-	transaction_commit_dirty();
-	if (results->status != CMD_SUCCESS) {
-		wlr_log(WLR_DEBUG, "could not run command for binding: %s (%s)",
-			binding->command, results->error);
-	}
-	free_cmd_results(results);
-}
-
 /**
  * Execute a built-in, hardcoded compositor binding. These are triggered from a
  * single keysym.
@@ -211,12 +197,13 @@ static size_t keyboard_keysyms_raw(struct sway_keyboard *keyboard,
 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 sway_seat* seat = keyboard->seat_device->sway_seat;
+	struct wlr_seat *wlr_seat = seat->wlr_seat;
 	struct wlr_input_device *wlr_device =
 		keyboard->seat_device->input_device->wlr_device;
-	wlr_idle_notify_activity(keyboard->seat_device->sway_seat->input->server->idle, wlr_seat);
+	wlr_idle_notify_activity(seat->input->server->idle, wlr_seat);
 	struct wlr_event_keyboard_key *event = data;
-	bool input_inhibited = keyboard->seat_device->sway_seat->exclusive_client != NULL;
+	bool input_inhibited = seat->exclusive_client != NULL;
 
 	// Identify new keycode, raw keysym(s), and translated keysym(s)
 	xkb_keycode_t keycode = event->keycode + 8;
@@ -266,7 +253,7 @@ static void handle_keyboard_key(struct wl_listener *listener, void *data) {
 	// Execute stored release binding once no longer active
 	if (keyboard->held_binding && binding_released != keyboard->held_binding &&
 			event->state == WLR_KEY_RELEASED) {
-		keyboard_execute_command(keyboard, keyboard->held_binding);
+		seat_execute_command(seat, keyboard->held_binding);
 		handled = true;
 	}
 	if (binding_released != keyboard->held_binding) {
@@ -277,6 +264,7 @@ static void handle_keyboard_key(struct wl_listener *listener, void *data) {
 	}
 
 	// Identify and execute active pressed binding
+	struct sway_binding *next_repeat_binding = NULL;
 	if (event->state == WLR_KEY_PRESSED) {
 		struct sway_binding *binding_pressed = NULL;
 		get_active_binding(&keyboard->state_keycodes,
@@ -290,8 +278,23 @@ static void handle_keyboard_key(struct wl_listener *listener, void *data) {
 				raw_modifiers, false, input_inhibited);
 
 		if (binding_pressed) {
-			keyboard_execute_command(keyboard, binding_pressed);
+			seat_execute_command(seat, binding_pressed);
 			handled = true;
+			next_repeat_binding = binding_pressed;
+		}
+	}
+
+	// Set up (or clear) keyboard repeat for a pressed binding
+	if (next_repeat_binding && wlr_device->keyboard->repeat_info.delay > 0) {
+		keyboard->repeat_binding = next_repeat_binding;
+		if (wl_event_source_timer_update(keyboard->key_repeat_source,
+				wlr_device->keyboard->repeat_info.delay) < 0) {
+			wlr_log(WLR_DEBUG, "failed to set key repeat timer");
+		}
+	} else if (keyboard->repeat_binding) {
+		keyboard->repeat_binding = NULL;
+		if (wl_event_source_timer_update(keyboard->key_repeat_source, 0) < 0) {
+			wlr_log(WLR_DEBUG, "failed to disarm key repeat timer");
 		}
 	}
 
@@ -312,6 +315,28 @@ static void handle_keyboard_key(struct wl_listener *listener, void *data) {
 		wlr_seat_keyboard_notify_key(wlr_seat, event->time_msec,
 				event->keycode, event->state);
 	}
+
+	transaction_commit_dirty();
+}
+
+static int handle_keyboard_repeat(void *data) {
+	struct sway_keyboard *keyboard = (struct sway_keyboard *)data;
+	struct wlr_keyboard *wlr_device =
+			keyboard->seat_device->input_device->wlr_device->keyboard;
+	if (keyboard->repeat_binding) {
+		if (wlr_device->repeat_info.rate > 0) {
+			// We queue the next event first, as the command might cancel it
+			if (wl_event_source_timer_update(keyboard->key_repeat_source,
+					1000 / wlr_device->repeat_info.rate) < 0) {
+				wlr_log(WLR_DEBUG, "failed to update key repeat timer");
+			}
+		}
+
+		seat_execute_command(keyboard->seat_device->sway_seat,
+				keyboard->repeat_binding);
+		transaction_commit_dirty();
+	}
+	return 0;
 }
 
 static void handle_keyboard_modifiers(struct wl_listener *listener,
@@ -339,6 +364,9 @@ struct sway_keyboard *sway_keyboard_create(struct sway_seat *seat,
 	wl_list_init(&keyboard->keyboard_key.link);
 	wl_list_init(&keyboard->keyboard_modifiers.link);
 
+	keyboard->key_repeat_source = wl_event_loop_add_timer(server.wl_event_loop,
+			handle_keyboard_repeat, keyboard);
+
 	return keyboard;
 }
 
@@ -397,6 +425,31 @@ void sway_keyboard_configure(struct sway_keyboard *keyboard) {
 	keyboard->keymap = keymap;
 	wlr_keyboard_set_keymap(wlr_device->keyboard, keyboard->keymap);
 
+	xkb_mod_mask_t locked_mods = 0;
+	if (input_config && input_config->xkb_numlock > 0) {
+		xkb_mod_index_t mod_index = xkb_map_mod_get_index(keymap, XKB_MOD_NAME_NUM);
+		if (mod_index != XKB_MOD_INVALID) {
+		       locked_mods |= (uint32_t)1 << mod_index;
+		}
+	}
+	if (input_config && input_config->xkb_capslock > 0) {
+		xkb_mod_index_t mod_index = xkb_map_mod_get_index(keymap, XKB_MOD_NAME_CAPS);
+		if (mod_index != XKB_MOD_INVALID) {
+		       locked_mods |= (uint32_t)1 << mod_index;
+		}
+	}
+	if (locked_mods) {
+		wlr_keyboard_notify_modifiers(wlr_device->keyboard, 0, 0, locked_mods, 0);
+		uint32_t leds = 0;
+		for (uint32_t i = 0; i < WLR_LED_COUNT; ++i) {
+			if (xkb_state_led_index_is_active(wlr_device->keyboard->xkb_state,
+					wlr_device->keyboard->led_indexes[i])) {
+				leds |= (1 << i);
+			}
+		}
+		wlr_keyboard_led_update(wlr_device->keyboard, leds);
+	}
+
 	if (input_config && input_config->repeat_delay != INT_MIN
 			&& input_config->repeat_rate != INT_MIN) {
 		wlr_keyboard_set_repeat_info(wlr_device->keyboard,
@@ -427,5 +480,6 @@ void sway_keyboard_destroy(struct sway_keyboard *keyboard) {
 	}
 	wl_list_remove(&keyboard->keyboard_key.link);
 	wl_list_remove(&keyboard->keyboard_modifiers.link);
+	wl_event_source_remove(keyboard->key_repeat_source);
 	free(keyboard);
 }
diff --git a/sway/input/seat.c b/sway/input/seat.c
index e77d88a8..dd4d5c3b 100644
--- a/sway/input/seat.c
+++ b/sway/input/seat.c
@@ -1,12 +1,19 @@
 #define _XOPEN_SOURCE 700
 #define _POSIX_C_SOURCE 199309L
 #include <assert.h>
+#include <errno.h>
+#ifdef __linux__
+#include <linux/input-event-codes.h>
+#elif __FreeBSD__
+#include <dev/evdev/input-event-codes.h>
+#endif
 #include <strings.h>
 #include <time.h>
 #include <wlr/types/wlr_cursor.h>
 #include <wlr/types/wlr_output_layout.h>
 #include <wlr/types/wlr_xcursor_manager.h>
 #include "log.h"
+#include "config.h"
 #include "sway/debug.h"
 #include "sway/desktop.h"
 #include "sway/input/cursor.h"
@@ -98,11 +105,13 @@ static void seat_send_focus(struct sway_container *con,
 
 	if (con->type == C_VIEW
 			&& seat_is_input_allowed(seat, con->sway_view->surface)) {
+#ifdef HAVE_XWAYLAND
 		if (con->sway_view->type == SWAY_VIEW_XWAYLAND) {
 			struct wlr_xwayland *xwayland =
 				seat->input->server->xwayland.wlr_xwayland;
 			wlr_xwayland_set_seat(xwayland, seat->wlr_seat);
 		}
+#endif
 		struct wlr_keyboard *keyboard = wlr_seat_get_keyboard(seat->wlr_seat);
 		if (keyboard) {
 			wlr_seat_keyboard_notify_enter(seat->wlr_seat,
@@ -116,12 +125,14 @@ static void seat_send_focus(struct sway_container *con,
 }
 
 static struct sway_container *seat_get_focus_by_type(struct sway_seat *seat,
-		struct sway_container *container, enum sway_container_type type) {
+		struct sway_container *container, enum sway_container_type type,
+		bool only_tiling) {
 	if (container->type == C_VIEW) {
 		return container;
 	}
 
-	struct sway_container *floating = container->type == C_WORKSPACE ?
+	struct sway_container *floating =
+		container->type == C_WORKSPACE && !only_tiling ?
 		container->sway_workspace->floating : NULL;
 	if (container->children->length == 0 &&
 			(!floating || floating->children->length == 0)) {
@@ -135,6 +146,10 @@ static struct sway_container *seat_get_focus_by_type(struct sway_seat *seat,
 		}
 
 		if (container_has_child(container, current->container)) {
+			if (only_tiling &&
+					container_is_floating_or_child(current->container)) {
+				continue;
+			}
 			return current->container;
 		}
 		if (floating && container_has_child(floating, current->container)) {
@@ -161,7 +176,7 @@ void seat_focus_inactive_children_for_each(struct sway_seat *seat,
 
 struct sway_container *seat_get_focus_inactive_view(struct sway_seat *seat,
 		struct sway_container *container) {
-	return seat_get_focus_by_type(seat, container, C_VIEW);
+	return seat_get_focus_by_type(seat, container, C_VIEW, false);
 }
 
 static void handle_seat_container_destroy(struct wl_listener *listener,
@@ -183,7 +198,7 @@ static void handle_seat_container_destroy(struct wl_listener *listener,
 	if (set_focus) {
 		struct sway_container *next_focus = NULL;
 		while (next_focus == NULL) {
-			next_focus = seat_get_focus_by_type(seat, parent, C_VIEW);
+			next_focus = seat_get_focus_by_type(seat, parent, C_VIEW, false);
 
 			if (next_focus == NULL && parent->type == C_WORKSPACE) {
 				next_focus = parent;
@@ -348,6 +363,7 @@ struct sway_seat *seat_create(struct sway_input_manager *input,
 		free(seat);
 		return NULL;
 	}
+	seat->wlr_seat->data = seat;
 
 	seat->cursor = sway_cursor_create(seat);
 	if (!seat->cursor) {
@@ -377,7 +393,6 @@ struct sway_seat *seat_create(struct sway_input_manager *input,
 		WL_SEAT_CAPABILITY_POINTER |
 		WL_SEAT_CAPABILITY_TOUCH);
 
-	seat_configure_xcursor(seat);
 
 	wl_list_insert(&input->seats, &seat->link);
 
@@ -422,6 +437,7 @@ static void seat_apply_input_config(struct sway_seat *seat,
 
 static void seat_configure_pointer(struct sway_seat *seat,
 		struct sway_seat_device *sway_device) {
+	seat_configure_xcursor(seat);
 	wlr_cursor_attach_input_device(seat->cursor->cursor,
 		sway_device->input_device->wlr_device);
 	seat_apply_input_config(seat, sway_device);
@@ -601,7 +617,7 @@ static int handle_urgent_timeout(void *data) {
 }
 
 void seat_set_focus_warp(struct sway_seat *seat,
-		struct sway_container *container, bool warp) {
+		struct sway_container *container, bool warp, bool notify) {
 	if (seat->focused_layer) {
 		return;
 	}
@@ -622,7 +638,7 @@ void seat_set_focus_warp(struct sway_seat *seat,
 
 	if (last_workspace && last_workspace == new_workspace
 			&& last_workspace->sway_workspace->fullscreen
-			&& !container->sway_view->is_fullscreen) {
+			&& container && !container_is_fullscreen_or_child(container)) {
 		return;
 	}
 
@@ -639,7 +655,7 @@ void seat_set_focus_warp(struct sway_seat *seat,
 	struct sway_container *new_output_last_ws = NULL;
 	if (last_output && new_output && last_output != new_output) {
 		new_output_last_ws =
-			seat_get_focus_by_type(seat, new_output, C_WORKSPACE);
+			seat_get_focus_by_type(seat, new_output, C_WORKSPACE, false);
 	}
 
 	if (container && container->parent) {
@@ -686,8 +702,14 @@ void seat_set_focus_warp(struct sway_seat *seat,
 				config->urgent_timeout > 0) {
 			view->urgent_timer = wl_event_loop_add_timer(server.wl_event_loop,
 					handle_urgent_timeout, view);
-			wl_event_source_timer_update(view->urgent_timer,
-					config->urgent_timeout);
+			if (view->urgent_timer) {
+				wl_event_source_timer_update(view->urgent_timer,
+						config->urgent_timeout);
+			} else {
+				wlr_log(WLR_ERROR, "Unable to create urgency timer (%s)",
+						strerror(errno));
+				handle_urgent_timeout(view);
+			}
 		} else {
 			view_set_urgent(view, false);
 		}
@@ -715,9 +737,18 @@ void seat_set_focus_warp(struct sway_seat *seat,
 		}
 	}
 
+	// Close any popups on the old focus
+	if (last_focus && last_focus != container) {
+		if (last_focus->type == C_VIEW) {
+			view_close_popups(last_focus->sway_view);
+		}
+	}
+
 	if (last_focus) {
 		if (last_workspace) {
-			ipc_event_workspace(last_workspace, container, "focus");
+			if (notify && last_workspace != new_workspace) {
+				 ipc_event_workspace(last_workspace, new_workspace, "focus");
+			}
 			if (!workspace_is_visible(last_workspace)
 					&& workspace_is_empty(last_workspace)) {
 				if (last_workspace == last_focus) {
@@ -744,8 +775,12 @@ void seat_set_focus_warp(struct sway_seat *seat,
 		}
 	}
 
-	if (last_focus != NULL) {
-		cursor_send_pointer_motion(seat->cursor, 0, true);
+	if (container) {
+		if (container->type == C_VIEW) {
+			ipc_event_window(container, "focus");
+		} else if (container->type == C_WORKSPACE) {
+			ipc_event_workspace(NULL, container, "focus");
+		}
 	}
 
 	seat->has_focus = (container != NULL);
@@ -755,7 +790,7 @@ void seat_set_focus_warp(struct sway_seat *seat,
 
 void seat_set_focus(struct sway_seat *seat,
 		struct sway_container *container) {
-	seat_set_focus_warp(seat, container, true);
+	seat_set_focus_warp(seat, container, true, true);
 }
 
 void seat_set_focus_surface(struct sway_seat *seat,
@@ -848,7 +883,12 @@ void seat_set_exclusive_client(struct sway_seat *seat,
 
 struct sway_container *seat_get_focus_inactive(struct sway_seat *seat,
 		struct sway_container *container) {
-	return seat_get_focus_by_type(seat, container, C_TYPES);
+	return seat_get_focus_by_type(seat, container, C_TYPES, false);
+}
+
+struct sway_container *seat_get_focus_inactive_tiling(struct sway_seat *seat,
+		struct sway_container *container) {
+	return seat_get_focus_by_type(seat, container, C_TYPES, true);
 }
 
 struct sway_container *seat_get_active_child(struct sway_seat *seat,
@@ -894,3 +934,68 @@ struct seat_config *seat_get_config(struct sway_seat *seat) {
 
 	return NULL;
 }
+
+void seat_begin_move(struct sway_seat *seat, struct sway_container *con,
+		uint32_t button) {
+	if (!seat->cursor) {
+		wlr_log(WLR_DEBUG, "Ignoring move request due to no cursor device");
+		return;
+	}
+	seat->operation = OP_MOVE;
+	seat->op_container = con;
+	seat->op_button = button;
+	cursor_set_image(seat->cursor, "grab", NULL);
+}
+
+void seat_begin_resize(struct sway_seat *seat, struct sway_container *con,
+		uint32_t button, enum wlr_edges edge) {
+	if (!seat->cursor) {
+		wlr_log(WLR_DEBUG, "Ignoring resize request due to no cursor device");
+		return;
+	}
+	struct wlr_keyboard *keyboard = wlr_seat_get_keyboard(seat->wlr_seat);
+	seat->operation = OP_RESIZE;
+	seat->op_container = con;
+	seat->op_resize_preserve_ratio = keyboard &&
+		(wlr_keyboard_get_modifiers(keyboard) & WLR_MODIFIER_SHIFT);
+	seat->op_resize_edge = edge == WLR_EDGE_NONE ?
+		RESIZE_EDGE_BOTTOM | RESIZE_EDGE_RIGHT : edge;
+	seat->op_button = button;
+	seat->op_ref_lx = seat->cursor->cursor->x;
+	seat->op_ref_ly = seat->cursor->cursor->y;
+	seat->op_ref_con_lx = con->x;
+	seat->op_ref_con_ly = con->y;
+	seat->op_ref_width = con->width;
+	seat->op_ref_height = con->height;
+
+	const char *image = edge == WLR_EDGE_NONE ?
+		"se-resize" : wlr_xcursor_get_resize_name(edge);
+	cursor_set_image(seat->cursor, image, NULL);
+}
+
+void seat_end_mouse_operation(struct sway_seat *seat) {
+	switch (seat->operation) {
+	case OP_MOVE:
+		{
+			// We "move" the container to its own location so it discovers its
+			// output again.
+			struct sway_container *con = seat->op_container;
+			container_floating_move_to(con, con->x, con->y);
+		}
+	case OP_RESIZE:
+		// Don't need to do anything here.
+		break;
+	case OP_NONE:
+		break;
+	}
+	seat->operation = OP_NONE;
+	seat->op_container = NULL;
+	cursor_set_image(seat->cursor, "left_ptr", NULL);
+}
+
+void seat_pointer_notify_button(struct sway_seat *seat, uint32_t time_msec,
+		uint32_t button, enum wlr_button_state state) {
+	seat->last_button = button;
+	seat->last_button_serial = wlr_seat_pointer_notify_button(seat->wlr_seat,
+			time_msec, button, state);
+}
diff --git a/sway/ipc-json.c b/sway/ipc-json.c
index c49ea47e..4c2bcc98 100644
--- a/sway/ipc-json.c
+++ b/sway/ipc-json.c
@@ -201,6 +201,15 @@ static void ipc_json_describe_view(struct sway_container *c, json_object *object
 	bool urgent = c->type == C_VIEW ?
 		view_is_urgent(c->sway_view) : container_has_urgent_child(c);
 	json_object_object_add(object, "urgent", json_object_new_boolean(urgent));
+
+	if (c->type == C_VIEW) {
+		json_object *marks = json_object_new_array();
+		list_t *view_marks = c->sway_view->marks;
+		for (int i = 0; i < view_marks->length; ++i) {
+			json_object_array_add(marks, json_object_new_string(view_marks->items[i]));
+		}
+		json_object_object_add(object, "marks", marks);
+	}
 }
 
 static void focus_inactive_children_iterator(struct sway_container *c, void *data) {
diff --git a/sway/ipc-server.c b/sway/ipc-server.c
index be703915..7d2d8969 100644
--- a/sway/ipc-server.c
+++ b/sway/ipc-server.c
@@ -3,12 +3,18 @@
 // Any value will hide SOCK_CLOEXEC on FreeBSD (__BSD_VISIBLE=0)
 #define _XOPEN_SOURCE 700
 #endif
+#ifdef __linux__
+#include <linux/input-event-codes.h>
+#elif __FreeBSD__
+#include <dev/evdev/input-event-codes.h>
+#endif
 #include <assert.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <json-c/json.h>
 #include <stdbool.h>
 #include <stdint.h>
+#include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <sys/socket.h>
@@ -28,6 +34,7 @@
 #include "sway/tree/view.h"
 #include "list.h"
 #include "log.h"
+#include "util.h"
 
 static int ipc_socket = -1;
 static struct wl_event_source *ipc_event_source =  NULL;
@@ -291,13 +298,11 @@ void ipc_event_workspace(struct sway_container *old,
 	wlr_log(WLR_DEBUG, "Sending workspace::%s event", change);
 	json_object *obj = json_object_new_object();
 	json_object_object_add(obj, "change", json_object_new_string(change));
-	if (strcmp("focus", change) == 0) {
-		if (old) {
-			json_object_object_add(obj, "old",
-					ipc_json_describe_container_recursive(old));
-		} else {
-			json_object_object_add(obj, "old", NULL);
-		}
+	if (old) {
+		json_object_object_add(obj, "old",
+				ipc_json_describe_container_recursive(old));
+	} else {
+		json_object_object_add(obj, "old", NULL);
 	}
 
 	if (new) {
@@ -353,6 +358,104 @@ void ipc_event_mode(const char *mode, bool pango) {
 	json_object_put(obj);
 }
 
+void ipc_event_shutdown(const char *reason) {
+	if (!ipc_has_event_listeners(IPC_EVENT_SHUTDOWN)) {
+		return;
+	}
+	wlr_log(WLR_DEBUG, "Sending shutdown::%s event", reason);
+
+	json_object *json = json_object_new_object();
+	json_object_object_add(json, "change", json_object_new_string(reason));
+
+	const char *json_string = json_object_to_json_string(json);
+	ipc_send_event(json_string, IPC_EVENT_SHUTDOWN);
+	json_object_put(json);
+}
+
+void ipc_event_binding(struct sway_binding *binding) {
+	if (!ipc_has_event_listeners(IPC_EVENT_BINDING)) {
+		return;
+	}
+	wlr_log(WLR_DEBUG, "Sending binding event");
+
+	json_object *json_binding = json_object_new_object();
+	json_object_object_add(json_binding, "command", json_object_new_string(binding->command));
+
+	const char *names[10];
+	int len = get_modifier_names(names, binding->modifiers);
+	json_object *modifiers = json_object_new_array();
+	for (int i = 0; i < len; ++i) {
+		json_object_array_add(modifiers, json_object_new_string(names[i]));
+	}
+	json_object_object_add(json_binding, "event_state_mask", modifiers);
+
+	json_object *input_codes = json_object_new_array();
+	int input_code = 0;
+	json_object *symbols = json_object_new_array();
+	json_object *symbol = NULL;
+
+	if (binding->type == BINDING_KEYCODE) { // bindcode: populate input_codes
+		uint32_t keycode;
+		for (int i = 0; i < binding->keys->length; ++i) {
+			keycode = *(uint32_t *)binding->keys->items[i];
+			json_object_array_add(input_codes, json_object_new_int(keycode));
+			if (i == 0) {
+				input_code = keycode;
+			}
+		}
+	} else { // bindsym/mouse: populate symbols
+		uint32_t keysym;
+		char buffer[64];
+		for (int i = 0; i < binding->keys->length; ++i) {
+			keysym = *(uint32_t *)binding->keys->items[i];
+			if (keysym >= BTN_LEFT && keysym <= BTN_LEFT + 8) {
+				snprintf(buffer, 64, "button%u", keysym - BTN_LEFT + 1);
+			} else if (xkb_keysym_get_name(keysym, buffer, 64) < 0) {
+				continue;
+			}
+
+			json_object *str = json_object_new_string(buffer);
+			if (i == 0) {
+				// str is owned by both symbol and symbols. Make sure
+				// to bump the ref count.
+				json_object_array_add(symbols, json_object_get(str));
+				symbol = str;
+			} else {
+				json_object_array_add(symbols, str);
+			}
+		}
+	}
+
+	json_object_object_add(json_binding, "input_codes", input_codes);
+	json_object_object_add(json_binding, "input_code", json_object_new_int(input_code));
+	json_object_object_add(json_binding, "symbols", symbols);
+	json_object_object_add(json_binding, "symbol", symbol);
+	json_object_object_add(json_binding, "input_type", binding->type == BINDING_MOUSE ?
+			json_object_new_string("mouse") : json_object_new_string("keyboard"));
+
+	json_object *json = json_object_new_object();
+	json_object_object_add(json, "change", json_object_new_string("run"));
+	json_object_object_add(json, "binding", json_binding);
+	const char *json_string = json_object_to_json_string(json);
+	ipc_send_event(json_string, IPC_EVENT_BINDING);
+	json_object_put(json);
+}
+
+static void ipc_event_tick(const char *payload) {
+	if (!ipc_has_event_listeners(IPC_EVENT_TICK)) {
+		return;
+	}
+	wlr_log(WLR_DEBUG, "Sending tick event");
+
+	json_object *json = json_object_new_object();
+	json_object_object_add(json, "first", json_object_new_boolean(false));
+	json_object_object_add(json, "payload", json_object_new_string(payload));
+
+	const char *json_string = json_object_to_json_string(json);
+	ipc_send_event(json_string, IPC_EVENT_TICK);
+	json_object_put(json);
+}
+
 int ipc_client_handle_writable(int client_fd, uint32_t mask, void *data) {
 	struct ipc_client *client = data;
 
@@ -494,6 +597,13 @@ void ipc_client_handle_command(struct ipc_client *client) {
 		goto exit_cleanup;
 	}
 
+	case IPC_SEND_TICK:
+	{
+		ipc_event_tick(buf);
+		ipc_send_reply(client, "{\"success\": true}", 17);
+		goto exit_cleanup;
+	}
+
 	case IPC_GET_OUTPUTS:
 	{
 		json_object *outputs = json_object_new_array();
@@ -540,6 +650,7 @@ void ipc_client_handle_command(struct ipc_client *client) {
 			goto exit_cleanup;
 		}
 
+		bool is_tick = false;
 		// parse requested event types
 		for (size_t i = 0; i < json_object_array_length(request); i++) {
 			const char *event_type = json_object_get_string(json_object_array_get_idx(request, i));
@@ -549,12 +660,15 @@ void ipc_client_handle_command(struct ipc_client *client) {
 				client->subscribed_events |= event_mask(IPC_EVENT_BARCONFIG_UPDATE);
 			} else if (strcmp(event_type, "mode") == 0) {
 				client->subscribed_events |= event_mask(IPC_EVENT_MODE);
+			} else if (strcmp(event_type, "shutdown") == 0) {
+				client->subscribed_events |= event_mask(IPC_EVENT_SHUTDOWN);
 			} else if (strcmp(event_type, "window") == 0) {
 				client->subscribed_events |= event_mask(IPC_EVENT_WINDOW);
-			} else if (strcmp(event_type, "modifier") == 0) {
-				client->subscribed_events |= event_mask(IPC_EVENT_MODIFIER);
 			} else if (strcmp(event_type, "binding") == 0) {
 				client->subscribed_events |= event_mask(IPC_EVENT_BINDING);
+			} else if (strcmp(event_type, "tick") == 0) {
+				client->subscribed_events |= event_mask(IPC_EVENT_TICK);
+				is_tick = true;
 			} else {
 				client_valid =
 					ipc_send_reply(client, "{\"success\": false}", 18);
@@ -566,6 +680,10 @@ void ipc_client_handle_command(struct ipc_client *client) {
 
 		json_object_put(request);
 		client_valid = ipc_send_reply(client, "{\"success\": true}", 17);
+		if (is_tick) {
+			client->current_command = IPC_EVENT_TICK;
+			ipc_send_reply(client, "{\"first\": true, \"payload\": \"\"}", 30);
+		}
 		goto exit_cleanup;
 	}
 
diff --git a/sway/main.c b/sway/main.c
index a20f1dac..477ffa5a 100644
--- a/sway/main.c
+++ b/sway/main.c
@@ -36,6 +36,7 @@ struct sway_server server;
 void sway_terminate(int exit_code) {
 	terminate_request = true;
 	exit_value = exit_code;
+	ipc_event_shutdown("exit");
 	wl_display_terminate(server.wl_display);
 }
 
diff --git a/sway/meson.build b/sway/meson.build
index 09bc40b8..a9503c3b 100644
--- a/sway/meson.build
+++ b/sway/meson.build
@@ -7,6 +7,7 @@ sway_sources = files(
 	'debug-tree.c',
 	'ipc-json.c',
 	'ipc-server.c',
+	'scratchpad.c',
 	'security.c',
 
 	'desktop/desktop.c',
@@ -17,7 +18,6 @@ sway_sources = files(
 	'desktop/transaction.c',
 	'desktop/xdg_shell_v6.c',
 	'desktop/xdg_shell.c',
-	'desktop/xwayland.c',
 
 	'input/input-manager.c',
 	'input/seat.c',
@@ -42,6 +42,7 @@ sway_sources = files(
 	'commands/exec_always.c',
 	'commands/floating.c',
 	'commands/floating_minmax_size.c',
+	'commands/floating_modifier.c',
 	'commands/focus.c',
 	'commands/focus_follows_mouse.c',
 	'commands/focus_wrapping.c',
@@ -66,6 +67,7 @@ sway_sources = files(
 	'commands/reload.c',
 	'commands/rename.c',
 	'commands/resize.c',
+	'commands/scratchpad.c',
 	'commands/seat.c',
 	'commands/seat/attach.c',
 	'commands/seat/cursor.c',
@@ -126,8 +128,10 @@ sway_sources = files(
 	'commands/input/scroll_method.c',
 	'commands/input/tap.c',
 	'commands/input/tap_button_map.c',
+	'commands/input/xkb_capslock.c',
 	'commands/input/xkb_layout.c',
 	'commands/input/xkb_model.c',
+	'commands/input/xkb_numlock.c',
 	'commands/input/xkb_options.c',
 	'commands/input/xkb_rules.c',
 	'commands/input/xkb_variant.c',
@@ -162,10 +166,14 @@ sway_deps = [
 	server_protos,
 	wayland_server,
 	wlroots,
-	xcb,
 	xkbcommon,
 ]
 
+if get_option('enable-xwayland')
+	sway_sources += 'desktop/xwayland.c'
+	sway_deps += xcb
+endif
+
 executable(
 	'sway',
 	sway_sources,
diff --git a/sway/scratchpad.c b/sway/scratchpad.c
new file mode 100644
index 00000000..b7d6fd99
--- /dev/null
+++ b/sway/scratchpad.c
@@ -0,0 +1,181 @@
+#define _XOPEN_SOURCE 700
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdbool.h>
+#include "sway/scratchpad.h"
+#include "sway/input/seat.h"
+#include "sway/tree/arrange.h"
+#include "sway/tree/container.h"
+#include "sway/tree/view.h"
+#include "sway/tree/workspace.h"
+#include "list.h"
+#include "log.h"
+
+void scratchpad_add_container(struct sway_container *con) {
+	if (!sway_assert(!con->scratchpad, "Container is already in scratchpad")) {
+		return;
+	}
+	con->scratchpad = true;
+	list_add(root_container.sway_root->scratchpad, con);
+
+	struct sway_container *parent = con->parent;
+	container_set_floating(con, true);
+	container_remove_child(con);
+	arrange_windows(parent);
+
+	struct sway_seat *seat = input_manager_current_seat(input_manager);
+	seat_set_focus(seat, seat_get_focus_inactive(seat, parent));
+}
+
+void scratchpad_remove_container(struct sway_container *con) {
+	if (!sway_assert(con->scratchpad, "Container is not in scratchpad")) {
+		return;
+	}
+	con->scratchpad = false;
+	for (int i = 0; i < root_container.sway_root->scratchpad->length; ++i) {
+		if (root_container.sway_root->scratchpad->items[i] == con) {
+			list_del(root_container.sway_root->scratchpad, i);
+			break;
+		}
+	}
+}
+
+/**
+ * Show a single scratchpad container.
+ * The container might be visible on another workspace already.
+ */
+static void scratchpad_show(struct sway_container *con) {
+	struct sway_seat *seat = input_manager_current_seat(input_manager);
+	struct sway_container *ws = seat_get_focus(seat);
+	if (ws->type != C_WORKSPACE) {
+		ws = container_parent(ws, C_WORKSPACE);
+	}
+
+    // If the current con or any of its parents are in fullscreen mode, we
+    // first need to disable it before showing the scratchpad con.
+	if (ws->sway_workspace->fullscreen) {
+		container_set_fullscreen(ws->sway_workspace->fullscreen, false);
+	}
+
+	// Show the container
+	if (con->parent) {
+		container_remove_child(con);
+	}
+	container_add_child(ws->sway_workspace->floating, con);
+
+	// Make sure the container's center point overlaps this workspace
+	double center_lx = con->x + con->width / 2;
+	double center_ly = con->y + con->height / 2;
+
+	struct wlr_box workspace_box;
+	container_get_box(ws, &workspace_box);
+	if (!wlr_box_contains_point(&workspace_box, center_lx, center_ly)) {
+		// Maybe resize it
+		if (con->width > ws->width || con->height > ws->height) {
+			container_init_floating(con);
+		}
+
+		// Center it
+		double new_lx = ws->x + (ws->width - con->width) / 2;
+		double new_ly = ws->y + (ws->height - con->height) / 2;
+		container_floating_move_to(con, new_lx, new_ly);
+	}
+
+	arrange_windows(ws);
+	seat_set_focus(seat, seat_get_focus_inactive(seat, con));
+
+	container_set_dirty(con->parent);
+}
+
+/**
+ * Hide a single scratchpad container.
+ * The container might not be the focused container (eg. when using criteria).
+ */
+static void scratchpad_hide(struct sway_container *con) {
+	struct sway_seat *seat = input_manager_current_seat(input_manager);
+	struct sway_container *focus = seat_get_focus(seat);
+	struct sway_container *ws = container_parent(con, C_WORKSPACE);
+
+	container_remove_child(con);
+	arrange_windows(ws);
+	if (con == focus) {
+		seat_set_focus(seat, seat_get_focus_inactive(seat, ws));
+	}
+	list_move_to_end(root_container.sway_root->scratchpad, con);
+}
+
+void scratchpad_toggle_auto(void) {
+	struct sway_seat *seat = input_manager_current_seat(input_manager);
+	struct sway_container *focus = seat_get_focus(seat);
+	struct sway_container *ws = focus->type == C_WORKSPACE ?
+		focus : container_parent(focus, C_WORKSPACE);
+
+	// If the focus is in a floating split container,
+	// operate on the split container instead of the child.
+	if (container_is_floating_or_child(focus)) {
+		while (focus->parent->layout != L_FLOATING) {
+			focus = focus->parent;
+		}
+	}
+
+
+    // Check if the currently focused window is a scratchpad window and should
+    // be hidden again.
+	if (focus->scratchpad) {
+		wlr_log(WLR_DEBUG, "Focus is a scratchpad window - hiding %s",
+				focus->name);
+		scratchpad_hide(focus);
+		return;
+	}
+
+    // Check if there is an unfocused scratchpad window on the current workspace
+    // and focus it.
+	for (int i = 0; i < ws->sway_workspace->floating->children->length; ++i) {
+		struct sway_container *floater =
+			ws->sway_workspace->floating->children->items[i];
+		if (floater->scratchpad && focus != floater) {
+			wlr_log(WLR_DEBUG,
+					"Focusing other scratchpad window (%s) in this workspace",
+					floater->name);
+			scratchpad_show(floater);
+			return;
+		}
+	}
+
+    // Check if there is a visible scratchpad window on another workspace.
+    // In this case we move it to the current workspace.
+	for (int i = 0; i < root_container.sway_root->scratchpad->length; ++i) {
+		struct sway_container *con =
+			root_container.sway_root->scratchpad->items[i];
+		if (con->parent) {
+			wlr_log(WLR_DEBUG,
+					"Moving a visible scratchpad window (%s) to this workspace",
+					con->name);
+			scratchpad_show(con);
+			return;
+		}
+	}
+
+	// Take the container at the bottom of the scratchpad list
+	if (!sway_assert(root_container.sway_root->scratchpad->length,
+				"Scratchpad is empty")) {
+		return;
+	}
+	struct sway_container *con = root_container.sway_root->scratchpad->items[0];
+	wlr_log(WLR_DEBUG, "Showing %s from list", con->name);
+	scratchpad_show(con);
+}
+
+void scratchpad_toggle_container(struct sway_container *con) {
+	if (!sway_assert(con->scratchpad, "Container isn't in the scratchpad")) {
+		return;
+	}
+
+    // Check if it matches a currently visible scratchpad window and hide it.
+	if (con->parent) {
+		scratchpad_hide(con);
+		return;
+	}
+
+	scratchpad_show(con);
+}
diff --git a/sway/server.c b/sway/server.c
index 91ae7c97..e8755360 100644
--- a/sway/server.c
+++ b/sway/server.c
@@ -26,7 +26,10 @@
 #include "sway/input/input-manager.h"
 #include "sway/server.h"
 #include "sway/tree/layout.h"
+#include "config.h"
+#ifdef HAVE_XWAYLAND
 #include "sway/xwayland.h"
+#endif
 
 bool server_privileged_prepare(struct sway_server *server) {
 	wlr_log(WLR_DEBUG, "Preparing Wayland server initialization");
@@ -83,6 +86,7 @@ bool server_init(struct sway_server *server) {
 	server->xdg_shell_surface.notify = handle_xdg_shell_surface;
 
 	// TODO make xwayland optional
+#ifdef HAVE_XWAYLAND
 	server->xwayland.wlr_xwayland =
 		wlr_xwayland_create(server->wl_display, server->compositor, true);
 	wl_signal_add(&server->xwayland.wlr_xwayland->events.new_surface,
@@ -103,6 +107,7 @@ bool server_init(struct sway_server *server) {
 			image->width * 4, image->width, image->height, image->hotspot_x,
 			image->hotspot_y);
 	}
+#endif
 
 	// TODO: Integration with sway borders
 	struct wlr_server_decoration_manager *deco_manager =
diff --git a/sway/sway-input.5.scd b/sway/sway-input.5.scd
index b6391431..707c36af 100644
--- a/sway/sway-input.5.scd
+++ b/sway/sway-input.5.scd
@@ -33,6 +33,14 @@ For more information on these xkb configuration options, see
 *input* <identifier> xkb\_variant <variant>
 	Sets the variant of the keyboard like _dvorak_ or _colemak_.
 
+The following commands may only be used in the configuration file.
+
+*input* <identifier> xkb\_capslock enabled|disabled
+	Initially enables or disables CapsLock, the default is disabled.
+
+*input* <identifier> xkb\_numlock enabled|disabled
+	Initially enables or disables NumLock, the default is disabled.
+
 ## MAPPING CONFIGURATION
 
 *input* <identifier> map\_to\_output <identifier>
@@ -100,7 +108,7 @@ For more information on these xkb configuration options, see
 *input* <identifier> tap enabled|disabled
 	Enables or disables tap for specified input device.
 
-*input* <identifier> tap_button_map lrm|lmr
+*input* <identifier> tap\_button\_map lrm|lmr
 	Specifies which button mapping to use for tapping. _lrm_ treats 1 finger as
 	left click, 2 fingers as right click, and 3 fingers as middle click. _lmr_
 	treats 1 finger as left click, 2 fingers as middle click, and 3 fingers as
diff --git a/sway/sway.1.scd b/sway/sway.1.scd
index 5b770cce..0c2ee974 100644
--- a/sway/sway.1.scd
+++ b/sway/sway.1.scd
@@ -92,4 +92,4 @@ source contributors. For more information about sway development, see
 
 # SEE ALSO
 
-*sway*(5) *swaymsg*(1) *swaygrab*(1) *sway-input*(5) *sway-bar*(5)
+*sway*(5) *swaymsg*(1) *sway-input*(5) *sway-bar*(5)
diff --git a/sway/tree/arrange.c b/sway/tree/arrange.c
index 533cf71c..5452b13c 100644
--- a/sway/tree/arrange.c
+++ b/sway/tree/arrange.c
@@ -220,8 +220,22 @@ static void arrange_workspace(struct sway_container *workspace) {
 	container_set_dirty(workspace);
 	wlr_log(WLR_DEBUG, "Arranging workspace '%s' at %f, %f", workspace->name,
 			workspace->x, workspace->y);
-	arrange_floating(workspace->sway_workspace->floating);
-	arrange_children_of(workspace);
+	if (workspace->sway_workspace->fullscreen) {
+		struct sway_container *fs = workspace->sway_workspace->fullscreen;
+		fs->x = workspace->parent->x;
+		fs->y = workspace->parent->y;
+		fs->width = workspace->parent->width;
+		fs->height = workspace->parent->height;
+		if (fs->type == C_VIEW) {
+			view_autoconfigure(fs->sway_view);
+		} else {
+			arrange_children_of(fs);
+		}
+		container_set_dirty(fs);
+	} else {
+		arrange_floating(workspace->sway_workspace->floating);
+		arrange_children_of(workspace);
+	}
 }
 
 static void arrange_output(struct sway_container *output) {
diff --git a/sway/tree/container.c b/sway/tree/container.c
index 4dbfbb29..46c54e2d 100644
--- a/sway/tree/container.c
+++ b/sway/tree/container.c
@@ -17,6 +17,7 @@
 #include "sway/input/seat.h"
 #include "sway/ipc-server.h"
 #include "sway/output.h"
+#include "sway/scratchpad.h"
 #include "sway/server.h"
 #include "sway/tree/arrange.h"
 #include "sway/tree/layout.h"
@@ -61,13 +62,17 @@ void container_create_notify(struct sway_container *container) {
 	// TODO send ipc event type based on the container type
 	wl_signal_emit(&root_container.sway_root->events.new_container, container);
 
-	if (container->type == C_VIEW || container->type == C_CONTAINER) {
+	if (container->type == C_VIEW) {
 		ipc_event_window(container, "new");
+	} else if (container->type == C_WORKSPACE) {
+		ipc_event_workspace(NULL, container, "init");
 	}
 }
 
-static void container_update_textures_recursive(struct sway_container *con) {
-	container_update_title_textures(con);
+void container_update_textures_recursive(struct sway_container *con) {
+	if (con->type == C_CONTAINER || con->type == C_VIEW) {
+		container_update_title_textures(con);
+	}
 
 	if (con->type == C_VIEW) {
 		view_update_marks_textures(con->sway_view);
@@ -76,6 +81,10 @@ static void container_update_textures_recursive(struct sway_container *con) {
 			struct sway_container *child = con->children->items[i];
 			container_update_textures_recursive(child);
 		}
+
+		if (con->type == C_WORKSPACE) {
+			container_update_textures_recursive(con->sway_workspace->floating);
+		}
 	}
 }
 
@@ -139,8 +148,6 @@ struct sway_container *container_create(enum sway_container_type type) {
 static void container_workspace_free(struct sway_workspace *ws) {
 	list_foreach(ws->output_priority, free);
 	list_free(ws->output_priority);
-	ws->floating->destroying = true;
-	container_free(ws->floating);
 	free(ws);
 }
 
@@ -193,6 +200,9 @@ void container_free(struct sway_container *cont) {
 	free(cont);
 }
 
+static struct sway_container *container_destroy_noreaping(
+		struct sway_container *con);
+
 static struct sway_container *container_workspace_destroy(
 		struct sway_container *workspace) {
 	if (!sway_assert(workspace, "cannot destroy null workspace")) {
@@ -237,6 +247,8 @@ static struct sway_container *container_workspace_destroy(
 		}
 	}
 
+	container_destroy_noreaping(workspace->sway_workspace->floating);
+
 	return output;
 }
 
@@ -271,7 +283,7 @@ static struct sway_container *container_output_destroy(
 				container_remove_child(workspace);
 				if (!workspace_is_empty(workspace)) {
 					container_add_child(new_output, workspace);
-					ipc_event_workspace(workspace, NULL, "move");
+					ipc_event_workspace(NULL, workspace, "move");
 				} else {
 					container_destroy(workspace);
 				}
@@ -309,7 +321,13 @@ static struct sway_container *container_destroy_noreaping(
 	}
 
 	wl_signal_emit(&con->events.destroy, con);
-	ipc_event_window(con, "close");
+
+	// emit IPC event
+	if (con->type == C_VIEW) {
+		ipc_event_window(con, "close");
+	} else if (con->type == C_WORKSPACE) {
+		ipc_event_workspace(NULL, con, "empty");
+	}
 
 	// The below functions move their children to somewhere else.
 	if (con->type == C_OUTPUT) {
@@ -323,9 +341,15 @@ static struct sway_container *container_destroy_noreaping(
 		}
 	}
 
+	container_end_mouse_operation(con);
+
 	con->destroying = true;
 	container_set_dirty(con);
 
+	if (con->scratchpad) {
+		scratchpad_remove_container(con);
+	}
+
 	if (!con->parent) {
 		return NULL;
 	}
@@ -398,6 +422,10 @@ struct sway_container *container_flatten(struct sway_container *container) {
  * This function just wraps container_destroy_noreaping(), then does reaping.
  */
 struct sway_container *container_destroy(struct sway_container *con) {
+	if (con->is_fullscreen) {
+		struct sway_container *ws = container_parent(con, C_WORKSPACE);
+		ws->sway_workspace->fullscreen = NULL;
+	}
 	struct sway_container *parent = container_destroy_noreaping(con);
 
 	if (!parent) {
@@ -507,7 +535,7 @@ struct sway_container *container_parent(struct sway_container *container,
 	return container;
 }
 
-static struct sway_container *container_at_view(struct sway_container *swayc,
+struct sway_container *container_at_view(struct sway_container *swayc,
 		double lx, double ly,
 		struct wlr_surface **surface, double *sx, double *sy) {
 	if (!sway_assert(swayc->type == C_VIEW, "Expected a view")) {
@@ -520,10 +548,12 @@ static struct sway_container *container_at_view(struct sway_container *swayc,
 	double _sx, _sy;
 	struct wlr_surface *_surface = NULL;
 	switch (sview->type) {
+#ifdef HAVE_XWAYLAND
 	case SWAY_VIEW_XWAYLAND:
 		_surface = wlr_surface_surface_at(sview->surface,
 				view_sx, view_sy, &_sx, &_sy);
 		break;
+#endif
 	case SWAY_VIEW_XDG_SHELL_V6:
 		_surface = wlr_xdg_surface_v6_surface_at(
 				sview->wlr_xdg_surface_v6,
@@ -539,10 +569,15 @@ static struct sway_container *container_at_view(struct sway_container *swayc,
 		*sx = _sx;
 		*sy = _sy;
 		*surface = _surface;
+		return swayc;
 	}
-	return swayc;
+	return NULL;
 }
 
+static struct sway_container *tiling_container_at(
+		struct sway_container *con, double lx, double ly,
+		struct wlr_surface **surface, double *sx, double *sy);
+
 /**
  * container_at for a container with layout L_TABBED.
  */
@@ -569,7 +604,7 @@ static struct sway_container *container_at_tabbed(struct sway_container *parent,
 	// Surfaces
 	struct sway_container *current = seat_get_active_child(seat, parent);
 
-	return container_at(current, lx, ly, surface, sx, sy);
+	return tiling_container_at(current, lx, ly, surface, sx, sy);
 }
 
 /**
@@ -594,7 +629,7 @@ static struct sway_container *container_at_stacked(
 	// Surfaces
 	struct sway_container *current = seat_get_active_child(seat, parent);
 
-	return container_at(current, lx, ly, surface, sx, sy);
+	return tiling_container_at(current, lx, ly, surface, sx, sy);
 }
 
 /**
@@ -612,45 +647,13 @@ static struct sway_container *container_at_linear(struct sway_container *parent,
 			.height = child->height,
 		};
 		if (wlr_box_contains_point(&box, lx, ly)) {
-			return container_at(child, lx, ly, surface, sx, sy);
+			return tiling_container_at(child, lx, ly, surface, sx, sy);
 		}
 	}
 	return NULL;
 }
 
-struct sway_container *container_at(struct sway_container *parent,
-		double lx, double ly,
-		struct wlr_surface **surface, double *sx, double *sy) {
-	if (!sway_assert(parent->type >= C_WORKSPACE,
-				"Expected workspace or deeper")) {
-		return NULL;
-	}
-	if (parent->type == C_VIEW) {
-		return container_at_view(parent, lx, ly, surface, sx, sy);
-	}
-	if (!parent->children->length) {
-		return NULL;
-	}
-
-	switch (parent->layout) {
-	case L_HORIZ:
-	case L_VERT:
-		return container_at_linear(parent, lx, ly, surface, sx, sy);
-	case L_TABBED:
-		return container_at_tabbed(parent, lx, ly, surface, sx, sy);
-	case L_STACKED:
-		return container_at_stacked(parent, lx, ly, surface, sx, sy);
-	case L_FLOATING:
-		sway_assert(false, "Didn't expect to see floating here");
-		return NULL;
-	case L_NONE:
-		return NULL;
-	}
-
-	return NULL;
-}
-
-struct sway_container *floating_container_at(double lx, double ly,
+static struct sway_container *floating_container_at(double lx, double ly,
 		struct wlr_surface **surface, double *sx, double *sy) {
 	for (int i = 0; i < root_container.children->length; ++i) {
 		struct sway_container *output = root_container.children->items[i];
@@ -672,7 +675,8 @@ struct sway_container *floating_container_at(double lx, double ly,
 					.height = floater->height,
 				};
 				if (wlr_box_contains_point(&box, lx, ly)) {
-					return container_at(floater, lx, ly, surface, sx, sy);
+					return tiling_container_at(floater, lx, ly,
+							surface, sx, sy);
 				}
 			}
 		}
@@ -680,6 +684,90 @@ struct sway_container *floating_container_at(double lx, double ly,
 	return NULL;
 }
 
+static struct sway_container *tiling_container_at(
+		struct sway_container *con, double lx, double ly,
+		struct wlr_surface **surface, double *sx, double *sy) {
+	if (con->type == C_VIEW) {
+		return container_at_view(con, lx, ly, surface, sx, sy);
+	}
+	if (!con->children->length) {
+		return NULL;
+	}
+
+	switch (con->layout) {
+	case L_HORIZ:
+	case L_VERT:
+		return container_at_linear(con, lx, ly, surface, sx, sy);
+	case L_TABBED:
+		return container_at_tabbed(con, lx, ly, surface, sx, sy);
+	case L_STACKED:
+		return container_at_stacked(con, lx, ly, surface, sx, sy);
+	case L_FLOATING:
+		sway_assert(false, "Didn't expect to see floating here");
+		return NULL;
+	case L_NONE:
+		return NULL;
+	}
+	return NULL;
+}
+
+static bool surface_is_popup(struct wlr_surface *surface) {
+	if (wlr_surface_is_xdg_surface(surface)) {
+		struct wlr_xdg_surface *xdg_surface =
+			wlr_xdg_surface_from_wlr_surface(surface);
+		while (xdg_surface) {
+			if (xdg_surface->role == WLR_XDG_SURFACE_ROLE_POPUP) {
+				return true;
+			}
+			xdg_surface = xdg_surface->toplevel->parent;
+		}
+		return false;
+	}
+
+	if (wlr_surface_is_xdg_surface_v6(surface)) {
+		struct wlr_xdg_surface_v6 *xdg_surface_v6 =
+			wlr_xdg_surface_v6_from_wlr_surface(surface);
+		while (xdg_surface_v6) {
+			if (xdg_surface_v6->role == WLR_XDG_SURFACE_V6_ROLE_POPUP) {
+				return true;
+			}
+			xdg_surface_v6 = xdg_surface_v6->toplevel->parent;
+		}
+		return false;
+	}
+
+	return false;
+}
+
+struct sway_container *container_at(struct sway_container *workspace,
+		double lx, double ly,
+		struct wlr_surface **surface, double *sx, double *sy) {
+	if (!sway_assert(workspace->type == C_WORKSPACE, "Expected a workspace")) {
+		return NULL;
+	}
+	struct sway_container *c;
+	// Focused view's popups
+	struct sway_seat *seat = input_manager_current_seat(input_manager);
+	struct sway_container *focus =
+		seat_get_focus_inactive(seat, &root_container);
+	if (focus && focus->type == C_VIEW) {
+		container_at_view(focus, lx, ly, surface, sx, sy);
+		if (*surface && surface_is_popup(*surface)) {
+			return focus;
+		}
+		*surface = NULL;
+	}
+	// Floating
+	if ((c = floating_container_at(lx, ly, surface, sx, sy))) {
+		return c;
+	}
+	// Tiling
+	if ((c = tiling_container_at(workspace, lx, ly, surface, sx, sy))) {
+		return c;
+	}
+	return NULL;
+}
+
 void container_for_each_descendant_dfs(struct sway_container *container,
 		void (*f)(struct sway_container *container, void *data),
 		void *data) {
@@ -934,36 +1022,104 @@ size_t container_titlebar_height() {
 	return config->font_height + TITLEBAR_V_PADDING * 2;
 }
 
+void container_init_floating(struct sway_container *con) {
+	if (!sway_assert(con->type == C_VIEW || con->type == C_CONTAINER,
+			"Expected a view or container")) {
+		return;
+	}
+	struct sway_container *ws = container_parent(con, C_WORKSPACE);
+	int min_width, min_height;
+	int max_width, max_height;
+
+	if (config->floating_minimum_width == -1) { // no minimum
+		min_width = 0;
+	} else if (config->floating_minimum_width == 0) { // automatic
+		min_width = 75;
+	} else {
+		min_width = config->floating_minimum_width;
+	}
+
+	if (config->floating_minimum_height == -1) { // no minimum
+		min_height = 0;
+	} else if (config->floating_minimum_height == 0) { // automatic
+		min_height = 50;
+	} else {
+		min_height = config->floating_minimum_height;
+	}
+
+	if (config->floating_maximum_width == -1) { // no maximum
+		max_width = INT_MAX;
+	} else if (config->floating_maximum_width == 0) { // automatic
+		max_width = ws->width * 0.6666;
+	} else {
+		max_width = config->floating_maximum_width;
+	}
+
+	if (config->floating_maximum_height == -1) { // no maximum
+		max_height = INT_MAX;
+	} else if (config->floating_maximum_height == 0) { // automatic
+		max_height = ws->height * 0.6666;
+	} else {
+		max_height = config->floating_maximum_height;
+	}
+
+	if (con->type == C_CONTAINER) {
+		con->width = max_width;
+		con->height = max_height;
+		con->x = ws->x + (ws->width - con->width) / 2;
+		con->y = ws->y + (ws->height - con->height) / 2;
+	} else {
+		struct sway_view *view = con->sway_view;
+		view->width = fmax(min_width, fmin(view->natural_width, max_width));
+		view->height = fmax(min_height, fmin(view->natural_height, max_height));
+		view->x = ws->x + (ws->width - view->width) / 2;
+		view->y = ws->y + (ws->height - view->height) / 2;
+
+		// If the view's border is B_NONE then these properties are ignored.
+		view->border_top = view->border_bottom = true;
+		view->border_left = view->border_right = true;
+
+		container_set_geometry_from_floating_view(view->swayc);
+	}
+}
+
 void container_set_floating(struct sway_container *container, bool enable) {
 	if (container_is_floating(container) == enable) {
 		return;
 	}
 
-	struct sway_container *workspace = container_parent(container, C_WORKSPACE);
 	struct sway_seat *seat = input_manager_current_seat(input_manager);
+	struct sway_container *workspace = container_parent(container, C_WORKSPACE);
 
 	if (enable) {
 		container_remove_child(container);
 		container_add_child(workspace->sway_workspace->floating, container);
+		container_init_floating(container);
 		if (container->type == C_VIEW) {
-			view_init_floating(container->sway_view);
 			view_set_tiled(container->sway_view, false);
 		}
-		seat_set_focus(seat, seat_get_focus_inactive(seat, container));
-		container_reap_empty_recursive(workspace);
 	} else {
 		// Returning to tiled
+		if (container->scratchpad) {
+			scratchpad_remove_container(container);
+		}
 		container_remove_child(container);
-		container_add_child(workspace, container);
+		struct sway_container *reference =
+			seat_get_focus_inactive_tiling(seat, workspace);
+		if (reference->type == C_VIEW) {
+			reference = reference->parent;
+		}
+		container_add_child(reference, container);
 		container->width = container->parent->width;
 		container->height = container->parent->height;
 		if (container->type == C_VIEW) {
 			view_set_tiled(container->sway_view, true);
 		}
 		container->is_sticky = false;
-		container_reap_empty_recursive(workspace->sway_workspace->floating);
 	}
 
+	container_end_mouse_operation(container);
+
 	ipc_event_window(container, "floating");
 }
 
@@ -1009,7 +1165,7 @@ void container_get_box(struct sway_container *container, struct wlr_box *box) {
 /**
  * Translate the container's position as well as all children.
  */
-static void container_floating_translate(struct sway_container *con,
+void container_floating_translate(struct sway_container *con,
 		double x_amount, double y_amount) {
 	con->x += x_amount;
 	con->y += y_amount;
@@ -1105,3 +1261,110 @@ static bool find_urgent_iterator(struct sway_container *con,
 bool container_has_urgent_child(struct sway_container *container) {
 	return container_find(container, find_urgent_iterator, NULL);
 }
+
+void container_end_mouse_operation(struct sway_container *container) {
+	struct sway_seat *seat;
+	wl_list_for_each(seat, &input_manager->seats, link) {
+		if (seat->op_container == container) {
+			seat_end_mouse_operation(seat);
+		}
+	}
+}
+
+static void set_fullscreen_iterator(struct sway_container *con, void *data) {
+	if (con->type != C_VIEW) {
+		return;
+	}
+	if (con->sway_view->impl->set_fullscreen) {
+		bool *enable = data;
+		con->sway_view->impl->set_fullscreen(con->sway_view, *enable);
+	}
+}
+
+void container_set_fullscreen(struct sway_container *container, bool enable) {
+	if (container->is_fullscreen == enable) {
+		return;
+	}
+
+	struct sway_container *workspace = container_parent(container, C_WORKSPACE);
+	if (enable && workspace->sway_workspace->fullscreen) {
+		container_set_fullscreen(workspace->sway_workspace->fullscreen, false);
+	}
+
+	container_for_each_descendant_dfs(container,
+			set_fullscreen_iterator, &enable);
+
+	container->is_fullscreen = enable;
+
+	if (enable) {
+		workspace->sway_workspace->fullscreen = container;
+		container->saved_x = container->x;
+		container->saved_y = container->y;
+		container->saved_width = container->width;
+		container->saved_height = container->height;
+
+		struct sway_seat *seat;
+		struct sway_container *focus, *focus_ws;
+		wl_list_for_each(seat, &input_manager->seats, link) {
+			focus = seat_get_focus(seat);
+			if (focus) {
+				focus_ws = focus;
+				if (focus_ws->type != C_WORKSPACE) {
+					focus_ws = container_parent(focus_ws, C_WORKSPACE);
+				}
+				if (focus_ws == workspace) {
+					seat_set_focus(seat, container);
+				}
+			}
+		}
+	} else {
+		workspace->sway_workspace->fullscreen = NULL;
+		if (container_is_floating(container)) {
+			container->x = container->saved_x;
+			container->y = container->saved_y;
+			container->width = container->saved_width;
+			container->height = container->saved_height;
+		} else {
+			container->width = container->saved_width;
+			container->height = container->saved_height;
+		}
+	}
+
+	container_end_mouse_operation(container);
+
+	ipc_event_window(container, "fullscreen_mode");
+}
+
+bool container_is_floating_or_child(struct sway_container *container) {
+	do {
+		if (container->parent && container->parent->layout == L_FLOATING) {
+			return true;
+		}
+		container = container->parent;
+	} while (container && container->type != C_WORKSPACE);
+
+	return false;
+}
+
+bool container_is_fullscreen_or_child(struct sway_container *container) {
+	do {
+		if (container->is_fullscreen) {
+			return true;
+		}
+		container = container->parent;
+	} while (container && container->type != C_WORKSPACE);
+
+	return false;
+}
+
+struct sway_container *container_wrap_children(struct sway_container *parent) {
+	struct sway_container *middle = container_create(C_CONTAINER);
+	middle->layout = parent->layout;
+	while (parent->children->length) {
+		struct sway_container *child = parent->children->items[0];
+		container_remove_child(child);
+		container_add_child(middle, child);
+	}
+	container_add_child(parent, middle);
+	return middle;
+}
diff --git a/sway/tree/layout.c b/sway/tree/layout.c
index 1f898f8a..1f82e534 100644
--- a/sway/tree/layout.c
+++ b/sway/tree/layout.c
@@ -6,6 +6,7 @@
 #include <string.h>
 #include <wlr/types/wlr_output.h>
 #include <wlr/types/wlr_output_layout.h>
+#include "config.h"
 #include "sway/debug.h"
 #include "sway/tree/arrange.h"
 #include "sway/tree/container.h"
@@ -39,9 +40,12 @@ void layout_init(void) {
 	root_container.sway_root = calloc(1, sizeof(*root_container.sway_root));
 	root_container.sway_root->output_layout = wlr_output_layout_create();
 	wl_list_init(&root_container.sway_root->outputs);
+#ifdef HAVE_XWAYLAND
 	wl_list_init(&root_container.sway_root->xwayland_unmanaged);
+#endif
 	wl_list_init(&root_container.sway_root->drag_icons);
 	wl_signal_init(&root_container.sway_root->events.new_container);
+	root_container.sway_root->scratchpad = create_list();
 
 	root_container.sway_root->output_layout_change.notify =
 		output_layout_handle_change;
@@ -62,10 +66,9 @@ static int index_child(const struct sway_container *child) {
 
 static void container_handle_fullscreen_reparent(struct sway_container *con,
 		struct sway_container *old_parent) {
-	if (con->type != C_VIEW || !con->sway_view->is_fullscreen) {
+	if (!con->is_fullscreen) {
 		return;
 	}
-	struct sway_view *view = con->sway_view;
 	struct sway_container *old_workspace = old_parent;
 	if (old_workspace && old_workspace->type != C_WORKSPACE) {
 		old_workspace = container_parent(old_workspace, C_WORKSPACE);
@@ -81,19 +84,27 @@ static void container_handle_fullscreen_reparent(struct sway_container *con,
 
 	// Mark the new workspace as fullscreen
 	if (new_workspace->sway_workspace->fullscreen) {
-		view_set_fullscreen(new_workspace->sway_workspace->fullscreen, false);
+		container_set_fullscreen(
+				new_workspace->sway_workspace->fullscreen, false);
 	}
-	new_workspace->sway_workspace->fullscreen = view;
-	// Resize view to new output dimensions
+	new_workspace->sway_workspace->fullscreen = con;
+
+	// Resize container to new output dimensions
 	struct sway_container *output = new_workspace->parent;
-	view->x = output->x;
-	view->y = output->y;
-	view->width = output->width;
-	view->height = output->height;
 	con->x = output->x;
 	con->y = output->y;
 	con->width = output->width;
 	con->height = output->height;
+
+	if (con->type == C_VIEW) {
+		struct sway_view *view = con->sway_view;
+		view->x = output->x;
+		view->y = output->y;
+		view->width = output->width;
+		view->height = output->height;
+	} else {
+		arrange_windows(new_workspace);
+	}
 }
 
 void container_insert_child(struct sway_container *parent,
@@ -135,10 +146,14 @@ void container_add_child(struct sway_container *parent,
 	list_add(parent->children, child);
 	child->parent = parent;
 	container_handle_fullscreen_reparent(child, old_parent);
+	if (old_parent) {
+		container_set_dirty(old_parent);
+	}
+	container_set_dirty(child);
 }
 
 struct sway_container *container_remove_child(struct sway_container *child) {
-	if (child->type == C_VIEW && child->sway_view->is_fullscreen) {
+	if (child->is_fullscreen) {
 		struct sway_container *workspace = container_parent(child, C_WORKSPACE);
 		workspace->sway_workspace->fullscreen = NULL;
 	}
@@ -153,6 +168,9 @@ struct sway_container *container_remove_child(struct sway_container *child) {
 	child->parent = NULL;
 	container_notify_subtree_changed(parent);
 
+	container_set_dirty(parent);
+	container_set_dirty(child);
+
 	return parent;
 }
 
@@ -199,7 +217,9 @@ void container_move_to(struct sway_container *container,
 		container_sort_workspaces(new_parent);
 		seat_set_focus(seat, new_parent);
 		workspace_output_raise_priority(container, old_parent, new_parent);
-		ipc_event_workspace(container, NULL, "move");
+		ipc_event_workspace(NULL, container, "move");
+	} else if (container->type == C_VIEW) {
+		ipc_event_window(container, "move");
 	}
 	container_notify_subtree_changed(old_parent);
 	container_notify_subtree_changed(new_parent);
@@ -218,10 +238,10 @@ void container_move_to(struct sway_container *container,
 			if (focus_ws->type != C_WORKSPACE) {
 				focus_ws = container_parent(focus_ws, C_WORKSPACE);
 			}
-			seat_set_focus(seat,
-					new_workspace->sway_workspace->fullscreen->swayc);
-			if (focus_ws != new_workspace) {
-				seat_set_focus(seat, focus);
+			if (focus_ws == new_workspace) {
+				struct sway_container *new_focus = seat_get_focus_inactive(seat,
+						new_workspace->sway_workspace->fullscreen);
+				seat_set_focus(seat, new_focus);
 			}
 		}
 	}
@@ -364,10 +384,18 @@ void container_move(struct sway_container *container,
 	struct sway_container *sibling = NULL;
 	struct sway_container *current = container;
 	struct sway_container *parent = current->parent;
+	struct sway_container *top = &root_container;
 
 	// If moving a fullscreen view, only consider outputs
-	if (container->type == C_VIEW && container->sway_view->is_fullscreen) {
+	if (container->is_fullscreen) {
 		current = container_parent(container, C_OUTPUT);
+	} else if (container_is_fullscreen_or_child(container) ||
+			container_is_floating_or_child(container)) {
+		// If we've fullscreened a split container, only allow the child to move
+		// around within the fullscreen parent.
+		// Same with floating a split container.
+		struct sway_container *ws = container_parent(container, C_WORKSPACE);
+		top = ws->sway_workspace->fullscreen;
 	}
 
 	struct sway_container *new_parent = container_flatten(parent);
@@ -377,7 +405,7 @@ void container_move(struct sway_container *container,
 	}
 
 	while (!sibling) {
-		if (current->type == C_ROOT) {
+		if (current == top) {
 			return;
 		}
 
@@ -441,8 +469,12 @@ void container_move(struct sway_container *container,
 				if ((index == parent->children->length - 1 && offs > 0)
 						|| (index == 0 && offs < 0)) {
 					if (current->parent == container->parent) {
-						if (parent->layout == L_TABBED
-								|| parent->layout == L_STACKED) {
+						if (parent->parent->layout == L_FLOATING) {
+							return;
+						}
+						if (!parent->is_fullscreen &&
+								(parent->layout == L_TABBED ||
+								 parent->layout == L_STACKED)) {
 							move_out_of_tabs_stacks(container, current,
 									move_dir, offs);
 							return;
@@ -463,10 +495,14 @@ void container_move(struct sway_container *container,
 					sibling = parent->children->items[index + offs];
 					wlr_log(WLR_DEBUG, "Selecting sibling id:%zd", sibling->id);
 				}
-			} else if (parent->layout == L_TABBED
-					|| parent->layout == L_STACKED) {
+			} else if (!parent->is_fullscreen &&
+					parent->parent->layout != L_FLOATING &&
+					(parent->layout == L_TABBED ||
+						parent->layout == L_STACKED)) {
 				move_out_of_tabs_stacks(container, current, move_dir, offs);
 				return;
+			} else if (parent->parent->layout == L_FLOATING) {
+				return;
 			} else {
 				wlr_log(WLR_DEBUG, "Moving up to find a parallel container");
 				current = current->parent;
@@ -544,6 +580,10 @@ void container_move(struct sway_container *container,
 	container_notify_subtree_changed(old_parent);
 	container_notify_subtree_changed(container->parent);
 
+	if (container->type == C_VIEW) {
+		ipc_event_window(container, "move");
+	}
+
 	if (old_parent) {
 		seat_set_focus(config->handler_context.seat, old_parent);
 		seat_set_focus(config->handler_context.seat, container);
@@ -558,10 +598,11 @@ void container_move(struct sway_container *container,
 		next_ws = container_parent(next_ws, C_WORKSPACE);
 	}
 	if (last_ws && next_ws && last_ws != next_ws) {
-		ipc_event_workspace(last_ws, container, "focus");
+		ipc_event_workspace(last_ws, next_ws, "focus");
 		workspace_detect_urgent(last_ws);
 		workspace_detect_urgent(next_ws);
 	}
+	container_end_mouse_operation(container);
 }
 
 enum sway_container_layout container_get_default_layout(
@@ -691,22 +732,18 @@ struct sway_container *container_get_in_direction(
 		enum movement_direction dir) {
 	struct sway_container *parent = container->parent;
 
-	if (container_is_floating(container)) {
-		return NULL;
+	if (dir == MOVE_CHILD) {
+		return seat_get_focus_inactive(seat, container);
 	}
-
-	if (container->type == C_VIEW && container->sway_view->is_fullscreen) {
-		if (dir == MOVE_PARENT || dir == MOVE_CHILD) {
+	if (container->is_fullscreen) {
+		if (dir == MOVE_PARENT) {
 			return NULL;
 		}
 		container = container_parent(container, C_OUTPUT);
 		parent = container->parent;
 	} else {
-		if (dir == MOVE_CHILD) {
-			return seat_get_focus_inactive(seat, container);
-		}
 		if (dir == MOVE_PARENT) {
-			if (parent->type == C_OUTPUT) {
+			if (parent->type == C_OUTPUT || container_is_floating(container)) {
 				return NULL;
 			} else {
 				return parent;
@@ -755,7 +792,8 @@ struct sway_container *container_get_in_direction(
 			}
 			sway_assert(next_workspace, "Next container has no workspace");
 			if (next_workspace->sway_workspace->fullscreen) {
-				return next_workspace->sway_workspace->fullscreen->swayc;
+				return seat_get_focus_inactive(seat,
+						next_workspace->sway_workspace->fullscreen);
 			}
 			if (next->children && next->children->length) {
 				// TODO consider floating children as well
@@ -963,13 +1001,13 @@ static void swap_focus(struct sway_container *con1,
 		if (focus == con1 && (con2->parent->layout == L_TABBED
 					|| con2->parent->layout == L_STACKED)) {
 			if (workspace_is_visible(ws2)) {
-				seat_set_focus_warp(seat, con2, false);
+				seat_set_focus_warp(seat, con2, false, true);
 			}
 			seat_set_focus(seat, ws1 != ws2 ? con2 : con1);
 		} else if (focus == con2 && (con1->parent->layout == L_TABBED
 					|| con1->parent->layout == L_STACKED)) {
 			if (workspace_is_visible(ws1)) {
-				seat_set_focus_warp(seat, con1, false);
+				seat_set_focus_warp(seat, con1, false, true);
 			}
 			seat_set_focus(seat, ws1 != ws2 ? con1 : con2);
 		} else if (ws1 != ws2) {
@@ -1002,13 +1040,13 @@ void container_swap(struct sway_container *con1, struct sway_container *con2) {
 
 	wlr_log(WLR_DEBUG, "Swapping containers %zu and %zu", con1->id, con2->id);
 
-	int fs1 = con1->type == C_VIEW && con1->sway_view->is_fullscreen;
-	int fs2 = con2->type == C_VIEW && con2->sway_view->is_fullscreen;
+	int fs1 = con1->is_fullscreen;
+	int fs2 = con2->is_fullscreen;
 	if (fs1) {
-		view_set_fullscreen(con1->sway_view, false);
+		container_set_fullscreen(con1, false);
 	}
 	if (fs2) {
-		view_set_fullscreen(con2->sway_view, false);
+		container_set_fullscreen(con2, false);
 	}
 
 	struct sway_seat *seat = input_manager_get_default_seat(input_manager);
@@ -1041,10 +1079,10 @@ void container_swap(struct sway_container *con1, struct sway_container *con2) {
 		prev_workspace_name = stored_prev_name;
 	}
 
-	if (fs1 && con2->type == C_VIEW) {
-		view_set_fullscreen(con2->sway_view, true);
+	if (fs1) {
+		container_set_fullscreen(con2, true);
 	}
-	if (fs2 && con1->type == C_VIEW) {
-		view_set_fullscreen(con1->sway_view, true);
+	if (fs2) {
+		container_set_fullscreen(con1, true);
 	}
 }
diff --git a/sway/tree/output.c b/sway/tree/output.c
index da535c18..31e3bf9b 100644
--- a/sway/tree/output.c
+++ b/sway/tree/output.c
@@ -22,7 +22,7 @@ static void restore_workspaces(struct sway_container *output) {
 			if (highest == output) {
 				container_remove_child(ws);
 				container_add_child(output, ws);
-				ipc_event_workspace(ws, NULL, "move");
+				ipc_event_workspace(NULL, ws, "move");
 				j--;
 			}
 		}
diff --git a/sway/tree/view.c b/sway/tree/view.c
index 7881e6d7..97318daa 100644
--- a/sway/tree/view.c
+++ b/sway/tree/view.c
@@ -2,7 +2,12 @@
 #include <stdlib.h>
 #include <wayland-server.h>
 #include <wlr/render/wlr_renderer.h>
+#include <wlr/types/wlr_buffer.h>
 #include <wlr/types/wlr_output_layout.h>
+#include "config.h"
+#ifdef HAVE_XWAYLAND
+#include <wlr/xwayland.h>
+#endif
 #include "list.h"
 #include "log.h"
 #include "sway/criteria.h"
@@ -107,14 +112,14 @@ const char *view_get_instance(struct sway_view *view) {
 	}
 	return NULL;
 }
-
+#ifdef HAVE_XWAYLAND
 uint32_t view_get_x11_window_id(struct sway_view *view) {
 	if (view->impl->get_int_prop) {
 		return view->impl->get_int_prop(view, VIEW_PROP_X11_WINDOW_ID);
 	}
 	return 0;
 }
-
+#endif
 const char *view_get_window_role(struct sway_view *view) {
 	if (view->impl->get_string_prop) {
 		return view->impl->get_string_prop(view, VIEW_PROP_WINDOW_ROLE);
@@ -135,12 +140,27 @@ const char *view_get_shell(struct sway_view *view) {
 		return "xdg_shell_v6";
 	case SWAY_VIEW_XDG_SHELL:
 		return "xdg_shell";
+#ifdef HAVE_XWAYLAND
 	case SWAY_VIEW_XWAYLAND:
 		return "xwayland";
+#endif
 	}
 	return "unknown";
 }
 
+void view_get_constraints(struct sway_view *view, double *min_width,
+		double *max_width, double *min_height, double *max_height) {
+	if (view->impl->get_constraints) {
+		view->impl->get_constraints(view,
+				min_width, max_width, min_height, max_height);
+	} else {
+		*min_width = DBL_MIN;
+		*max_width = DBL_MAX;
+		*min_height = DBL_MIN;
+		*max_height = DBL_MAX;
+	}
+}
+
 uint32_t view_configure(struct sway_view *view, double lx, double ly, int width,
 		int height) {
 	if (view->impl->configure) {
@@ -149,55 +169,6 @@ uint32_t view_configure(struct sway_view *view, double lx, double ly, int width,
 	return 0;
 }
 
-void view_init_floating(struct sway_view *view) {
-	struct sway_container *ws = container_parent(view->swayc, C_WORKSPACE);
-	int min_width, min_height;
-	int max_width, max_height;
-
-	if (config->floating_minimum_width == -1) { // no minimum
-		min_width = 0;
-	} else if (config->floating_minimum_width == 0) { // automatic
-		min_width = 75;
-	} else {
-		min_width = config->floating_minimum_width;
-	}
-
-	if (config->floating_minimum_height == -1) { // no minimum
-		min_height = 0;
-	} else if (config->floating_minimum_height == 0) { // automatic
-		min_height = 50;
-	} else {
-		min_height = config->floating_minimum_height;
-	}
-
-	if (config->floating_maximum_width == -1) { // no maximum
-		max_width = INT_MAX;
-	} else if (config->floating_maximum_width == 0) { // automatic
-		max_width = ws->width * 0.6666;
-	} else {
-		max_width = config->floating_maximum_width;
-	}
-
-	if (config->floating_maximum_height == -1) { // no maximum
-		max_height = INT_MAX;
-	} else if (config->floating_maximum_height == 0) { // automatic
-		max_height = ws->height * 0.6666;
-	} else {
-		max_height = config->floating_maximum_height;
-	}
-
-	view->width = fmax(min_width, fmin(view->natural_width, max_width));
-	view->height = fmax(min_height, fmin(view->natural_height, max_height));
-	view->x = ws->x + (ws->width - view->width) / 2;
-	view->y = ws->y + (ws->height - view->height) / 2;
-
-	// If the view's border is B_NONE then these properties are ignored.
-	view->border_top = view->border_bottom = true;
-	view->border_left = view->border_right = true;
-
-	container_set_geometry_from_floating_view(view->swayc);
-}
-
 void view_autoconfigure(struct sway_view *view) {
 	if (!sway_assert(view->swayc,
 				"Called view_autoconfigure() on a view without a swayc")) {
@@ -206,7 +177,7 @@ void view_autoconfigure(struct sway_view *view) {
 
 	struct sway_container *output = container_parent(view->swayc, C_OUTPUT);
 
-	if (view->is_fullscreen) {
+	if (view->swayc->is_fullscreen) {
 		view->x = output->x;
 		view->y = output->y;
 		view->width = output->width;
@@ -214,10 +185,6 @@ void view_autoconfigure(struct sway_view *view) {
 		return;
 	}
 
-	if (container_is_floating(view->swayc)) {
-		return;
-	}
-
 	struct sway_container *ws = container_parent(view->swayc, C_WORKSPACE);
 
 	int other_views = 0;
@@ -330,72 +297,18 @@ void view_set_tiled(struct sway_view *view, bool tiled) {
 	}
 }
 
-void view_set_fullscreen(struct sway_view *view, bool fullscreen) {
-	if (view->is_fullscreen == fullscreen) {
-		return;
-	}
-
-	struct sway_container *workspace =
-		container_parent(view->swayc, C_WORKSPACE);
-
-	if (view->impl->set_fullscreen) {
-		view->impl->set_fullscreen(view, fullscreen);
-	}
-
-	view->is_fullscreen = fullscreen;
-
-	if (fullscreen) {
-		if (workspace->sway_workspace->fullscreen) {
-			view_set_fullscreen(workspace->sway_workspace->fullscreen, false);
-		}
-		workspace->sway_workspace->fullscreen = view;
-		view->saved_x = view->x;
-		view->saved_y = view->y;
-		view->saved_width = view->width;
-		view->saved_height = view->height;
-		view->swayc->saved_x = view->swayc->x;
-		view->swayc->saved_y = view->swayc->y;
-		view->swayc->saved_width = view->swayc->width;
-		view->swayc->saved_height = view->swayc->height;
-
-		struct sway_seat *seat;
-		struct sway_container *focus, *focus_ws;
-		wl_list_for_each(seat, &input_manager->seats, link) {
-			focus = seat_get_focus(seat);
-			if (focus) {
-				focus_ws = focus;
-				if (focus && focus_ws->type != C_WORKSPACE) {
-					focus_ws = container_parent(focus_ws, C_WORKSPACE);
-				}
-				seat_set_focus(seat, view->swayc);
-				if (focus_ws != workspace) {
-					seat_set_focus(seat, focus);
-				}
-			}
-		}
-	} else {
-		workspace->sway_workspace->fullscreen = NULL;
-		if (container_is_floating(view->swayc)) {
-			view->x = view->saved_x;
-			view->y = view->saved_y;
-			view->width = view->saved_width;
-			view->height = view->saved_height;
-			container_set_geometry_from_floating_view(view->swayc);
-		} else {
-			view->swayc->width = view->swayc->saved_width;
-			view->swayc->height = view->swayc->saved_height;
-		}
-	}
-
-	ipc_event_window(view->swayc, "fullscreen_mode");
-}
-
 void view_close(struct sway_view *view) {
 	if (view->impl->close) {
 		view->impl->close(view);
 	}
 }
 
+void view_close_popups(struct sway_view *view) {
+	if (view->impl->close_popups) {
+		view->impl->close_popups(view);
+	}
+}
+
 void view_damage_from(struct sway_view *view) {
 	for (int i = 0; i < root_container.children->length; ++i) {
 		struct sway_container *cont = root_container.children->items[i];
@@ -426,6 +339,16 @@ void view_for_each_surface(struct sway_view *view,
 	}
 }
 
+void view_for_each_popup(struct sway_view *view,
+		wlr_surface_iterator_func_t iterator, void *user_data) {
+	if (!view->surface) {
+		return;
+	}
+	if (view->impl->for_each_popup) {
+		view->impl->for_each_popup(view, iterator, user_data);
+	}
+}
+
 static void view_subsurface_create(struct sway_view *view,
 	struct wlr_subsurface *subsurface);
 
@@ -521,12 +444,82 @@ void view_execute_criteria(struct sway_view *view) {
 	seat_set_focus(seat, prior_focus);
 }
 
+static struct sway_container *select_workspace(struct sway_view *view) {
+	struct sway_seat *seat = input_manager_current_seat(input_manager);
+
+	// Check if there's any `assign` criteria for the view
+	list_t *criterias = criteria_for_view(view,
+			CT_ASSIGN_WORKSPACE | CT_ASSIGN_OUTPUT);
+	struct sway_container *ws = NULL;
+	for (int i = 0; i < criterias->length; ++i) {
+		struct criteria *criteria = criterias->items[i];
+		if (criteria->type == CT_ASSIGN_WORKSPACE) {
+			ws = workspace_by_name(criteria->target);
+			if (!ws) {
+				ws = workspace_create(NULL, criteria->target);
+			}
+			break;
+		} else {
+			// CT_ASSIGN_OUTPUT
+			struct sway_container *output = output_by_name(criteria->target);
+			if (output) {
+				ws = seat_get_active_child(seat, output);
+				break;
+			}
+		}
+	}
+	list_free(criterias);
+	if (ws) {
+		return ws;
+	}
+
+	// Check if there's a PID mapping
+	pid_t pid;
+#ifdef HAVE_XWAYLAND
+	if (view->type == SWAY_VIEW_XWAYLAND) {
+		struct wlr_xwayland_surface *surf =
+			wlr_xwayland_surface_from_wlr_surface(view->surface);
+		pid = surf->pid;
+	} else {
+		struct wl_client *client =
+			wl_resource_get_client(view->surface->resource);
+		wl_client_get_credentials(client, &pid, NULL, NULL);
+	}
+#else
+	struct wl_client *client =
+		wl_resource_get_client(view->surface->resource);
+	wl_client_get_credentials(client, &pid, NULL, NULL);
+#endif
+	ws = workspace_for_pid(pid);
+	if (ws) {
+		return ws;
+	}
+
+	// Use the focused workspace
+	ws = seat_get_focus_inactive(seat, &root_container);
+	if (ws->type != C_WORKSPACE) {
+		ws = container_parent(ws, C_WORKSPACE);
+	}
+	return ws;
+}
+
 static bool should_focus(struct sway_view *view) {
+	struct sway_seat *seat = input_manager_current_seat(input_manager);
+	struct sway_container *prev_focus =
+		seat_get_focus_inactive(seat, &root_container);
+	struct sway_container *prev_ws = prev_focus->type == C_WORKSPACE ?
+		prev_focus : container_parent(prev_focus, C_WORKSPACE);
+	struct sway_container *map_ws = container_parent(view->swayc, C_WORKSPACE);
+
+	// Views can only take focus if they are mapped into the active workspace
+	if (prev_ws != map_ws) {
+		return false;
+	}
+
 	// If the view is the only one in the focused workspace, it'll get focus
 	// regardless of any no_focus criteria.
 	struct sway_container *parent = view->swayc->parent;
-	struct sway_seat *seat = input_manager_current_seat(input_manager);
-	if (parent->type == C_WORKSPACE && seat_get_focus(seat) == parent) {
+	if (parent->type == C_WORKSPACE && prev_focus == parent) {
 		size_t num_children = parent->children->length +
 			parent->sway_workspace->floating->children->length;
 		if (num_children == 1) {
@@ -545,42 +538,19 @@ void view_map(struct sway_view *view, struct wlr_surface *wlr_surface) {
 	if (!sway_assert(view->surface == NULL, "cannot map mapped view")) {
 		return;
 	}
+	view->surface = wlr_surface;
 
 	struct sway_seat *seat = input_manager_current_seat(input_manager);
-	struct sway_container *focus =
-		seat_get_focus_inactive(seat, &root_container);
-	struct sway_container *cont = NULL;
+	struct sway_container *ws = select_workspace(view);
+	struct sway_container *target_sibling = seat_get_focus_inactive(seat, ws);
 
-	// Check if there's any `assign` criteria for the view
-	list_t *criterias = criteria_for_view(view,
-			CT_ASSIGN_WORKSPACE | CT_ASSIGN_OUTPUT);
-	struct sway_container *workspace = NULL;
-	if (criterias->length) {
-		struct criteria *criteria = criterias->items[0];
-		if (criteria->type == CT_ASSIGN_WORKSPACE) {
-			workspace = workspace_by_name(criteria->target);
-			if (!workspace) {
-				workspace = workspace_create(NULL, criteria->target);
-			}
-			focus = seat_get_focus_inactive(seat, workspace);
-		} else {
-			// CT_ASSIGN_OUTPUT
-			struct sway_container *output = output_by_name(criteria->target);
-			if (output) {
-				focus = seat_get_focus_inactive(seat, output);
-			}
-		}
-	}
 	// If we're about to launch the view into the floating container, then
 	// launch it as a tiled view in the root of the workspace instead.
-	if (container_is_floating(focus)) {
-		focus = focus->parent->parent;
+	if (container_is_floating(target_sibling)) {
+		target_sibling = target_sibling->parent->parent;
 	}
-	list_free(criterias);
-	cont = container_view_create(focus, view);
 
-	view->surface = wlr_surface;
-	view->swayc = cont;
+	view->swayc = container_view_create(target_sibling, view);
 
 	view_init_subsurfaces(view, wlr_surface);
 	wl_signal_add(&wlr_surface->events.new_subsurface,
@@ -601,10 +571,7 @@ void view_map(struct sway_view *view, struct wlr_surface *wlr_surface) {
 	}
 
 	if (should_focus(view)) {
-		input_manager_set_focus(input_manager, cont);
-		if (workspace) {
-			workspace_switch(workspace);
-		}
+		input_manager_set_focus(input_manager, view->swayc);
 	}
 
 	view_update_title(view, false);
@@ -628,10 +595,8 @@ void view_unmap(struct sway_view *view) {
 	struct sway_container *ws = container_parent(view->swayc, C_WORKSPACE);
 
 	struct sway_container *parent;
-	if (view->is_fullscreen) {
-		ws->sway_workspace->fullscreen = NULL;
+	if (container_is_fullscreen_or_child(view->swayc)) {
 		parent = container_destroy(view->swayc);
-
 		arrange_windows(ws->parent);
 	} else {
 		parent = container_destroy(view->swayc);
@@ -784,11 +749,13 @@ struct sway_view *view_from_wlr_surface(struct wlr_surface *wlr_surface) {
 			wlr_xdg_surface_v6_from_wlr_surface(wlr_surface);
 		return view_from_wlr_xdg_surface_v6(xdg_surface_v6);
 	}
+#ifdef HAVE_XWAYLAND
 	if (wlr_surface_is_xwayland_surface(wlr_surface)) {
 		struct wlr_xwayland_surface *xsurface =
 			wlr_xwayland_surface_from_wlr_surface(wlr_surface);
 		return view_from_wlr_xwayland_surface(xsurface);
 	}
+#endif
 	if (wlr_surface_is_subsurface(wlr_surface)) {
 		struct wlr_subsurface *subsurface =
 			wlr_subsurface_from_wlr_surface(wlr_surface);
@@ -915,6 +882,8 @@ void view_update_title(struct sway_view *view, bool force) {
 
 	// Update title after the global font height is updated
 	container_update_title_textures(view->swayc);
+
+	ipc_event_window(view->swayc, "title");
 }
 
 static bool find_by_mark_iterator(struct sway_container *con,
@@ -937,6 +906,7 @@ bool view_find_and_unmark(char *mark) {
 			free(view_mark);
 			list_del(view->marks, i);
 			view_update_marks_textures(view);
+			ipc_event_window(container, "mark");
 			return true;
 		}
 	}
@@ -944,11 +914,10 @@ bool view_find_and_unmark(char *mark) {
 }
 
 void view_clear_marks(struct sway_view *view) {
-	for (int i = 0; i < view->marks->length; ++i) {
-		free(view->marks->items[i]);
+	while (view->marks->length) {
+		list_del(view->marks, 0);
+		ipc_event_window(view->swayc, "mark");
 	}
-	list_free(view->marks);
-	view->marks = create_list();
 }
 
 bool view_has_mark(struct sway_view *view, char *mark) {
@@ -961,6 +930,11 @@ bool view_has_mark(struct sway_view *view, char *mark) {
 	return false;
 }
 
+void view_add_mark(struct sway_view *view, char *mark) {
+	list_add(view->marks, strdup(mark));
+	ipc_event_window(view->swayc, "mark");
+}
+
 static void update_marks_texture(struct sway_view *view,
 		struct wlr_texture **texture, struct border_colors *class) {
 	struct sway_container *output = container_parent(view->swayc, C_OUTPUT);
@@ -1055,6 +1029,9 @@ bool view_is_visible(struct sway_view *view) {
 	}
 	struct sway_container *workspace =
 		container_parent(view->swayc, C_WORKSPACE);
+	if (!workspace) {
+		return false;
+	}
 	// Determine if view is nested inside a floating container which is sticky.
 	// A simple floating view will have this ancestry:
 	// C_VIEW -> floating -> workspace
@@ -1079,7 +1056,8 @@ bool view_is_visible(struct sway_view *view) {
 		container = container->parent;
 	}
 	// Check view isn't hidden by another fullscreen view
-	if (workspace->sway_workspace->fullscreen && !view->is_fullscreen) {
+	if (workspace->sway_workspace->fullscreen &&
+			!container_is_fullscreen_or_child(view->swayc)) {
 		return false;
 	}
 	// Check the workspace is visible
@@ -1117,3 +1095,22 @@ void view_set_urgent(struct sway_view *view, bool enable) {
 bool view_is_urgent(struct sway_view *view) {
 	return view->urgent.tv_sec || view->urgent.tv_nsec;
 }
+
+void view_remove_saved_buffer(struct sway_view *view) {
+	if (!sway_assert(view->saved_buffer, "Expected a saved buffer")) {
+		return;
+	}
+	wlr_buffer_unref(view->saved_buffer);
+	view->saved_buffer = NULL;
+}
+
+void view_save_buffer(struct sway_view *view) {
+	if (!sway_assert(!view->saved_buffer, "Didn't expect saved buffer")) {
+		view_remove_saved_buffer(view);
+	}
+	if (view->surface && wlr_surface_has_buffer(view->surface)) {
+		view->saved_buffer = wlr_buffer_ref(view->surface->buffer);
+		view->saved_buffer_width = view->surface->current.width;
+		view->saved_buffer_height = view->surface->current.height;
+	}
+}
diff --git a/sway/tree/workspace.c b/sway/tree/workspace.c
index 622f01ec..588e2aae 100644
--- a/sway/tree/workspace.c
+++ b/sway/tree/workspace.c
@@ -9,6 +9,7 @@
 #include "sway/input/input-manager.h"
 #include "sway/input/seat.h"
 #include "sway/ipc-server.h"
+#include "sway/output.h"
 #include "sway/tree/arrange.h"
 #include "sway/tree/container.h"
 #include "sway/tree/view.h"
@@ -107,6 +108,84 @@ static bool workspace_valid_on_output(const char *output_name,
 	return true;
 }
 
+static void workspace_name_from_binding(const struct sway_binding * binding,
+		const char* output_name, int *min_order, char **earliest_name) {
+	char *cmdlist = strdup(binding->command);
+	char *dup = cmdlist;
+	char *name = NULL;
+
+	// workspace n
+	char *cmd = argsep(&cmdlist, " ");
+	if (cmdlist) {
+		name = argsep(&cmdlist, ",;");
+	}
+
+	if (strcmp("workspace", cmd) == 0 && name) {
+		char *_target = strdup(name);
+		_target = do_var_replacement(_target);
+		strip_quotes(_target);
+		wlr_log(WLR_DEBUG, "Got valid workspace command for target: '%s'",
+				_target);
+
+		// Make sure that the command references an actual workspace
+		// not a command about workspaces
+		if (strcmp(_target, "next") == 0 ||
+				strcmp(_target, "prev") == 0 ||
+				strcmp(_target, "next_on_output") == 0 ||
+				strcmp(_target, "prev_on_output") == 0 ||
+				strcmp(_target, "number") == 0 ||
+				strcmp(_target, "back_and_forth") == 0 ||
+				strcmp(_target, "current") == 0) {
+			free(_target);
+			free(dup);
+			return;
+		}
+
+		// If the command is workspace number <name>, isolate the name
+		if (strncmp(_target, "number ", strlen("number ")) == 0) {
+			size_t length = strlen(_target) - strlen("number ") + 1;
+			char *temp = malloc(length);
+			strncpy(temp, _target + strlen("number "), length - 1);
+			temp[length - 1] = '\0';
+			free(_target);
+			_target = temp;
+			wlr_log(WLR_DEBUG, "Isolated name from workspace number: '%s'", _target);
+
+			// Make sure the workspace number doesn't already exist
+			if (workspace_by_number(_target)) {
+				free(_target);
+				free(dup);
+				return;
+			}
+		}
+
+		// Make sure that the workspace doesn't already exist
+		if (workspace_by_name(_target)) {
+			free(_target);
+			free(dup);
+			return;
+		}
+
+		// make sure that the workspace can appear on the given
+		// output
+		if (!workspace_valid_on_output(output_name, _target)) {
+			free(_target);
+			free(dup);
+			return;
+		}
+
+		if (binding->order < *min_order) {
+			*min_order = binding->order;
+			free(*earliest_name);
+			*earliest_name = _target;
+			wlr_log(WLR_DEBUG, "Workspace: Found free name %s", _target);
+		} else {
+			free(_target);
+		}
+	}
+	free(dup);
+}
+
 char *workspace_next_name(const char *output_name) {
 	wlr_log(WLR_DEBUG, "Workspace: Generating new workspace name for output %s",
 			output_name);
@@ -114,89 +193,15 @@ char *workspace_next_name(const char *output_name) {
 	// if none are found/available then default to a number
 	struct sway_mode *mode = config->current_mode;
 
-	// TODO: iterate over keycode bindings too
 	int order = INT_MAX;
 	char *target = NULL;
 	for (int i = 0; i < mode->keysym_bindings->length; ++i) {
-		struct sway_binding *binding = mode->keysym_bindings->items[i];
-		char *cmdlist = strdup(binding->command);
-		char *dup = cmdlist;
-		char *name = NULL;
-
-		// workspace n
-		char *cmd = argsep(&cmdlist, " ");
-		if (cmdlist) {
-			name = argsep(&cmdlist, ",;");
-		}
-
-		if (strcmp("workspace", cmd) == 0 && name) {
-			char *_target = strdup(name);
-			_target = do_var_replacement(_target);
-			strip_quotes(_target);
-			while (isspace(*_target)) {
-				memmove(_target, _target+1, strlen(_target+1));
-			}
-			wlr_log(WLR_DEBUG, "Got valid workspace command for target: '%s'",
-					_target);
-
-			// Make sure that the command references an actual workspace
-			// not a command about workspaces
-			if (strcmp(_target, "next") == 0 ||
-				strcmp(_target, "prev") == 0 ||
-				strcmp(_target, "next_on_output") == 0 ||
-				strcmp(_target, "prev_on_output") == 0 ||
-				strcmp(_target, "number") == 0 ||
-				strcmp(_target, "back_and_forth") == 0 ||
-				strcmp(_target, "current") == 0)
-			{
-				free(_target);
-				free(dup);
-				continue;
-			}
-
-			// If the command is workspace number <name>, isolate the name
-			if (strncmp(_target, "number ", strlen("number ")) == 0) {
-				size_t length = strlen(_target) - strlen("number ") + 1;
-				char *temp = malloc(length);
-				strncpy(temp, _target + strlen("number "), length - 1);
-				temp[length - 1] = '\0';
-				free(_target);
-				_target = temp;
-				wlr_log(WLR_DEBUG, "Isolated name from workspace number: '%s'", _target);
-
-				// Make sure the workspace number doesn't already exist
-				if (workspace_by_number(_target)) {
-					free(_target);
-					free(dup);
-					continue;
-				}
-			}
-
-			// Make sure that the workspace doesn't already exist
-			if (workspace_by_name(_target)) {
-				free(_target);
-				free(dup);
-				continue;
-			}
-
-			// make sure that the workspace can appear on the given
-			// output
-			if (!workspace_valid_on_output(output_name, _target)) {
-				free(_target);
-				free(dup);
-				continue;
-			}
-
-			if (binding->order < order) {
-				order = binding->order;
-				free(target);
-				target = _target;
-				wlr_log(WLR_DEBUG, "Workspace: Found free name %s", _target);
-			} else {
-				free(_target);
-			}
-		}
-		free(dup);
+		workspace_name_from_binding(mode->keysym_bindings->items[i],
+				output_name, &order, &target);
+	}
+	for (int i = 0; i < mode->keycode_bindings->length; ++i) {
+		workspace_name_from_binding(mode->keycode_bindings->items[i],
+				output_name, &order, &target);
 	}
 	if (target != NULL) {
 		return target;
@@ -529,3 +534,116 @@ void workspace_detect_urgent(struct sway_container *workspace) {
 		container_damage_whole(workspace);
 	}
 }
+
+struct pid_workspace {
+	pid_t pid;
+	char *workspace;
+	struct timespec time_added;
+
+	struct sway_container *output;
+	struct wl_listener output_destroy;
+
+	struct wl_list link;
+};
+
+static struct wl_list pid_workspaces;
+
+struct sway_container *workspace_for_pid(pid_t pid) {
+	if (!pid_workspaces.prev && !pid_workspaces.next) {
+		wl_list_init(&pid_workspaces);
+		return NULL;
+	}
+
+	struct sway_container *ws = NULL;
+	struct pid_workspace *pw = NULL;
+
+	wlr_log(WLR_DEBUG, "Looking up workspace for pid %d", pid);
+
+	do {
+		struct pid_workspace *_pw = NULL;
+		wl_list_for_each(_pw, &pid_workspaces, link) {
+			if (pid == _pw->pid) {
+				pw = _pw;
+				wlr_log(WLR_DEBUG,
+						"found pid_workspace for pid %d, workspace %s",
+						pid, pw->workspace);
+				goto found;
+			}
+		}
+		pid = get_parent_pid(pid);
+	} while (pid > 1);
+found:
+
+	if (pw && pw->workspace) {
+		ws = workspace_by_name(pw->workspace);
+
+		if (!ws) {
+			wlr_log(WLR_DEBUG,
+					"Creating workspace %s for pid %d because it disappeared",
+					pw->workspace, pid);
+			ws = workspace_create(pw->output, pw->workspace);
+		}
+
+		wl_list_remove(&pw->output_destroy.link);
+		wl_list_remove(&pw->link);
+		free(pw->workspace);
+		free(pw);
+	}
+
+	return ws;
+}
+
+static void pw_handle_output_destroy(struct wl_listener *listener, void *data) {
+	struct pid_workspace *pw = wl_container_of(listener, pw, output_destroy);
+	pw->output = NULL;
+	wl_list_remove(&pw->output_destroy.link);
+	wl_list_init(&pw->output_destroy.link);
+}
+
+void workspace_record_pid(pid_t pid) {
+	wlr_log(WLR_DEBUG, "Recording workspace for process %d", pid);
+	if (!pid_workspaces.prev && !pid_workspaces.next) {
+		wl_list_init(&pid_workspaces);
+	}
+
+	struct sway_seat *seat = input_manager_current_seat(input_manager);
+	struct sway_container *ws =
+		seat_get_focus_inactive(seat, &root_container);
+	if (ws && ws->type != C_WORKSPACE) {
+		ws = container_parent(ws, C_WORKSPACE);
+	}
+	if (!ws) {
+		wlr_log(WLR_DEBUG, "Bailing out, no workspace");
+		return;
+	}
+	struct sway_container *output = ws->parent;
+	if (!output) {
+		wlr_log(WLR_DEBUG, "Bailing out, no output");
+		return;
+	}
+
+	struct timespec now;
+	clock_gettime(CLOCK_MONOTONIC, &now);
+
+	// Remove expired entries
+	static const int timeout = 60;
+	struct pid_workspace *old, *_old;
+	wl_list_for_each_safe(old, _old, &pid_workspaces, link) {
+		if (now.tv_sec - old->time_added.tv_sec >= timeout) {
+			wl_list_remove(&old->output_destroy.link);
+			wl_list_remove(&old->link);
+			free(old->workspace);
+			free(old);
+		}
+	}
+
+	struct pid_workspace *pw = calloc(1, sizeof(struct pid_workspace));
+	pw->workspace = strdup(ws->name);
+	pw->output = output;
+	pw->pid = pid;
+	memcpy(&pw->time_added, &now, sizeof(struct timespec));
+	pw->output_destroy.notify = pw_handle_output_destroy;
+	wl_signal_add(&output->sway_output->wlr_output->events.destroy,
+			&pw->output_destroy);
+	wl_list_insert(&pid_workspaces, &pw->link);
+}
diff --git a/swayidle/swayidle.1.scd b/swayidle/swayidle.1.scd
index 5cd4a7fd..7c1b138a 100644
--- a/swayidle/swayidle.1.scd
+++ b/swayidle/swayidle.1.scd
@@ -58,4 +58,4 @@ https://github.com/swaywm/sway.
 
 # SEE ALSO
 
-*sway*(5) *swaymsg*(1) *swaygrab*(1) *sway-input*(5) *sway-bar*(5)
+*sway*(5) *swaymsg*(1) *sway-input*(5) *sway-bar*(5)
diff --git a/swaymsg/main.c b/swaymsg/main.c
index c4141ca5..3767daf3 100644
--- a/swaymsg/main.c
+++ b/swaymsg/main.c
@@ -250,12 +250,16 @@ static void pretty_print(int type, json_object *resp) {
 	if (type != IPC_COMMAND && type != IPC_GET_WORKSPACES &&
 			type != IPC_GET_INPUTS && type != IPC_GET_OUTPUTS &&
 			type != IPC_GET_VERSION && type != IPC_GET_SEATS &&
-			type != IPC_GET_CONFIG) {
+			type != IPC_GET_CONFIG && type != IPC_SEND_TICK) {
 		printf("%s\n", json_object_to_json_string_ext(resp,
 			JSON_C_TO_STRING_PRETTY | JSON_C_TO_STRING_SPACED));
 		return;
 	}
 
+	if (type == IPC_SEND_TICK) {
+		return;
+	}
+
 	if (type == IPC_GET_VERSION) {
 		pretty_print_version(resp);
 		return;
@@ -384,6 +388,8 @@ int main(int argc, char **argv) {
 		type = IPC_GET_BINDING_MODES;
 	} else if (strcasecmp(cmdtype, "get_config") == 0) {
 		type = IPC_GET_CONFIG;
+	} else if (strcasecmp(cmdtype, "send_tick") == 0) {
+		type = IPC_SEND_TICK;
 	} else {
 		sway_abort("Unknown message type %s", cmdtype);
 	}
diff --git a/swaymsg/swaymsg.1.scd b/swaymsg/swaymsg.1.scd
index a6e279da..8cf1b222 100644
--- a/swaymsg/swaymsg.1.scd
+++ b/swaymsg/swaymsg.1.scd
@@ -64,3 +64,6 @@ _swaymsg_ [options...] [message]
 
 *get\_config*
 	Gets a JSON-encoded copy of the current configuration.
+
+*send\_tick*
+	Sends a tick event to all subscribed clients.
diff --git a/swaynag/config.c b/swaynag/config.c
new file mode 100644
index 00000000..d6c5739d
--- /dev/null
+++ b/swaynag/config.c
@@ -0,0 +1,401 @@
+#define _XOPEN_SOURCE 700
+#define _POSIX_C_SOURCE 200112L
+#include <getopt.h>
+#include <stdlib.h>
+#include <wordexp.h>
+#include "log.h"
+#include "list.h"
+#include "readline.h"
+#include "swaynag/swaynag.h"
+#include "swaynag/types.h"
+#include "util.h"
+#include "wlr-layer-shell-unstable-v1-client-protocol.h"
+
+static char *read_from_stdin() {
+	char *buffer = NULL;
+	while (!feof(stdin)) {
+		char *line = read_line(stdin);
+		if (!line) {
+			continue;
+		}
+
+		size_t curlen = buffer ? strlen(buffer) : 0;
+		buffer = realloc(buffer, curlen + strlen(line) + 2);
+		snprintf(buffer + curlen, strlen(line) + 2, "%s\n", line);
+
+		free(line);
+	}
+
+	while (buffer && buffer[strlen(buffer) - 1] == '\n') {
+		buffer[strlen(buffer) - 1] = '\0';
+	}
+
+	return buffer;
+}
+
+int swaynag_parse_options(int argc, char **argv, struct swaynag *swaynag,
+		list_t *types, struct swaynag_type *type, char **config, bool *debug) {
+	enum type_options {
+		TO_COLOR_BACKGROUND = 256,
+		TO_COLOR_BORDER,
+		TO_COLOR_BORDER_BOTTOM,
+		TO_COLOR_BUTTON,
+		TO_COLOR_TEXT,
+		TO_THICK_BAR_BORDER,
+		TO_PADDING_MESSAGE,
+		TO_THICK_DET_BORDER,
+		TO_THICK_BTN_BORDER,
+		TO_GAP_BTN,
+		TO_GAP_BTN_DISMISS,
+		TO_MARGIN_BTN_RIGHT,
+		TO_PADDING_BTN,
+	};
+
+	static struct option opts[] = {
+		{"button", required_argument, NULL, 'b'},
+		{"config", required_argument, NULL, 'c'},
+		{"debug", no_argument, NULL, 'd'},
+		{"edge", required_argument, NULL, 'e'},
+		{"font", required_argument, NULL, 'f'},
+		{"help", no_argument, NULL, 'h'},
+		{"detailed-message", no_argument, NULL, 'l'},
+		{"detailed-button", required_argument, NULL, 'L'},
+		{"message", required_argument, NULL, 'm'},
+		{"output", required_argument, NULL, 'o'},
+		{"dismiss-button", required_argument, NULL, 's'},
+		{"type", required_argument, NULL, 't'},
+		{"version", no_argument, NULL, 'v'},
+
+		{"background", required_argument, NULL, TO_COLOR_BACKGROUND},
+		{"border", required_argument, NULL, TO_COLOR_BORDER},
+		{"border-bottom", required_argument, NULL, TO_COLOR_BORDER_BOTTOM},
+		{"button-background", required_argument, NULL, TO_COLOR_BUTTON},
+		{"text", required_argument, NULL, TO_COLOR_TEXT},
+		{"border-bottom-size", required_argument, NULL, TO_THICK_BAR_BORDER},
+		{"message-padding", required_argument, NULL, TO_PADDING_MESSAGE},
+		{"details-border-size", required_argument, NULL, TO_THICK_DET_BORDER},
+		{"button-border-size", required_argument, NULL, TO_THICK_BTN_BORDER},
+		{"button-gap", required_argument, NULL, TO_GAP_BTN},
+		{"button-dismiss-gap", required_argument, NULL, TO_GAP_BTN_DISMISS},
+		{"button-margin-right", required_argument, NULL, TO_MARGIN_BTN_RIGHT},
+		{"button-padding", required_argument, NULL, TO_PADDING_BTN},
+
+		{0, 0, 0, 0}
+	};
+
+	const char *usage =
+		"Usage: swaynag [options...]\n"
+		"\n"
+		"  -b, --button <text> <action>  Create a button with text that "
+			"executes action when pressed. Multiple buttons can be defined.\n"
+		"  -c, --config <path>           Path to config file.\n"
+		"  -d, --debug                   Enable debugging.\n"
+		"  -e, --edge top|bottom         Set the edge to use.\n"
+		"  -f, --font <font>             Set the font to use.\n"
+		"  -h, --help                    Show help message and quit.\n"
+		"  -l, --detailed-message        Read a detailed message from stdin.\n"
+		"  -L, --detailed-button <text>  Set the text of the detail button.\n"
+		"  -m, --message <msg>           Set the message text.\n"
+		"  -o, --output <output>         Set the output to use.\n"
+		"  -s, --dismiss-button <text>   Set the dismiss button text.\n"
+		"  -t, --type <type>             Set the message type.\n"
+		"  -v, --version                 Show the version number and quit.\n"
+		"\n"
+		"The following appearance options can also be given:\n"
+		"  --background RRGGBB[AA]       Background color.\n"
+		"  --border RRGGBB[AA]           Border color.\n"
+		"  --border-bottom RRGGBB[AA]    Bottom border color.\n"
+		"  --button-background RRGGBB[AA]           Button background color.\n"
+		"  --text RRGGBB[AA]             Text color.\n"
+		"  --border-bottom-size size     Thickness of the bar border.\n"
+		"  --message-padding padding     Padding for the message.\n"
+		"  --details-border-size size    Thickness for the details border.\n"
+		"  --button-border-size size     Thickness for the button border.\n"
+		"  --button-gap gap              Size of the gap between buttons\n"
+		"  --button-dismiss-gap gap      Size of the gap for dismiss button.\n"
+		"  --button-margin-right margin  Margin from dismiss button to edge.\n"
+		"  --button-padding padding      Padding for the button text.\n";
+
+	optind = 1;
+	while (1) {
+		int c = getopt_long(argc, argv, "b:c:de:f:hlL:m:o:s:t:v", opts, NULL);
+		if (c == -1) {
+			break;
+		}
+		switch (c) {
+		case 'b': // Button
+			if (swaynag) {
+				if (optind >= argc) {
+					fprintf(stderr, "Missing action for button %s\n", optarg);
+					return EXIT_FAILURE;
+				}
+				struct swaynag_button *button;
+				button = calloc(sizeof(struct swaynag_button), 1);
+				button->text = strdup(optarg);
+				button->type = SWAYNAG_ACTION_COMMAND;
+				button->action = strdup(argv[optind]);
+				list_add(swaynag->buttons, button);
+			}
+			optind++;
+			break;
+		case 'c': // Config
+			if (config) {
+				*config = strdup(optarg);
+			}
+			break;
+		case 'd': // Debug
+			if (debug) {
+				*debug = true;
+			}
+			break;
+		case 'e': // Edge
+			if (type) {
+				if (strcmp(optarg, "top") == 0) {
+					type->anchors = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP
+						| ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT
+						| ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT;
+				} else if (strcmp(optarg, "bottom") == 0) {
+					type->anchors = ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM
+						| ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT
+						| ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT;
+				} else {
+					fprintf(stderr, "Invalid edge: %s\n", optarg);
+					return EXIT_FAILURE;
+				}
+			}
+			break;
+		case 'f': // Font
+			if (type) {
+				free(type->font);
+				type->font = strdup(optarg);
+			}
+			break;
+		case 'l': // Detailed Message
+			if (swaynag) {
+				free(swaynag->details.message);
+				swaynag->details.message = read_from_stdin();
+				swaynag->details.button_up.text = strdup("▲");
+				swaynag->details.button_down.text = strdup("▼");
+			}
+			break;
+		case 'L': // Detailed Button Text
+			if (swaynag) {
+				free(swaynag->details.button_details.text);
+				swaynag->details.button_details.text = strdup(optarg);
+			}
+			break;
+		case 'm': // Message
+			if (swaynag) {
+				free(swaynag->message);
+				swaynag->message = strdup(optarg);
+			}
+			break;
+		case 'o': // Output
+			if (type) {
+				free(type->output);
+				type->output = strdup(optarg);
+			}
+			break;
+		case 's': // Dismiss Button Text
+			if (swaynag) {
+				struct swaynag_button *button_close;
+				button_close = swaynag->buttons->items[0];
+				free(button_close->text);
+				button_close->text = strdup(optarg);
+			}
+			break;
+		case 't': // Type
+			if (swaynag) {
+				swaynag->type = swaynag_type_get(types, optarg);
+				if (!swaynag->type) {
+					fprintf(stderr, "Unknown type %s\n", optarg);
+					return EXIT_FAILURE;
+				}
+			}
+			break;
+		case 'v': // Version
+			fprintf(stdout, "swaynag version " SWAY_VERSION "\n");
+			return -1;
+		case TO_COLOR_BACKGROUND: // Background color
+			if (type) {
+				type->background = parse_color(optarg);
+			}
+			break;
+		case TO_COLOR_BORDER: // Border color
+			if (type) {
+				type->border = parse_color(optarg);
+			}
+			break;
+		case TO_COLOR_BORDER_BOTTOM: // Bottom border color
+			if (type) {
+				type->border_bottom = parse_color(optarg);
+			}
+			break;
+		case TO_COLOR_BUTTON:  // Button background color
+			if (type) {
+				type->button_background = parse_color(optarg);
+			}
+			break;
+		case TO_COLOR_TEXT:  // Text color
+			if (type) {
+				type->text = parse_color(optarg);
+			}
+			break;
+		case TO_THICK_BAR_BORDER:  // Bottom border thickness
+			if (type) {
+				type->bar_border_thickness = strtol(optarg, NULL, 0);
+			}
+			break;
+		case TO_PADDING_MESSAGE:  // Message padding
+			if (type) {
+				type->message_padding = strtol(optarg, NULL, 0);
+			}
+			break;
+		case TO_THICK_DET_BORDER:  // Details border thickness
+			if (type) {
+				type->details_border_thickness = strtol(optarg, NULL, 0);
+			}
+			break;
+		case TO_THICK_BTN_BORDER:  // Button border thickness
+			if (type) {
+				type->button_border_thickness = strtol(optarg, NULL, 0);
+			}
+			break;
+		case TO_GAP_BTN: // Gap between buttons
+			if (type) {
+				type->button_gap = strtol(optarg, NULL, 0);
+			}
+			break;
+		case TO_GAP_BTN_DISMISS:  // Gap between dismiss button
+			if (type) {
+				type->button_gap_close = strtol(optarg, NULL, 0);
+			}
+			break;
+		case TO_MARGIN_BTN_RIGHT:  // Margin on the right side of button area
+			if (type) {
+				type->button_margin_right = strtol(optarg, NULL, 0);
+			}
+			break;
+		case TO_PADDING_BTN:  // Padding for the button text
+			if (type) {
+				type->button_padding = strtol(optarg, NULL, 0);
+			}
+			break;
+		default: // Help or unknown flag
+			fprintf(c == 'h' ? stdout : stderr, "%s", usage);
+			return -1;
+		}
+	}
+
+	return 0;
+}
+
+static bool file_exists(const char *path) {
+	return path && access(path, R_OK) != -1;
+}
+
+char *swaynag_get_config_path(void) {
+	static const char *config_paths[] = {
+		"$HOME/.swaynag/config",
+		"$XDG_CONFIG_HOME/swaynag/config",
+		SYSCONFDIR "/swaynag/config",
+	};
+
+	if (!getenv("XDG_CONFIG_HOME")) {
+		char *home = getenv("HOME");
+		char *config_home = malloc(strlen(home) + strlen("/.config") + 1);
+		if (!config_home) {
+			wlr_log(WLR_ERROR, "Unable to allocate $HOME/.config");
+		} else {
+			strcpy(config_home, home);
+			strcat(config_home, "/.config");
+			setenv("XDG_CONFIG_HOME", config_home, 1);
+			wlr_log(WLR_DEBUG, "Set XDG_CONFIG_HOME to %s", config_home);
+			free(config_home);
+		}
+	}
+
+	wordexp_t p;
+	char *path;
+	for (size_t i = 0; i < sizeof(config_paths) / sizeof(char *); ++i) {
+		if (wordexp(config_paths[i], &p, 0) == 0) {
+			path = strdup(p.we_wordv[0]);
+			wordfree(&p);
+			if (file_exists(path)) {
+				return path;
+			}
+			free(path);
+		}
+	}
+
+	return NULL;
+}
+
+int swaynag_load_config(char *path, struct swaynag *swaynag, list_t *types) {
+	FILE *config = fopen(path, "r");
+	if (!config) {
+		fprintf(stderr, "Failed to read config. Running without it.\n");
+		return 0;
+	}
+
+	struct swaynag_type *type;
+	type = calloc(1, sizeof(struct swaynag_type));
+	type->name = strdup("<config>");
+	list_add(types, type);
+
+	char *line;
+	int line_number = 0;
+	while (!feof(config)) {
+		line = read_line(config);
+		if (!line) {
+			continue;
+		}
+
+		line_number++;
+		if (line[0] == '#') {
+			free(line);
+			continue;
+		}
+		if (strlen(line) == 0) {
+			free(line);
+			continue;
+		}
+
+		if (line[0] == '[') {
+			char *close = strchr(line, ']');
+			if (!close) {
+				free(line);
+				fclose(config);
+				fprintf(stderr, "Closing bracket not found on line %d\n",
+						line_number);
+				return 1;
+			}
+			char *name = calloc(1, close - line);
+			strncat(name, line + 1, close - line - 1);
+			type = swaynag_type_get(types, name);
+			if (!type) {
+				type = calloc(1, sizeof(struct swaynag_type));
+				type->name = strdup(name);
+				list_add(types, type);
+			}
+			free(name);
+		} else {
+			char flag[strlen(line) + 3];
+			sprintf(flag, "--%s", line);
+			char *argv[] = {"swaynag", flag};
+			int result;
+			result = swaynag_parse_options(2, argv, swaynag, types, type,
+					NULL, NULL);
+			if (result != 0) {
+				free(line);
+				fclose(config);
+				return result;
+			}
+		}
+
+		free(line);
+	}
+	fclose(config);
+	return 0;
+}
+
diff --git a/swaynag/main.c b/swaynag/main.c
new file mode 100644
index 00000000..854368e5
--- /dev/null
+++ b/swaynag/main.c
@@ -0,0 +1,129 @@
+#define _XOPEN_SOURCE 500
+#include <signal.h>
+#include "log.h"
+#include "list.h"
+#include "swaynag/config.h"
+#include "swaynag/swaynag.h"
+#include "swaynag/types.h"
+
+static struct swaynag swaynag;
+
+void sig_handler(int signal) {
+	swaynag_destroy(&swaynag);
+	exit(EXIT_FAILURE);
+}
+
+void sway_terminate(int code) {
+	swaynag_destroy(&swaynag);
+	exit(code);
+}
+
+int main(int argc, char **argv) {
+	int exit_code = EXIT_SUCCESS;
+
+	list_t *types = create_list();
+	swaynag_types_add_default(types);
+
+	memset(&swaynag, 0, sizeof(swaynag));
+	swaynag.buttons = create_list();
+
+	struct swaynag_button *button_close =
+		calloc(sizeof(struct swaynag_button), 1);
+	button_close->text = strdup("X");
+	button_close->type = SWAYNAG_ACTION_DISMISS;
+	list_add(swaynag.buttons, button_close);
+
+	swaynag.details.button_details.text = strdup("Toggle Details");
+	swaynag.details.button_details.type = SWAYNAG_ACTION_EXPAND;
+
+
+	char *config_path = NULL;
+	bool debug = false;
+	int launch_status = swaynag_parse_options(argc, argv, NULL, NULL, NULL,
+			&config_path, &debug);
+	if (launch_status != 0)  {
+		exit_code = launch_status;
+		goto cleanup;
+	}
+	wlr_log_init(debug ? WLR_DEBUG : WLR_ERROR, NULL);
+
+	if (!config_path) {
+		config_path = swaynag_get_config_path();
+	}
+	if (config_path) {
+		wlr_log(WLR_DEBUG, "Loading config file: %s", config_path);
+		int config_status = swaynag_load_config(config_path, &swaynag, types);
+		free(config_path);
+		if (config_status != 0) {
+			exit_code = config_status;
+			goto cleanup;
+		}
+	}
+
+	if (argc > 1) {
+		struct swaynag_type *type_args;
+		type_args = calloc(1, sizeof(struct swaynag_type));
+		type_args->name = strdup("<args>");
+		list_add(types, type_args);
+
+		int result = swaynag_parse_options(argc, argv, &swaynag, types,
+				type_args, NULL, NULL);
+		if (result != 0) {
+			exit_code = result;
+			goto cleanup;
+		}
+	}
+
+	if (!swaynag.message) {
+		wlr_log(WLR_ERROR, "No message passed. Please provide --message/-m");
+		exit_code = EXIT_FAILURE;
+		goto cleanup;
+	}
+
+	if (!swaynag.type) {
+		swaynag.type = swaynag_type_get(types, "error");
+	}
+
+	// Construct a new type using the config defaults as base, then merging
+	// config type defaults on top, then merging arguments on top of that, and
+	// finally merging defaults on top.
+	struct swaynag_type *type = calloc(1, sizeof(struct swaynag_type));
+	type->name = strdup(swaynag.type->name);
+	swaynag_type_merge(type, swaynag_type_get(types, "<args>"));
+	swaynag_type_merge(type, swaynag.type);
+	swaynag_type_merge(type, swaynag_type_get(types, "<config>"));
+	swaynag_type_merge(type, swaynag_type_get(types, "<defaults>"));
+	swaynag.type = type;
+
+	swaynag_types_free(types);
+
+	if (swaynag.details.message) {
+		list_add(swaynag.buttons, &swaynag.details.button_details);
+	} else {
+		free(swaynag.details.button_details.text);
+	}
+
+	wlr_log(WLR_DEBUG, "Output: %s", swaynag.type->output);
+	wlr_log(WLR_DEBUG, "Anchors: %d", swaynag.type->anchors);
+	wlr_log(WLR_DEBUG, "Type: %s", swaynag.type->name);
+	wlr_log(WLR_DEBUG, "Message: %s", swaynag.message);
+	wlr_log(WLR_DEBUG, "Font: %s", swaynag.type->font);
+	wlr_log(WLR_DEBUG, "Buttons");
+	for (int i = 0; i < swaynag.buttons->length; i++) {
+		struct swaynag_button *button = swaynag.buttons->items[i];
+		wlr_log(WLR_DEBUG, "\t[%s] `%s`", button->text, button->action);
+	}
+
+	signal(SIGTERM, sig_handler);
+
+	swaynag_setup(&swaynag);
+	swaynag_run(&swaynag);
+	return exit_code;
+
+cleanup:
+	swaynag_types_free(types);
+	free(swaynag.details.button_details.text);
+	swaynag_destroy(&swaynag);
+	return exit_code;
+}
+
diff --git a/swaynag/meson.build b/swaynag/meson.build
new file mode 100644
index 00000000..2ba3ed95
--- /dev/null
+++ b/swaynag/meson.build
@@ -0,0 +1,23 @@
+executable(
+	'swaynag', [
+		'config.c',
+		'main.c',
+		'render.c',
+		'swaynag.c',
+		'types.c',
+	],
+	include_directories: [sway_inc],
+	dependencies: [
+		cairo,
+		client_protos,
+		gdk_pixbuf,
+		math,
+		pango,
+		pangocairo,
+		wayland_client,
+		wayland_cursor,
+		wlroots,
+	],
+	link_with: [lib_sway_common, lib_sway_client],
+	install: true
+)
diff --git a/swaynag/render.c b/swaynag/render.c
new file mode 100644
index 00000000..766409e4
--- /dev/null
+++ b/swaynag/render.c
@@ -0,0 +1,308 @@
+#include <stdint.h>
+#include "cairo.h"
+#include "log.h"
+#include "pango.h"
+#include "pool-buffer.h"
+#include "swaynag/swaynag.h"
+#include "swaynag/types.h"
+#include "wlr-layer-shell-unstable-v1-client-protocol.h"
+
+static uint32_t render_message(cairo_t *cairo, struct swaynag *swaynag) {
+	uint32_t height = swaynag->height * swaynag->scale;
+	height -= swaynag->type->bar_border_thickness * swaynag->scale;
+
+	int text_width, text_height;
+	get_text_size(cairo, swaynag->type->font, &text_width, &text_height,
+			swaynag->scale, true, "%s", swaynag->message);
+
+	int padding = swaynag->type->message_padding * swaynag->scale;
+
+	uint32_t ideal_height = text_height + padding * 2;
+	uint32_t ideal_surface_height = ideal_height / swaynag->scale;
+	if (swaynag->height < ideal_surface_height) {
+		return ideal_surface_height;
+	}
+
+	cairo_set_source_u32(cairo, swaynag->type->text);
+	cairo_move_to(cairo, padding, (int)(ideal_height - text_height) / 2);
+	pango_printf(cairo, swaynag->type->font, swaynag->scale, false,
+			"%s", swaynag->message);
+
+	return ideal_surface_height;
+}
+
+static void render_details_scroll_button(cairo_t *cairo,
+		struct swaynag *swaynag, struct swaynag_button *button) {
+	int text_width, text_height;
+	get_text_size(cairo, swaynag->type->font, &text_width, &text_height,
+			swaynag->scale, true, "%s", button->text);
+
+	int border = swaynag->type->button_border_thickness * swaynag->scale;
+	int padding = swaynag->type->button_padding * swaynag->scale;
+
+	cairo_set_source_u32(cairo, swaynag->type->border);
+	cairo_rectangle(cairo, button->x, button->y,
+			button->width, button->height);
+	cairo_fill(cairo);
+
+	cairo_set_source_u32(cairo, swaynag->type->button_background);
+	cairo_rectangle(cairo, button->x + border, button->y + border,
+			button->width - (border * 2), button->height - (border * 2));
+	cairo_fill(cairo);
+
+	cairo_set_source_u32(cairo, swaynag->type->text);
+	cairo_move_to(cairo, button->x + border + padding,
+			button->y + border + (button->height - text_height) / 2);
+	pango_printf(cairo, swaynag->type->font, swaynag->scale, true,
+			"%s", button->text);
+}
+
+static int get_detailed_scroll_button_width(cairo_t *cairo,
+		struct swaynag *swaynag) {
+	int up_width, down_width, temp_height;
+	get_text_size(cairo, swaynag->type->font, &up_width, &temp_height,
+			swaynag->scale, true,
+			"%s", swaynag->details.button_up.text);
+	get_text_size(cairo, swaynag->type->font, &down_width, &temp_height,
+			swaynag->scale, true,
+			"%s", swaynag->details.button_down.text);
+
+	int text_width =  up_width > down_width ? up_width : down_width;
+	int border = swaynag->type->button_border_thickness * swaynag->scale;
+	int padding = swaynag->type->button_padding * swaynag->scale;
+
+	return text_width + border * 2 + padding * 2;
+}
+
+static uint32_t render_detailed(cairo_t *cairo, struct swaynag *swaynag,
+		uint32_t y) {
+	uint32_t width = swaynag->width * swaynag->scale;
+	uint32_t height = swaynag->height * swaynag->scale;
+	height -= swaynag->type->bar_border_thickness * swaynag->scale;
+
+	int border = swaynag->type->details_border_thickness * swaynag->scale;
+	int padding = swaynag->type->message_padding * swaynag->scale;
+	int decor = padding + border;
+
+	swaynag->details.x = decor;
+	swaynag->details.y = y * swaynag->scale + decor;
+	swaynag->details.width = width - decor * 2;
+
+	PangoLayout *layout = get_pango_layout(cairo, swaynag->type->font,
+			swaynag->details.message, swaynag->scale, false);
+	pango_layout_set_width(layout,
+			(swaynag->details.width - padding * 2) * PANGO_SCALE);
+	pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR);
+	pango_layout_set_single_paragraph_mode(layout, false);
+	pango_cairo_update_layout(cairo, layout);
+	swaynag->details.total_lines = pango_layout_get_line_count(layout);
+
+	PangoLayoutLine *line;
+	line = pango_layout_get_line_readonly(layout, swaynag->details.offset);
+	gint offset = line->start_index;
+	const char *text = pango_layout_get_text(layout);
+	pango_layout_set_text(layout, text + offset, strlen(text) - offset);
+
+	int text_width, text_height;
+	pango_cairo_update_layout(cairo, layout);
+	pango_layout_get_pixel_size(layout, &text_width, &text_height);
+
+	bool show_buttons = swaynag->details.offset > 0;
+	int button_width = get_detailed_scroll_button_width(cairo, swaynag);
+	if (show_buttons) {
+		swaynag->details.width -= button_width;
+		pango_layout_set_width(layout,
+				(swaynag->details.width - padding * 2) * PANGO_SCALE);
+	}
+
+	uint32_t ideal_height;
+	do {
+		ideal_height = swaynag->details.y + text_height + decor + padding * 2;
+		if (ideal_height > SWAYNAG_MAX_HEIGHT) {
+			ideal_height = SWAYNAG_MAX_HEIGHT;
+
+			if (!show_buttons) {
+				show_buttons = true;
+				swaynag->details.width -= button_width;
+				pango_layout_set_width(layout,
+						(swaynag->details.width - padding * 2) * PANGO_SCALE);
+			}
+		}
+
+		swaynag->details.height = ideal_height - swaynag->details.y - decor;
+		pango_layout_set_height(layout,
+				(swaynag->details.height - padding * 2) * PANGO_SCALE);
+		pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END);
+		pango_cairo_update_layout(cairo, layout);
+		pango_layout_get_pixel_size(layout, &text_width, &text_height);
+	} while (text_height != (swaynag->details.height - padding * 2));
+
+	swaynag->details.visible_lines = pango_layout_get_line_count(layout);
+
+	if (show_buttons) {
+		swaynag->details.button_up.x =
+			swaynag->details.x + swaynag->details.width;
+		swaynag->details.button_up.y = swaynag->details.y;
+		swaynag->details.button_up.width = button_width;
+		swaynag->details.button_up.height = swaynag->details.height / 2;
+		render_details_scroll_button(cairo, swaynag,
+				&swaynag->details.button_up);
+
+		swaynag->details.button_down.x =
+			swaynag->details.x + swaynag->details.width;
+		swaynag->details.button_down.y =
+			swaynag->details.button_up.y + swaynag->details.button_up.height;
+		swaynag->details.button_down.width = button_width;
+		swaynag->details.button_down.height = swaynag->details.height / 2;
+		render_details_scroll_button(cairo, swaynag,
+				&swaynag->details.button_down);
+	}
+
+	cairo_set_source_u32(cairo, swaynag->type->border);
+	cairo_rectangle(cairo, swaynag->details.x, swaynag->details.y,
+			swaynag->details.width, swaynag->details.height);
+	cairo_fill(cairo);
+
+	cairo_move_to(cairo, swaynag->details.x + padding,
+			swaynag->details.y + padding);
+	cairo_set_source_u32(cairo, swaynag->type->text);
+	pango_cairo_show_layout(cairo, layout);
+	g_object_unref(layout);
+
+	return ideal_height / swaynag->scale;
+}
+
+static uint32_t render_button(cairo_t *cairo, struct swaynag *swaynag,
+		int button_index, int *x) {
+	uint32_t height = swaynag->height * swaynag->scale;
+	height -= swaynag->type->bar_border_thickness * swaynag->scale;
+	struct swaynag_button *button = swaynag->buttons->items[button_index];
+
+	int text_width, text_height;
+	get_text_size(cairo, swaynag->type->font, &text_width, &text_height,
+			swaynag->scale, true, "%s", button->text);
+
+	int border = swaynag->type->button_border_thickness * swaynag->scale;
+	int padding = swaynag->type->button_padding * swaynag->scale;
+
+	uint32_t ideal_height = text_height + padding * 2 + border * 2;
+	uint32_t ideal_surface_height = ideal_height / swaynag->scale;
+	if (swaynag->height < ideal_surface_height) {
+		return ideal_surface_height;
+	}
+
+	button->x = *x - border - text_width - padding * 2;
+	button->y = (int)(ideal_height - text_height) / 2 - padding;
+	button->width = text_width + padding * 2;
+	button->height = text_height + padding * 2;
+
+	cairo_set_source_u32(cairo, swaynag->type->border);
+	cairo_rectangle(cairo, button->x - border, button->y - border,
+			button->width + border * 2, button->height + border * 2);
+	cairo_fill(cairo);
+
+	cairo_set_source_u32(cairo, swaynag->type->button_background);
+	cairo_rectangle(cairo, button->x, button->y,
+			button->width, button->height);
+	cairo_fill(cairo);
+
+	cairo_set_source_u32(cairo, swaynag->type->text);
+	cairo_move_to(cairo, button->x + padding, button->y + padding);
+	pango_printf(cairo, swaynag->type->font, swaynag->scale, true,
+			"%s", button->text);
+
+	*x = button->x - border;
+
+	return ideal_surface_height;
+}
+
+static uint32_t render_to_cairo(cairo_t *cairo, struct swaynag *swaynag) {
+	uint32_t max_height = 0;
+
+	cairo_set_operator(cairo, CAIRO_OPERATOR_SOURCE);
+	cairo_set_source_u32(cairo, swaynag->type->background);
+	cairo_paint(cairo);
+
+	uint32_t h = render_message(cairo, swaynag);
+	max_height = h > max_height ? h : max_height;
+
+	int x = swaynag->width - swaynag->type->button_margin_right;
+	x *= swaynag->scale;
+	for (int i = 0; i < swaynag->buttons->length; i++) {
+		h = render_button(cairo, swaynag, i, &x);
+		max_height = h > max_height ? h : max_height;
+		x -= swaynag->type->button_gap * swaynag->scale;
+		if (i == 0) {
+			x -= swaynag->type->button_gap_close * swaynag->scale;
+		}
+	}
+
+	if (swaynag->details.visible) {
+		h = render_detailed(cairo, swaynag, max_height);
+		max_height = h > max_height ? h : max_height;
+	}
+
+	int border = swaynag->type->bar_border_thickness * swaynag->scale;
+	if (max_height > swaynag->height) {
+		max_height += border;
+	}
+	cairo_set_source_u32(cairo, swaynag->type->border_bottom);
+	cairo_rectangle(cairo, 0,
+			swaynag->height * swaynag->scale - border,
+			swaynag->width * swaynag->scale,
+			border);
+	cairo_fill(cairo);
+
+	return max_height;
+}
+
+void render_frame(struct swaynag *swaynag) {
+	if (!swaynag->run_display) {
+		return;
+	}
+
+	cairo_surface_t *recorder = cairo_recording_surface_create(
+			CAIRO_CONTENT_COLOR_ALPHA, NULL);
+	cairo_t *cairo = cairo_create(recorder);
+	cairo_save(cairo);
+	cairo_set_operator(cairo, CAIRO_OPERATOR_CLEAR);
+	cairo_paint(cairo);
+	cairo_restore(cairo);
+	uint32_t height = render_to_cairo(cairo, swaynag);
+	if (height != swaynag->height) {
+		zwlr_layer_surface_v1_set_size(swaynag->layer_surface, 0, height);
+		zwlr_layer_surface_v1_set_exclusive_zone(swaynag->layer_surface,
+				height);
+		wl_surface_commit(swaynag->surface);
+		wl_display_roundtrip(swaynag->display);
+	} else {
+		swaynag->current_buffer = get_next_buffer(swaynag->shm,
+				swaynag->buffers,
+				swaynag->width * swaynag->scale,
+				swaynag->height * swaynag->scale);
+		if (!swaynag->current_buffer) {
+			wlr_log(WLR_DEBUG, "Failed to get buffer. Skipping frame.");
+			goto cleanup;
+		}
+
+		cairo_t *shm = swaynag->current_buffer->cairo;
+		cairo_save(shm);
+		cairo_set_operator(shm, CAIRO_OPERATOR_CLEAR);
+		cairo_paint(shm);
+		cairo_restore(shm);
+		cairo_set_source_surface(shm, recorder, 0.0, 0.0);
+		cairo_paint(shm);
+
+		wl_surface_set_buffer_scale(swaynag->surface, swaynag->scale);
+		wl_surface_attach(swaynag->surface,
+				swaynag->current_buffer->buffer, 0, 0);
+		wl_surface_damage(swaynag->surface, 0, 0,
+				swaynag->width, swaynag->height);
+		wl_surface_commit(swaynag->surface);
+		wl_display_roundtrip(swaynag->display);
+	}
+
+cleanup:
+	cairo_surface_destroy(recorder);
+	cairo_destroy(cairo);
+}
diff --git a/swaynag/swaynag.1.scd b/swaynag/swaynag.1.scd
new file mode 100644
index 00000000..1c395aee
--- /dev/null
+++ b/swaynag/swaynag.1.scd
@@ -0,0 +1,106 @@
+swaynag(1)
+
+# NAME
+
+swaynag - Show a warning or error message with buttons
+
+# SYNOPSIS
+
+_swaynag_ [options...]
+
+# OPTIONS
+
+*-b, --button* <text> <action>
+	Create a button with the text _text_ that executes _action_ when pressed.
+	Multiple buttons can be defined by providing the flag multiple times.
+
+*-c, --config* <path>
+	The config file to use. By default, the following paths are checked:
+	_$HOME/.swaynag/config_, _$XDG\_CONFIG\_HOME/swaynag/config_, and
+	_SYSCONFDIR/swaynag/config_. All flags aside from this one and _debug_ are
+	valid options in the configuration file using the format
+	_long-option=value_. All leading dashes should be omitted and the equals
+	sign is required. See swaynag(5) for more information.
+
+*-d, --debug*
+	Enable debugging.
+
+*-e, --edge* top|bottom
+	Set the edge to use.
+
+*-f, --font* <font>
+	Set the font to use.
+
+*-h, --help*
+	Show help message and quit.
+
+*-l, --detailed-message*
+	Read a detailed message from stdin. A button to toggle details will be
+	added. Details are shown in a scrollable multi-line text area.
+
+*-L, --detailed-button* <text>
+	Set the text for the button that toggles details. This has no effect if
+	there is not a detailed message. The default is _Toggle Details_.
+
+*-m, --message* <msg>
+	Set the message text.
+
+*-o, --output* <output>
+	Set the output to use. This should be the name of a _xdg\_output_.
+
+*-s, --dismiss-button* <text>
+	Sets the text for the dismiss nagbar button. The default is _X_.
+
+*-t, --type* <type>
+	Set the message type. Two types are created by default _error_ and
+	_warning_. Custom types can be defined in the config file. See
+	_--config_ and swaynag(5) for details. Both of the default types can be
+	overridden in the config file as well.
+
+*-v, --version*
+	Show the version number and quit.
+
+# APPEARANCE OPTIONS
+
+*--background* <RRGGBB[AA]>
+	Set the color of the background.
+
+*--border* <RRGGBB[AA]>
+	Set the color of the border.
+
+*--border-bottom* <RRGGBB[AA]>
+	Set the color of the bottom border.
+
+*--button-background* <RRGGBB[AA]>
+	Set the color for the background for buttons.
+
+*--text* <RRGGBB[AA]>
+	Set the text color.
+
+*--border-bottom-size* <size>
+	Set the thickness of the bottom border.
+
+*--message-padding* <padding>
+	Set the padding for the message.
+
+*--details-border-size* <size>
+	Set the thickness for the details border.
+
+*--button-border-size* <size>
+	Set the thickness for the button border.
+
+*--button-gap* <gap>
+	Set the size of the gap between buttons.
+
+*--button-dismiss-gap* <gap>
+	Set the size of the gap between the dismiss button and another button.
+
+*--button-margin-right* <margin>
+	Set the margin from the right of the dismiss button to edge.
+
+*--button-padding* <padding>
+	Set the padding for the button text.
+
+# SEE
+
+swaynag(5)
diff --git a/swaynag/swaynag.5.scd b/swaynag/swaynag.5.scd
new file mode 100644
index 00000000..d3daadf7
--- /dev/null
+++ b/swaynag/swaynag.5.scd
@@ -0,0 +1,100 @@
+swaynag(5)
+
+# NAME
+
+swaynag - swaynag configuration file
+
+# SYNOPSIS
+
+$HOME/.swaynag/config, $XDG\_CONFIG\_HOME/swaynag/config,
+SYSCONFDIR/swaynag/config
+
+# CONFIG FILE
+
+At the top of the config file, _swaynag_ options can be set using the format
+_long-option=value_. These will be used as default values if _swaynag_ is not
+given the option. This can be useful for setting a preferred font, output, and
+edge.
+
+Below the options, custom types may be defined. To define a type, use the
+following format:
+
+```
+[name-of-type]
+option=value
+```
+
+All colors may be given in the form _RRGGBB_ or _RRGGBBAA_. The following
+colors can be set:
+
+*background=<color>*
+	The background color for _swaynag_.
+
+*border=<color>*
+	The color to use for borders of buttons.
+
+*border-bottom=<color>*
+	The color of the border line at the bottom of _swaynag_.
+
+*button-background=<color>*
+	The background color for the buttons.
+
+*text=<color>*
+	The color of the text.
+
+The following sizing options can also be set:
+
+*border-bottom-size=<size>*
+	Set the thickness of the bottom border.
+
+*message-padding=<padding>*
+	Set the padding for the message.
+
+*details-border-size=<size>*
+	Set the thickness for the details border.
+
+*button-border-size=<size>*
+	Set the thickness for the button border.
+
+*button-gap=<gap>*
+	Set the size of the gap between buttons.
+
+*button-dismiss-gap=<gap>*
+	Set the size of the gap between the dismiss button and another button.
+
+*button-margin-right=<margin>*
+	Set the margin from the right of the dismiss button to edge.
+
+*button-padding=<padding>*
+	Set the padding for the button text.
+
+Additionally, the following options can be assigned a default per-type:
+
+*edge=top|bottom*
+	Set the edge to use.
+
+*font=<font>*
+	Set the font to use.
+
+*output=<output>*
+	Set the output to use. This should be the name of a _xdg\_output_.
+
+# EXAMPLE
+
+```
+font=Monospace 12
+edge=bottom
+
+[green]
+edge=top
+background=00AA00
+border=006600
+border-bottom=004400
+text=FFFFFF
+button-background=00CC00
+message-padding=10
+```
+
+# SEE
+
+swaynag(1)
diff --git a/swaynag/swaynag.c b/swaynag/swaynag.c
new file mode 100644
index 00000000..3966277d
--- /dev/null
+++ b/swaynag/swaynag.c
@@ -0,0 +1,451 @@
+#define _XOPEN_SOURCE 500
+#include <assert.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <wayland-client.h>
+#include <wayland-cursor.h>
+#include "log.h"
+#include "list.h"
+#include "swaynag/render.h"
+#include "swaynag/swaynag.h"
+#include "swaynag/types.h"
+#include "wlr-layer-shell-unstable-v1-client-protocol.h"
+
+static void nop() {
+	// Intentionally left blank
+}
+
+static bool terminal_execute(char *terminal, char *command) {
+	char fname[] = "/tmp/swaynagXXXXXX";
+	FILE *tmp= fdopen(mkstemp(fname), "w");
+	if (!tmp) {
+		wlr_log(WLR_ERROR, "Failed to create temp script");
+		return false;
+	}
+	wlr_log(WLR_DEBUG, "Created temp script: %s", fname);
+	fprintf(tmp, "#!/bin/sh\nrm %s\n%s", fname, command);
+	fclose(tmp);
+	chmod(fname, S_IRUSR | S_IWUSR | S_IXUSR);
+	char cmd[strlen(terminal) + strlen(" -e ") + strlen(fname) + 1];
+	sprintf(cmd, "%s -e %s", terminal, fname);
+	execl("/bin/sh", "/bin/sh", "-c", cmd, NULL);
+	return true;
+}
+
+static void swaynag_button_execute(struct swaynag *swaynag,
+		struct swaynag_button *button) {
+	wlr_log(WLR_DEBUG, "Executing [%s]: %s", button->text, button->action);
+	if (button->type == SWAYNAG_ACTION_DISMISS) {
+		swaynag->run_display = false;
+	} else if (button->type == SWAYNAG_ACTION_EXPAND) {
+		swaynag->details.visible = !swaynag->details.visible;
+		render_frame(swaynag);
+	} else {
+		if (fork() == 0) {
+			// Child process. Will be used to prevent zombie processes
+			setsid();
+			if (fork() == 0) {
+				// Child of the child. Will be reparented to the init process
+				char *terminal = getenv("TERMINAL");
+				if (terminal && strlen(terminal)) {
+					wlr_log(WLR_DEBUG, "Found $TERMINAL: %s", terminal);
+					if (!terminal_execute(terminal, button->action)) {
+						swaynag_destroy(swaynag);
+						exit(EXIT_FAILURE);
+					}
+				} else {
+					wlr_log(WLR_DEBUG, "$TERMINAL not found. Running directly");
+					execl("/bin/sh", "/bin/sh", "-c", button->action, NULL);
+				}
+			}
+			exit(EXIT_SUCCESS);
+		}
+	}
+	wait(0);
+}
+
+static void layer_surface_configure(void *data,
+		struct zwlr_layer_surface_v1 *surface,
+		uint32_t serial, uint32_t width, uint32_t height) {
+	struct swaynag *swaynag = data;
+	swaynag->width = width;
+	swaynag->height = height;
+	zwlr_layer_surface_v1_ack_configure(surface, serial);
+	render_frame(swaynag);
+}
+
+static void layer_surface_closed(void *data,
+		struct zwlr_layer_surface_v1 *surface) {
+	struct swaynag *swaynag = data;
+	swaynag_destroy(swaynag);
+}
+
+static struct zwlr_layer_surface_v1_listener layer_surface_listener = {
+	.configure = layer_surface_configure,
+	.closed = layer_surface_closed,
+};
+
+static void surface_enter(void *data, struct wl_surface *surface,
+		struct wl_output *output) {
+	struct swaynag *swaynag = data;
+	struct swaynag_output *swaynag_output;
+	wl_list_for_each(swaynag_output, &swaynag->outputs, link) {
+		if (swaynag_output->wl_output == output) {
+			wlr_log(WLR_DEBUG, "Surface enter on output %s",
+					swaynag_output->name);
+			swaynag->output = swaynag_output;
+			swaynag->scale = swaynag->output->scale;
+			render_frame(swaynag);
+			break;
+		}
+	};
+}
+
+static struct wl_surface_listener surface_listener = {
+	.enter = surface_enter,
+	.leave = nop,
+};
+
+static void update_cursor(struct swaynag *swaynag) {
+	struct swaynag_pointer *pointer = &swaynag->pointer;
+	pointer->cursor_theme = wl_cursor_theme_load(NULL, 24 * swaynag->scale,
+			swaynag->shm);
+	struct wl_cursor *cursor =
+		wl_cursor_theme_get_cursor(pointer->cursor_theme, "left_ptr");
+	pointer->cursor_image = cursor->images[0];
+	wl_surface_set_buffer_scale(pointer->cursor_surface,
+			swaynag->scale);
+	wl_surface_attach(pointer->cursor_surface,
+			wl_cursor_image_get_buffer(pointer->cursor_image), 0, 0);
+	wl_pointer_set_cursor(pointer->pointer, pointer->serial,
+			pointer->cursor_surface,
+			pointer->cursor_image->hotspot_x / swaynag->scale,
+			pointer->cursor_image->hotspot_y / swaynag->scale);
+	wl_surface_commit(pointer->cursor_surface);
+}
+
+static void wl_pointer_enter(void *data, struct wl_pointer *wl_pointer,
+		uint32_t serial, struct wl_surface *surface,
+		wl_fixed_t surface_x, wl_fixed_t surface_y) {
+	struct swaynag *swaynag = data;
+	struct swaynag_pointer *pointer = &swaynag->pointer;
+	pointer->serial = serial;
+	update_cursor(swaynag);
+}
+
+static void wl_pointer_motion(void *data, struct wl_pointer *wl_pointer,
+		uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) {
+	struct swaynag *swaynag = data;
+	swaynag->pointer.x = wl_fixed_to_int(surface_x);
+	swaynag->pointer.y = wl_fixed_to_int(surface_y);
+}
+
+static void wl_pointer_button(void *data, struct wl_pointer *wl_pointer,
+		uint32_t serial, uint32_t time, uint32_t button, uint32_t state) {
+	struct swaynag *swaynag = data;
+
+	if (state != WL_POINTER_BUTTON_STATE_PRESSED) {
+		return;
+	}
+
+	double x = swaynag->pointer.x * swaynag->scale;
+	double y = swaynag->pointer.y * swaynag->scale;
+	for (int i = 0; i < swaynag->buttons->length; i++) {
+		struct swaynag_button *nagbutton = swaynag->buttons->items[i];
+		if (x >= nagbutton->x
+				&& y >= nagbutton->y
+				&& x < nagbutton->x + nagbutton->width
+				&& y < nagbutton->y + nagbutton->height) {
+			swaynag_button_execute(swaynag, nagbutton);
+			return;
+		}
+	}
+
+	if (swaynag->details.visible &&
+			swaynag->details.total_lines != swaynag->details.visible_lines) {
+		struct swaynag_button button_up = swaynag->details.button_up;
+		if (x >= button_up.x
+				&& y >= button_up.y
+				&& x < button_up.x + button_up.width
+				&& y < button_up.y + button_up.height
+				&& swaynag->details.offset > 0) {
+			swaynag->details.offset--;
+			render_frame(swaynag);
+			return;
+		}
+
+		struct swaynag_button button_down = swaynag->details.button_down;
+		int bot = swaynag->details.total_lines;
+		bot -= swaynag->details.visible_lines;
+		if (x >= button_down.x
+				&& y >= button_down.y
+				&& x < button_down.x + button_down.width
+				&& y < button_down.y + button_down.height
+				&& swaynag->details.offset < bot) {
+			swaynag->details.offset++;
+			render_frame(swaynag);
+			return;
+		}
+	}
+}
+
+static void wl_pointer_axis(void *data, struct wl_pointer *wl_pointer,
+		uint32_t time, uint32_t axis, wl_fixed_t value) {
+	struct swaynag *swaynag = data;
+	if (!swaynag->details.visible
+			|| swaynag->pointer.x < swaynag->details.x
+			|| swaynag->pointer.y < swaynag->details.y
+			|| swaynag->pointer.x >= swaynag->details.x + swaynag->details.width
+			|| swaynag->pointer.y >= swaynag->details.y + swaynag->details.height
+			|| swaynag->details.total_lines == swaynag->details.visible_lines) {
+		return;
+	}
+
+	int direction = wl_fixed_to_int(value);
+	int bot = swaynag->details.total_lines - swaynag->details.visible_lines;
+	if (direction < 0 && swaynag->details.offset > 0) {
+		swaynag->details.offset--;
+	} else if (direction > 0 && swaynag->details.offset < bot) {
+		swaynag->details.offset++;
+	}
+
+	render_frame(swaynag);
+}
+
+static struct wl_pointer_listener pointer_listener = {
+	.enter = wl_pointer_enter,
+	.leave = nop,
+	.motion = wl_pointer_motion,
+	.button = wl_pointer_button,
+	.axis = wl_pointer_axis,
+	.frame = nop,
+	.axis_source = nop,
+	.axis_stop = nop,
+	.axis_discrete = nop,
+};
+
+static void seat_handle_capabilities(void *data, struct wl_seat *wl_seat,
+		enum wl_seat_capability caps) {
+	struct swaynag *swaynag = data;
+	if ((caps & WL_SEAT_CAPABILITY_POINTER)) {
+		swaynag->pointer.pointer = wl_seat_get_pointer(wl_seat);
+		wl_pointer_add_listener(swaynag->pointer.pointer, &pointer_listener,
+				swaynag);
+	}
+}
+
+const struct wl_seat_listener seat_listener = {
+	.capabilities = seat_handle_capabilities,
+	.name = nop,
+};
+
+static void output_scale(void *data, struct wl_output *output,
+		int32_t factor) {
+	struct swaynag_output *swaynag_output = data;
+	swaynag_output->scale = factor;
+	if (swaynag_output->swaynag->output == swaynag_output) {
+		swaynag_output->swaynag->scale = swaynag_output->scale;
+		update_cursor(swaynag_output->swaynag);
+		render_frame(swaynag_output->swaynag);
+	}
+}
+
+static struct wl_output_listener output_listener = {
+	.geometry = nop,
+	.mode = nop,
+	.done = nop,
+	.scale = output_scale,
+};
+
+static void xdg_output_handle_name(void *data,
+		struct zxdg_output_v1 *xdg_output, const char *name) {
+	struct swaynag_output *swaynag_output = data;
+	char *outname = swaynag_output->swaynag->type->output;
+	wlr_log(WLR_DEBUG, "Checking against output %s for %s", name, outname);
+	if (!swaynag_output->swaynag->output && outname && name
+			&& strcmp(outname, name) == 0) {
+		wlr_log(WLR_DEBUG, "Using output %s", name);
+		swaynag_output->swaynag->output = swaynag_output;
+	}
+	swaynag_output->name = strdup(name);
+	zxdg_output_v1_destroy(xdg_output);
+	swaynag_output->swaynag->querying_outputs--;
+}
+
+static struct zxdg_output_v1_listener xdg_output_listener = {
+	.logical_position = nop,
+	.logical_size = nop,
+	.done = nop,
+	.name = xdg_output_handle_name,
+	.description = nop,
+};
+
+static void handle_global(void *data, struct wl_registry *registry,
+		uint32_t name, const char *interface, uint32_t version) {
+	struct swaynag *swaynag = data;
+	if (strcmp(interface, wl_compositor_interface.name) == 0) {
+		swaynag->compositor = wl_registry_bind(registry, name,
+				&wl_compositor_interface, 3);
+	} else if (strcmp(interface, wl_seat_interface.name) == 0) {
+		swaynag->seat = wl_registry_bind(registry, name, &wl_seat_interface, 1);
+		wl_seat_add_listener(swaynag->seat, &seat_listener, swaynag);
+	} else if (strcmp(interface, wl_shm_interface.name) == 0) {
+		swaynag->shm = wl_registry_bind(registry, name, &wl_shm_interface, 1);
+	} else if (strcmp(interface, wl_output_interface.name) == 0) {
+		if (!swaynag->output && swaynag->xdg_output_manager) {
+			swaynag->querying_outputs++;
+			struct swaynag_output *output =
+				calloc(1, sizeof(struct swaynag_output));
+			output->wl_output = wl_registry_bind(registry, name,
+					&wl_output_interface, 3);
+			output->wl_name = name;
+			output->scale = 1;
+			output->swaynag = swaynag;
+			wl_list_insert(&swaynag->outputs, &output->link);
+			wl_output_add_listener(output->wl_output,
+					&output_listener, output);
+
+			struct zxdg_output_v1 *xdg_output;
+			xdg_output = zxdg_output_manager_v1_get_xdg_output(
+					swaynag->xdg_output_manager, output->wl_output);
+			zxdg_output_v1_add_listener(xdg_output,
+					&xdg_output_listener, output);
+		}
+	} else if (strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) {
+		swaynag->layer_shell = wl_registry_bind(
+				registry, name, &zwlr_layer_shell_v1_interface, 1);
+	} else if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0
+			&& version >= ZXDG_OUTPUT_V1_NAME_SINCE_VERSION) {
+		swaynag->xdg_output_manager = wl_registry_bind(registry, name,
+				&zxdg_output_manager_v1_interface,
+				ZXDG_OUTPUT_V1_NAME_SINCE_VERSION);
+	}
+}
+
+static void handle_global_remove(void *data, struct wl_registry *registry,
+		uint32_t name) {
+	struct swaynag *swaynag = data;
+	if (swaynag->output->wl_name == name) {
+		swaynag->run_display = false;
+	}
+}
+
+static const struct wl_registry_listener registry_listener = {
+	.global = handle_global,
+	.global_remove = handle_global_remove,
+};
+
+void swaynag_setup(struct swaynag *swaynag) {
+	swaynag->display = wl_display_connect(NULL);
+	assert(swaynag->display);
+
+	swaynag->scale = 1;
+	wl_list_init(&swaynag->outputs);
+
+	struct wl_registry *registry = wl_display_get_registry(swaynag->display);
+	wl_registry_add_listener(registry, &registry_listener, swaynag);
+	wl_display_roundtrip(swaynag->display);
+	assert(swaynag->compositor && swaynag->layer_shell && swaynag->shm);
+
+	while (swaynag->querying_outputs > 0) {
+		wl_display_roundtrip(swaynag->display);
+	}
+
+	if (!swaynag->output && swaynag->type->output) {
+		wlr_log(WLR_ERROR, "Output '%s' not found", swaynag->type->output);
+		swaynag_destroy(swaynag);
+		exit(EXIT_FAILURE);
+	}
+
+	struct swaynag_pointer *pointer = &swaynag->pointer;
+	pointer->cursor_surface = wl_compositor_create_surface(swaynag->compositor);
+	assert(pointer->cursor_surface);
+
+	swaynag->surface = wl_compositor_create_surface(swaynag->compositor);
+	assert(swaynag->surface);
+	wl_surface_add_listener(swaynag->surface, &surface_listener, swaynag);
+
+	swaynag->layer_surface = zwlr_layer_shell_v1_get_layer_surface(
+			swaynag->layer_shell, swaynag->surface,
+			swaynag->output ? swaynag->output->wl_output : NULL,
+			ZWLR_LAYER_SHELL_V1_LAYER_TOP, "swaynag");
+	assert(swaynag->layer_surface);
+	zwlr_layer_surface_v1_add_listener(swaynag->layer_surface,
+			&layer_surface_listener, swaynag);
+	zwlr_layer_surface_v1_set_anchor(swaynag->layer_surface,
+			swaynag->type->anchors);
+
+	wl_registry_destroy(registry);
+}
+
+void swaynag_run(struct swaynag *swaynag) {
+	swaynag->run_display = true;
+	render_frame(swaynag);
+	while (swaynag->run_display
+			&& wl_display_dispatch(swaynag->display) != -1) {
+		// This is intentionally left blank
+	}
+}
+
+void swaynag_destroy(struct swaynag *swaynag) {
+	swaynag->run_display = false;
+
+	free(swaynag->message);
+	while (swaynag->buttons->length) {
+		struct swaynag_button *button = swaynag->buttons->items[0];
+		list_del(swaynag->buttons, 0);
+		free(button->text);
+		free(button->action);
+		free(button);
+	}
+	list_free(swaynag->buttons);
+	free(swaynag->details.message);
+	free(swaynag->details.button_up.text);
+	free(swaynag->details.button_down.text);
+
+	if (swaynag->type) {
+		swaynag_type_free(swaynag->type);
+	}
+
+	if (swaynag->layer_surface) {
+		zwlr_layer_surface_v1_destroy(swaynag->layer_surface);
+	}
+
+	if (swaynag->surface) {
+		wl_surface_destroy(swaynag->surface);
+	}
+
+	if (swaynag->pointer.cursor_theme) {
+		wl_cursor_theme_destroy(swaynag->pointer.cursor_theme);
+	}
+
+	if (&swaynag->buffers[0]) {
+		destroy_buffer(&swaynag->buffers[0]);
+	}
+
+	if (&swaynag->buffers[1]) {
+		destroy_buffer(&swaynag->buffers[1]);
+	}
+
+	if (swaynag->outputs.prev || swaynag->outputs.next) {
+		struct swaynag_output *output, *temp;
+		wl_list_for_each_safe(output, temp, &swaynag->outputs, link) {
+			wl_output_destroy(output->wl_output);
+			free(output->name);
+			wl_list_remove(&output->link);
+			free(output);
+		};
+	}
+
+	if (swaynag->compositor) {
+		wl_compositor_destroy(swaynag->compositor);
+	}
+
+	if (swaynag->shm) {
+		wl_shm_destroy(swaynag->shm);
+	}
+
+	if (swaynag->display) {
+		wl_display_disconnect(swaynag->display);
+	}
+}
diff --git a/swaynag/types.c b/swaynag/types.c
new file mode 100644
index 00000000..1e0a138b
--- /dev/null
+++ b/swaynag/types.c
@@ -0,0 +1,156 @@
+#define _XOPEN_SOURCE 500
+#include <getopt.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <strings.h>
+#include "list.h"
+#include "swaynag/config.h"
+#include "swaynag/types.h"
+#include "util.h"
+#include "wlr-layer-shell-unstable-v1-client-protocol.h"
+
+void swaynag_types_add_default(list_t *types) {
+	struct swaynag_type *type_defaults;
+	type_defaults = calloc(1, sizeof(struct swaynag_type));
+	type_defaults->name = strdup("<defaults>");
+	type_defaults->font = strdup("pango:Monospace 10");
+	type_defaults->anchors = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP
+		| ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT
+		| ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT;
+	type_defaults->button_background = 0x333333FF;
+	type_defaults->background = 0x323232FF;
+	type_defaults->text = 0xFFFFFFFF;
+	type_defaults->border = 0x222222FF;
+	type_defaults->border_bottom = 0x444444FF;
+	type_defaults->bar_border_thickness = 2;
+	type_defaults->message_padding = 8;
+	type_defaults->details_border_thickness = 3;
+	type_defaults->button_border_thickness = 3;
+	type_defaults->button_gap = 20;
+	type_defaults->button_gap_close = 15;
+	type_defaults->button_margin_right = 2;
+	type_defaults->button_padding = 3;
+	list_add(types, type_defaults);
+
+	struct swaynag_type *type_error;
+	type_error = calloc(1, sizeof(struct swaynag_type));
+	type_error->button_background = 0x680A0AFF;
+	type_error->background = 0x900000FF;
+	type_error->text = 0xFFFFFFFF;
+	type_error->border = 0xD92424FF;
+	type_error->border_bottom = 0x470909FF;
+	type_error->name = strdup("error");
+	list_add(types, type_error);
+
+	struct swaynag_type *type_warning;
+	type_warning = calloc(1, sizeof(struct swaynag_type));
+	type_warning->name = strdup("warning");
+	type_warning->button_background = 0xFFC100FF;
+	type_warning->background = 0xFFA800FF;
+	type_warning->text = 0x000000FF;
+	type_warning->border = 0xAB7100FF;
+	type_warning->border_bottom = 0xAB7100FF;
+	list_add(types, type_warning);
+}
+
+struct swaynag_type *swaynag_type_get(list_t *types, char *name) {
+	for (int i = 0; i < types->length; i++) {
+		struct swaynag_type *type = types->items[i];
+		if (strcasecmp(type->name, name) == 0) {
+			return type;
+		}
+	}
+	return NULL;
+}
+
+void swaynag_type_merge(struct swaynag_type *dest, struct swaynag_type *src) {
+	if (!dest || !src) {
+		return;
+	}
+
+	if (!dest->font && src->font) {
+		dest->font = strdup(src->font);
+	}
+
+	if (!dest->output && src->output) {
+		dest->output = strdup(src->output);
+	}
+
+	if (dest->anchors == 0 && src->anchors > 0) {
+		dest->anchors = src->anchors;
+	}
+
+	// Colors
+	if (dest->button_background == 0 && src->button_background > 0) {
+		dest->button_background = src->button_background;
+	}
+
+	if (dest->background == 0 && src->background > 0) {
+		dest->background = src->background;
+	}
+
+	if (dest->text == 0 && src->text > 0) {
+		dest->text = src->text;
+	}
+
+	if (dest->border == 0 && src->border > 0) {
+		dest->border = src->border;
+	}
+
+	if (dest->border_bottom == 0 && src->border_bottom > 0) {
+		dest->border_bottom = src->border_bottom;
+	}
+
+	// Sizing
+	if (dest->bar_border_thickness == 0 && src->bar_border_thickness > 0) {
+		dest->bar_border_thickness = src->bar_border_thickness;
+	}
+
+	if (dest->message_padding == 0 && src->message_padding > 0) {
+		dest->message_padding = src->message_padding;
+	}
+
+	if (dest->details_border_thickness == 0
+			&& src->details_border_thickness > 0) {
+		dest->details_border_thickness = src->details_border_thickness;
+	}
+
+	if (dest->button_border_thickness == 0
+			&& src->button_border_thickness > 0) {
+		dest->button_border_thickness = src->button_border_thickness;
+	}
+
+	if (dest->button_gap == 0 && src->button_gap > 0) {
+		dest->button_gap = src->button_gap;
+	}
+
+	if (dest->button_gap_close == 0 && src->button_gap_close > 0) {
+		dest->button_gap_close = src->button_gap_close;
+	}
+
+	if (dest->button_margin_right == 0 && src->button_margin_right > 0) {
+		dest->button_margin_right = src->button_margin_right;
+	}
+
+	if (dest->button_padding == 0 && src->button_padding > 0) {
+		dest->button_padding = src->button_padding;
+	}
+}
+
+void swaynag_type_free(struct swaynag_type *type) {
+	free(type->name);
+	free(type->font);
+	free(type->output);
+	free(type);
+}
+
+void swaynag_types_free(list_t *types) {
+	while (types->length) {
+		struct swaynag_type *type = types->items[0];
+		swaynag_type_free(type);
+		list_del(types, 0);
+	}
+	list_free(types);
+}