diff --git a/COPYING b/COPYING
index 779ecbd55aab1d00cd34fdab34c75dcc98e61014..86febebf10557d332182dc743b015f77bf484015 100644
--- a/COPYING
+++ b/COPYING
@@ -1,4 +1,4 @@
-SPDX-License-Identifier: MIT and LGPL-2.1-or-later and Apache-2.0
+SPDX-License-Identifier: MIT and LGPL-2.1-or-later and Apache-2.0 and Zlib
 
 The overall license of steam-runtime-tools and pressure-vessel is
 LGPL-2.1-or-later <https://spdx.org/licenses/LGPL-2.1-or-later.html>,
@@ -20,27 +20,9 @@ which can be found in the file `COPYING.LGPL-2.1`:
     License along with this library. If not, see <http://www.gnu.org/licenses/>.
 
 Some of the individual source files are under the more permissive
-MIT/X11 license, and can be extracted into proprietary projects
-without needing to follow the terms of the LGPL:
-
-    Permission is hereby granted, free of charge, to any person obtaining
-    a copy of this software and associated documentation files (the
-    "Software"), to deal in the Software without restriction, including
-    without limitation the rights to use, copy, modify, merge, publish,
-    distribute, sublicense, and/or sell copies of the Software, and to
-    permit persons to whom the Software is furnished to do so, subject to
-    the following conditions:
-
-    The above copyright notice and this permission notice shall be included
-    in all copies or substantial portions of the Software.
-
-    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-    IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-    CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
-    TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-    SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+MIT/X11 or Zlib licenses, and can be extracted into proprietary projects
+without needing to follow the terms of the LGPL. See `COPYING.MIT` and
+`COPYING.Zlib` or the individual source files for details.
 
 See individual source files for copyright and licensing details, and
 in particular pressure-vessel/THIRD-PARTY.md for details of third-party
diff --git a/COPYING.MIT b/COPYING.MIT
new file mode 100644
index 0000000000000000000000000000000000000000..06bc0920b135e3ed839e08a513a169f81e7ce6be
--- /dev/null
+++ b/COPYING.MIT
@@ -0,0 +1,20 @@
+SPDX-License-Identifier: MIT
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/COPYING.Zlib b/COPYING.Zlib
new file mode 100644
index 0000000000000000000000000000000000000000..88aaec01ee2d4197a53d624a4f43e28b0f02e74c
--- /dev/null
+++ b/COPYING.Zlib
@@ -0,0 +1,17 @@
+SPDX-License-Identifier: Zlib
+
+This software is provided 'as-is', without any express or implied
+warranty.  In no event will the authors be held liable for any damages
+arising from the use of this software.
+
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+
+1. The origin of this software must not be misrepresented; you must not
+   claim that you wrote the original software. If you use this software
+   in a product, an acknowledgment in the product documentation would be
+   appreciated but is not required.
+2. Altered source versions must be plainly marked as such, and must not be
+   misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.
diff --git a/bin/input-monitor.c b/bin/input-monitor.c
new file mode 100644
index 0000000000000000000000000000000000000000..6d5bc0f0136d83efd1a42f935adf3be744c2efae
--- /dev/null
+++ b/bin/input-monitor.c
@@ -0,0 +1,962 @@
+/*
+ * Input device monitor, adapted from SDL code.
+ *
+ * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org>
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: Zlib
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ */
+
+#include <libglnx.h>
+
+#include <steam-runtime-tools/steam-runtime-tools.h>
+
+#include "steam-runtime-tools/glib-backports-internal.h"
+#include "steam-runtime-tools/input-device-internal.h"
+#include "steam-runtime-tools/json-glib-backports-internal.h"
+#include "steam-runtime-tools/utils-internal.h"
+
+#include <stdio.h>
+
+#include <glib-unix.h>
+
+#include <json-glib/json-glib.h>
+
+#define RECORD_SEPARATOR "\x1e"
+
+static FILE *original_stdout = NULL;
+static gboolean opt_one_line = FALSE;
+static gboolean opt_seq = FALSE;
+static gboolean opt_verbose = FALSE;
+
+static void
+jsonify_flags (JsonBuilder *builder,
+               GType flags_type,
+               unsigned int values)
+{
+  GFlagsClass *class;
+  GFlagsValue *flags_value;
+
+  g_return_if_fail (G_TYPE_IS_FLAGS (flags_type));
+
+  class = g_type_class_ref (flags_type);
+
+  while (values != 0)
+    {
+      flags_value = g_flags_get_first_value (class, values);
+
+      if (flags_value == NULL)
+        break;
+
+      json_builder_add_string_value (builder, flags_value->value_nick);
+      values &= ~flags_value->value;
+    }
+
+  if (values)
+    {
+      gchar *rest = g_strdup_printf ("0x%x", values);
+
+      json_builder_add_string_value (builder, rest);
+
+      g_free (rest);
+    }
+
+  g_type_class_unref (class);
+}
+
+static void
+print_json (JsonBuilder *builder)
+{
+  g_autoptr(JsonGenerator) generator = json_generator_new ();
+  g_autofree gchar *text = NULL;
+  JsonNode *root;
+
+  root = json_builder_get_root (builder);
+  generator = json_generator_new ();
+  json_generator_set_root (generator, root);
+
+  if (opt_seq)
+    fputs (RECORD_SEPARATOR, original_stdout);
+
+  if (!opt_one_line)
+    json_generator_set_pretty (generator, TRUE);
+
+  text = json_generator_to_data (generator, NULL);
+
+  if (fputs (text, original_stdout) < 0)
+    g_warning ("Unable to write output: %s", g_strerror (errno));
+
+  if (fputs ("\n", original_stdout) < 0)
+    g_warning ("Unable to write final newline: %s", g_strerror (errno));
+}
+
+static void
+add_uevent_member (JsonBuilder *builder,
+                   const char *name,
+                   const char *value)
+{
+  const char *start = value;
+  const char *end;
+
+  /* value is conceptually a single string, but we
+   * present it as an array of lines here, because that's
+   * a lot easier to read! */
+  json_builder_set_member_name (builder, name);
+  json_builder_begin_array (builder);
+
+  while (*start != '\0')
+    {
+      g_autofree gchar *valid = NULL;
+
+      while (*start == '\n')
+        start++;
+
+      if (*start == '\0')
+        break;
+
+      end = strchrnul (start, '\n');
+
+      valid = g_utf8_make_valid (start, end - start);
+      json_builder_add_string_value (builder, valid);
+
+      if (*end == '\0')
+        break;
+
+      start = end + 1;
+    }
+
+  json_builder_end_array (builder);
+}
+
+static void
+append_evdev_hex (GString *buf,
+                  const unsigned long *bits,
+                  size_t n_longs)
+{
+  size_t i, j;
+
+  for (i = 0; i < n_longs; i++)
+    {
+      unsigned long word = bits[i];
+
+      for (j = 0; j < sizeof (long); j++)
+        {
+          unsigned char byte = (word >> (CHAR_BIT * j)) & 0xff;
+          g_string_append_printf (buf, "%02x ", byte);
+        }
+      g_string_append_c (buf, ' ');
+    }
+}
+
+static void
+added (SrtInputDeviceMonitor *monitor,
+       SrtInputDevice *dev,
+       void *user_data)
+{
+  g_autoptr(JsonBuilder) builder = json_builder_new ();
+
+  json_builder_begin_object (builder);
+    {
+      json_builder_set_member_name (builder, "added");
+      json_builder_begin_object (builder);
+        {
+          g_auto(GStrv) udev_properties = NULL;
+          const char *sys_path;
+          struct
+          {
+            const char *name;
+            const char *phys;
+            const char *uniq;
+            const char *manufacturer;
+            unsigned int bus_type, vendor_id, product_id, version;
+          } id, blank_id = {};
+          unsigned long bits[LONGS_FOR_BITS (HIGHEST_EVENT_CODE)];
+          SrtInputDeviceTypeFlags type_flags;
+
+          json_builder_set_member_name (builder, "interface_flags");
+          json_builder_begin_array (builder);
+          jsonify_flags (builder, SRT_TYPE_INPUT_DEVICE_INTERFACE_FLAGS,
+                         srt_input_device_get_interface_flags (dev));
+          json_builder_end_array (builder);
+
+          type_flags = srt_input_device_get_type_flags (dev);
+          json_builder_set_member_name (builder, "type_flags");
+          json_builder_begin_array (builder);
+          jsonify_flags (builder, SRT_TYPE_INPUT_DEVICE_TYPE_FLAGS,
+                         type_flags);
+          json_builder_end_array (builder);
+
+          json_builder_set_member_name (builder, "dev_node");
+          json_builder_add_string_value (builder,
+                                         srt_input_device_get_dev_node (dev));
+          json_builder_set_member_name (builder, "subsystem");
+          json_builder_add_string_value (builder,
+                                         srt_input_device_get_subsystem (dev));
+          json_builder_set_member_name (builder, "sys_path");
+          json_builder_add_string_value (builder,
+                                         srt_input_device_get_sys_path (dev));
+
+          id = blank_id;
+
+          if (srt_input_device_get_identity (dev,
+                                             &id.bus_type,
+                                             &id.vendor_id,
+                                             &id.product_id,
+                                             &id.version))
+            {
+                {
+                  g_autofree gchar *s = g_strdup_printf ("0x%04x", id.bus_type);
+                  json_builder_set_member_name (builder, "bus_type");
+                  json_builder_add_string_value (builder, s);
+                }
+                {
+                  g_autofree gchar *s = g_strdup_printf ("0x%04x", id.vendor_id);
+                  json_builder_set_member_name (builder, "vendor_id");
+                  json_builder_add_string_value (builder, s);
+                }
+                {
+                  g_autofree gchar *s = g_strdup_printf ("0x%04x", id.product_id);
+                  json_builder_set_member_name (builder, "product_id");
+                  json_builder_add_string_value (builder, s);
+                }
+                {
+                  g_autofree gchar *s = g_strdup_printf ("0x%04x", id.version);
+                  json_builder_set_member_name (builder, "version");
+                  json_builder_add_string_value (builder, s);
+                }
+            }
+
+          if (srt_input_device_get_interface_flags (dev)
+              & SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT)
+            {
+              SrtInputDeviceTypeFlags guessed_flags;
+
+              json_builder_set_member_name (builder, "evdev");
+              json_builder_begin_object (builder);
+
+              if (srt_input_device_get_event_types (dev, bits, G_N_ELEMENTS (bits)) > 0)
+                {
+                  json_builder_set_member_name (builder, "types");
+                  json_builder_begin_array (builder);
+                    {
+#define BIT(x) \
+                      do \
+                        { \
+                          if (test_bit_checked (EV_ ## x, bits, G_N_ELEMENTS (bits))) \
+                            { \
+                              json_builder_add_string_value (builder, #x); \
+                            } \
+                        } \
+                      while (0)
+
+                      BIT (SYN);
+                      BIT (KEY);
+                      BIT (REL);
+                      BIT (ABS);
+                      BIT (MSC);
+                      BIT (SW);
+                      BIT (LED);
+                      BIT (SND);
+                      BIT (REP);
+                      BIT (FF);
+                      BIT (PWR);
+                      BIT (FF_STATUS);
+
+#undef BIT
+                    }
+                  json_builder_end_array (builder);
+
+                  if (opt_verbose)
+                    {
+                      g_autoptr(GString) buf = g_string_new ("");
+
+                      append_evdev_hex (buf, bits, LONGS_FOR_BITS (EV_MAX));
+                      json_builder_set_member_name (builder, "raw_types");
+                      json_builder_add_string_value (builder, buf->str);
+                    }
+                }
+
+              if (srt_input_device_get_event_capabilities (dev, EV_ABS,
+                                                           bits, G_N_ELEMENTS (bits)) > 0)
+                {
+                  json_builder_set_member_name (builder, "absolute_axes");
+                  json_builder_begin_array (builder);
+                    {
+#define BIT(x) \
+                      do \
+                        { \
+                          if (test_bit_checked (ABS_ ## x, bits, G_N_ELEMENTS (bits))) \
+                            { \
+                              json_builder_add_string_value (builder, #x); \
+                            } \
+                        } \
+                      while (0)
+
+                      BIT (X);
+                      BIT (Y);
+                      BIT (Z);
+                      BIT (RX);
+                      BIT (RY);
+                      BIT (RZ);
+                      BIT (THROTTLE);
+                      BIT (RUDDER);
+                      BIT (WHEEL);
+                      BIT (GAS);
+                      BIT (BRAKE);
+                      BIT (HAT0X);
+                      BIT (HAT0Y);
+                      BIT (HAT1X);
+                      BIT (HAT1Y);
+                      BIT (HAT2X);
+                      BIT (HAT2Y);
+                      BIT (HAT3X);
+                      BIT (HAT3Y);
+                      BIT (PRESSURE);
+                      BIT (DISTANCE);
+                      BIT (TILT_X);
+                      BIT (TILT_Y);
+                      BIT (TOOL_WIDTH);
+                      BIT (VOLUME);
+                      BIT (MISC);
+                      BIT (RESERVED);
+                      BIT (MT_SLOT);
+
+#undef BIT
+                    }
+                  json_builder_end_array (builder);
+
+                  if (opt_verbose)
+                    {
+                      g_autoptr(GString) buf = g_string_new ("");
+
+                      append_evdev_hex (buf, bits, LONGS_FOR_BITS (ABS_MAX));
+                      json_builder_set_member_name (builder, "raw_abs");
+                      json_builder_add_string_value (builder, buf->str);
+                    }
+                }
+
+              if (srt_input_device_get_event_capabilities (dev, EV_REL,
+                                                           bits, G_N_ELEMENTS (bits)) > 0)
+                {
+                  json_builder_set_member_name (builder, "relative_axes");
+                  json_builder_begin_array (builder);
+                    {
+#define BIT(x) \
+                      do \
+                        { \
+                          if (test_bit_checked (REL_ ## x, bits, G_N_ELEMENTS (bits))) \
+                            { \
+                              json_builder_add_string_value (builder, #x); \
+                            } \
+                        } \
+                      while (0)
+
+                      BIT (X);
+                      BIT (Y);
+                      BIT (Z);
+                      BIT (RX);
+                      BIT (RY);
+                      BIT (RZ);
+                      BIT (HWHEEL);
+                      BIT (DIAL);
+                      BIT (WHEEL);
+                      BIT (MISC);
+                      BIT (RESERVED);
+                      BIT (WHEEL_HI_RES);
+                      BIT (HWHEEL_HI_RES);
+
+#undef BIT
+                    }
+                  json_builder_end_array (builder);
+
+                  if (opt_verbose)
+                    {
+                      g_autoptr(GString) buf = g_string_new ("");
+
+                      append_evdev_hex (buf, bits, LONGS_FOR_BITS (REL_MAX));
+                      json_builder_set_member_name (builder, "raw_rel");
+                      json_builder_add_string_value (builder, buf->str);
+                    }
+                }
+
+              if (srt_input_device_get_event_capabilities (dev, EV_KEY,
+                                                           bits, G_N_ELEMENTS (bits)) > 0)
+                {
+                  json_builder_set_member_name (builder, "keys");
+                  json_builder_begin_array (builder);
+                    {
+#define BIT(x) \
+                      do \
+                        { \
+                          if (test_bit_checked (x, bits, G_N_ELEMENTS (bits))) \
+                            { \
+                              json_builder_add_string_value (builder, #x); \
+                            } \
+                        } \
+                      while (0)
+
+                      /* We don't show all the keyboard keys here because
+                       * that would be ridiculous, but we do show a
+                       * selection that should be enough to tell the
+                       * difference between keyboards, mice, joysticks
+                       * and so on. We do show most joystick buttons. */
+
+                      /* Gamepads */
+                      BIT (BTN_A);    /* aka BTN_GAMEPAD, BTN_SOUTH */
+                      BIT (BTN_B);
+                      BIT (BTN_C);
+                      BIT (BTN_X);
+                      BIT (BTN_Y);
+                      BIT (BTN_Z);
+                      BIT (BTN_TL);
+                      BIT (BTN_TR);
+                      BIT (BTN_TL2);
+                      BIT (BTN_TR2);
+                      BIT (BTN_SELECT);
+                      BIT (BTN_START);
+                      BIT (BTN_MODE);
+                      BIT (BTN_THUMBL);
+                      BIT (BTN_THUMBR);
+                      /* Not all gamepads have a digital d-pad, some only
+                       * represent it as the hat0x and hat0y absolute axes;
+                       * but some do have it */
+                      BIT (BTN_DPAD_UP);
+                      BIT (BTN_DPAD_DOWN);
+                      BIT (BTN_DPAD_LEFT);
+                      BIT (BTN_DPAD_RIGHT);
+
+                      /* Flight sticks and similar joysticks */
+                      BIT (BTN_TRIGGER);
+                      BIT (BTN_THUMB);
+                      BIT (BTN_THUMB2);
+                      BIT (BTN_TOP);
+                      BIT (BTN_TOP2);
+                      BIT (BTN_PINKIE);
+                      BIT (BTN_BASE);
+                      BIT (BTN_BASE2);
+                      BIT (BTN_BASE3);
+                      BIT (BTN_BASE4);
+                      BIT (BTN_BASE5);
+                      BIT (BTN_BASE6);
+                      BIT (BTN_DEAD);
+                      BIT (BTN_TRIGGER_HAPPY);
+
+                      /* Steering wheels */
+                      BIT (BTN_GEAR_DOWN);
+                      BIT (BTN_GEAR_UP);
+
+                      /* Keyboards */
+                      BIT (KEY_ESC);
+                      BIT (KEY_0);
+                      BIT (KEY_A);
+                      BIT (KEY_KP0);
+                      BIT (KEY_PLAY);
+
+                      /* Mice and friends. BTN_LEFT is an alias for
+                       * BTN_MOUSE, but we use BTN_MOUSE here as a
+                       * hint that the rest are also mouse buttons. */
+                      BIT (BTN_MOUSE);
+                      BIT (BTN_RIGHT);
+                      BIT (BTN_MIDDLE);
+                      BIT (BTN_SIDE);
+                      BIT (BTN_EXTRA);
+                      BIT (BTN_FORWARD);
+                      BIT (BTN_BACK);
+                      BIT (BTN_TASK);
+                      BIT (BTN_DIGI);
+                      BIT (KEY_MACRO1);
+
+                      /* Generic buttons that nobody knows what they do... */
+                      BIT (BTN_0);
+
+#undef BIT
+                    }
+                  json_builder_end_array (builder);
+
+                  if (opt_verbose)
+                    {
+                      g_autoptr(GString) buf = g_string_new ("");
+
+                      append_evdev_hex (buf, bits, LONGS_FOR_BITS (KEY_MAX));
+                      json_builder_set_member_name (builder, "raw_keys");
+                      json_builder_add_string_value (builder, buf->str);
+                    }
+                }
+
+              if (srt_input_device_get_input_properties (dev, bits,
+                                                         G_N_ELEMENTS (bits)) > 0)
+                {
+                  json_builder_set_member_name (builder, "input_properties");
+                  json_builder_begin_array (builder);
+                    {
+#define BIT(x) \
+                      do \
+                        { \
+                          if (test_bit_checked (INPUT_PROP_ ## x, bits, G_N_ELEMENTS (bits))) \
+                            { \
+                              json_builder_add_string_value (builder, #x); \
+                            } \
+                        } \
+                      while (0)
+
+                      BIT (POINTER);
+                      BIT (DIRECT);
+                      BIT (BUTTONPAD);
+                      BIT (SEMI_MT);
+                      BIT (TOPBUTTONPAD);
+                      BIT (POINTING_STICK);
+                      BIT (ACCELEROMETER);
+
+#undef BIT
+                    }
+                  json_builder_end_array (builder);
+
+                  if (opt_verbose)
+                    {
+                      g_autoptr(GString) buf = g_string_new ("");
+
+                      append_evdev_hex (buf, bits, LONGS_FOR_BITS (REL_MAX));
+                      json_builder_set_member_name (builder, "raw_input_properties");
+                      json_builder_add_string_value (builder, buf->str);
+                    }
+                }
+
+              guessed_flags = srt_input_device_guess_type_flags_from_event_capabilities (dev);
+
+              if (opt_verbose || guessed_flags != type_flags)
+                {
+                  json_builder_set_member_name (builder, "guessed_type_flags");
+                  json_builder_begin_array (builder);
+                  jsonify_flags (builder, SRT_TYPE_INPUT_DEVICE_TYPE_FLAGS,
+                                 guessed_flags);
+                  json_builder_end_array (builder);
+                }
+
+              json_builder_end_object (builder);
+            }
+
+          udev_properties = srt_input_device_dup_udev_properties (dev);
+
+          if (udev_properties != NULL)
+            {
+              gsize i;
+
+              json_builder_set_member_name (builder, "udev_properties");
+              json_builder_begin_array (builder);
+
+              for (i = 0; udev_properties[i] != NULL; i++)
+                {
+                  g_autofree gchar *valid = NULL;
+
+                  valid = g_utf8_make_valid (udev_properties[i], -1);
+                  json_builder_add_string_value (builder, valid);
+                }
+
+              json_builder_end_array (builder);
+            }
+
+          if (opt_verbose)
+            {
+              g_autofree gchar *uevent = srt_input_device_dup_uevent (dev);
+
+              if (uevent != NULL)
+                add_uevent_member (builder, "uevent", uevent);
+            }
+
+          sys_path = srt_input_device_get_hid_sys_path (dev);
+
+          id = blank_id;
+
+          if (srt_input_device_get_hid_identity (dev,
+                                                 &id.bus_type,
+                                                 &id.vendor_id,
+                                                 &id.product_id,
+                                                 &id.name,
+                                                 &id.phys,
+                                                 &id.uniq)
+              || sys_path != NULL)
+            {
+              json_builder_set_member_name (builder, "hid_ancestor");
+              json_builder_begin_object (builder);
+
+              json_builder_set_member_name (builder, "sys_path");
+              json_builder_add_string_value (builder, sys_path);
+
+              json_builder_set_member_name (builder, "name");
+              json_builder_add_string_value (builder, id.name);
+
+                {
+                  g_autofree gchar *s = g_strdup_printf ("0x%04x", id.bus_type);
+                  json_builder_set_member_name (builder, "bus_type");
+                  json_builder_add_string_value (builder, s);
+                }
+                {
+                  g_autofree gchar *s = g_strdup_printf ("0x%04x", id.vendor_id);
+                  json_builder_set_member_name (builder, "vendor_id");
+                  json_builder_add_string_value (builder, s);
+                }
+                {
+                  g_autofree gchar *s = g_strdup_printf ("0x%04x", id.product_id);
+                  json_builder_set_member_name (builder, "product_id");
+                  json_builder_add_string_value (builder, s);
+                }
+
+              json_builder_set_member_name (builder, "uniq");
+              json_builder_add_string_value (builder, id.uniq);
+
+              if (opt_verbose)
+                {
+                  g_autofree gchar *uevent = srt_input_device_dup_hid_uevent (dev);
+
+                  json_builder_set_member_name (builder, "phys");
+                  json_builder_add_string_value (builder, id.phys);
+
+                  if (uevent != NULL)
+                    add_uevent_member (builder, "uevent", uevent);
+                }
+
+              json_builder_end_object (builder);
+            }
+
+          sys_path = srt_input_device_get_input_sys_path (dev);
+
+          id = blank_id;
+
+          if (srt_input_device_get_input_identity (dev,
+                                                   &id.bus_type,
+                                                   &id.vendor_id,
+                                                   &id.product_id,
+                                                   &id.version,
+                                                   &id.name,
+                                                   &id.phys,
+                                                   &id.uniq)
+              || sys_path != NULL)
+            {
+              json_builder_set_member_name (builder, "input_ancestor");
+              json_builder_begin_object (builder);
+
+              json_builder_set_member_name (builder, "sys_path");
+              json_builder_add_string_value (builder, sys_path);
+
+              json_builder_set_member_name (builder, "name");
+              json_builder_add_string_value (builder, id.name);
+
+                {
+                  g_autofree gchar *s = g_strdup_printf ("0x%04x", id.bus_type);
+                  json_builder_set_member_name (builder, "bus_type");
+                  json_builder_add_string_value (builder, s);
+                }
+                {
+                  g_autofree gchar *s = g_strdup_printf ("0x%04x", id.vendor_id);
+                  json_builder_set_member_name (builder, "vendor_id");
+                  json_builder_add_string_value (builder, s);
+                }
+                {
+                  g_autofree gchar *s = g_strdup_printf ("0x%04x", id.product_id);
+                  json_builder_set_member_name (builder, "product_id");
+                  json_builder_add_string_value (builder, s);
+                }
+                {
+                  g_autofree gchar *s = g_strdup_printf ("0x%04x", id.version);
+                  json_builder_set_member_name (builder, "version");
+                  json_builder_add_string_value (builder, s);
+                }
+
+              if (opt_verbose)
+                {
+                  g_autofree gchar *uevent = srt_input_device_dup_input_uevent (dev);
+
+                  json_builder_set_member_name (builder, "phys");
+                  json_builder_add_string_value (builder, id.phys);
+
+                  if (uevent != NULL)
+                    add_uevent_member (builder, "uevent", uevent);
+                }
+
+              json_builder_end_object (builder);
+            }
+
+          sys_path = srt_input_device_get_usb_device_sys_path (dev);
+
+          id = blank_id;
+
+          if (srt_input_device_get_usb_device_identity (dev,
+                                                        &id.vendor_id,
+                                                        &id.product_id,
+                                                        &id.version,
+                                                        &id.manufacturer,
+                                                        &id.name,
+                                                        &id.uniq)
+              || sys_path != NULL)
+            {
+              json_builder_set_member_name (builder, "usb_device_ancestor");
+              json_builder_begin_object (builder);
+
+              json_builder_set_member_name (builder, "sys_path");
+              json_builder_add_string_value (builder, sys_path);
+
+                {
+                  g_autofree gchar *s = g_strdup_printf ("0x%04x", id.vendor_id);
+                  json_builder_set_member_name (builder, "vendor_id");
+                  json_builder_add_string_value (builder, s);
+                }
+                {
+                  g_autofree gchar *s = g_strdup_printf ("0x%04x", id.product_id);
+                  json_builder_set_member_name (builder, "product_id");
+                  json_builder_add_string_value (builder, s);
+                }
+                {
+                  g_autofree gchar *s = g_strdup_printf ("0x%04x", id.version);
+                  json_builder_set_member_name (builder, "version");
+                  json_builder_add_string_value (builder, s);
+                }
+
+              json_builder_set_member_name (builder, "manufacturer");
+              json_builder_add_string_value (builder, id.manufacturer);
+              json_builder_set_member_name (builder, "product");
+              json_builder_add_string_value (builder, id.name);
+              json_builder_set_member_name (builder, "serial");
+              json_builder_add_string_value (builder, id.uniq);
+
+              if (opt_verbose)
+                {
+                  g_autofree gchar *uevent = srt_input_device_dup_usb_device_uevent (dev);
+
+                  if (uevent != NULL)
+                    add_uevent_member (builder, "uevent", uevent);
+                }
+
+              json_builder_end_object (builder);
+            }
+        }
+      json_builder_end_object (builder);
+    }
+  json_builder_end_object (builder);
+
+  print_json (builder);
+}
+
+static void
+removed (SrtInputDeviceMonitor *monitor,
+         SrtInputDevice *dev,
+         void *user_data)
+{
+  g_autoptr(JsonBuilder) builder = json_builder_new ();
+
+  json_builder_begin_object (builder);
+    {
+      json_builder_set_member_name (builder, "removed");
+      json_builder_begin_object (builder);
+        {
+          /* Only print enough details to identify the object */
+          json_builder_set_member_name (builder, "dev_node");
+          json_builder_add_string_value (builder,
+                                         srt_input_device_get_dev_node (dev));
+          json_builder_set_member_name (builder, "sys_path");
+          json_builder_add_string_value (builder,
+                                         srt_input_device_get_sys_path (dev));
+        }
+      json_builder_end_object (builder);
+    }
+  json_builder_end_object (builder);
+
+  print_json (builder);
+}
+
+static void
+all_for_now (SrtInputDeviceMonitor *source,
+             void *user_data)
+{
+  g_autoptr(SrtInputDeviceMonitor) monitor = NULL;
+  SrtInputDeviceMonitor **monitor_p = user_data;
+
+  if (srt_input_device_monitor_get_flags (source) & SRT_INPUT_DEVICE_MONITOR_FLAGS_ONCE)
+    {
+      monitor = g_steal_pointer (monitor_p);
+      g_assert (monitor == NULL || monitor == source);
+      g_clear_object (&monitor);
+    }
+
+  if (opt_seq)
+    fputs (RECORD_SEPARATOR, original_stdout);
+
+  fputs ("{\"all-for-now\": true}\n", original_stdout);
+}
+
+static gboolean
+interrupt_cb (void *user_data)
+{
+  g_autoptr(SrtInputDeviceMonitor) monitor = NULL;
+  SrtInputDeviceMonitor **monitor_p = user_data;
+
+  monitor = g_steal_pointer (monitor_p);
+  g_clear_object (&monitor);
+
+  return G_SOURCE_CONTINUE;
+}
+
+static SrtInputDeviceMonitorFlags opt_mode = SRT_INPUT_DEVICE_MONITOR_FLAGS_NONE;
+static SrtInputDeviceMonitorFlags opt_flags = SRT_INPUT_DEVICE_MONITOR_FLAGS_NONE;
+static gboolean opt_evdev = FALSE;
+static gboolean opt_hidraw = FALSE;
+static gboolean opt_print_version = FALSE;
+
+static gboolean
+opt_once_cb (const gchar *option_name,
+             const gchar *value,
+             gpointer data,
+             GError **error)
+{
+  opt_flags |= SRT_INPUT_DEVICE_MONITOR_FLAGS_ONCE;
+  return TRUE;
+}
+
+static gboolean
+opt_direct_cb (const gchar *option_name,
+               const gchar *value,
+               gpointer data,
+               GError **error)
+{
+  opt_mode = SRT_INPUT_DEVICE_MONITOR_FLAGS_DIRECT;
+  return TRUE;
+}
+
+static gboolean
+opt_udev_cb (const gchar *option_name,
+             const gchar *value,
+             gpointer data,
+             GError **error)
+{
+  opt_mode = SRT_INPUT_DEVICE_MONITOR_FLAGS_UDEV;
+  return TRUE;
+}
+
+static const GOptionEntry option_entries[] =
+{
+  { "direct", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_direct_cb,
+    "Find devices using /dev and /sys", NULL },
+  { "evdev", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_evdev,
+    "List evdev event devices", NULL },
+  { "hidraw", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_hidraw,
+    "List raw HID devices", NULL },
+  { "once", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_once_cb,
+    "Print devices that are initially discovered, then exit", NULL },
+  { "one-line", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_one_line,
+    "Print one device per line [default: pretty-print as concatenated JSON]",
+    NULL },
+  { "seq", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_seq,
+    "Output application/json-seq [default: pretty-print as concatenated JSON]",
+    NULL },
+  { "udev", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_udev_cb,
+    "Find devices using udev", NULL },
+  { "verbose", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_verbose,
+    "Be more verbose", NULL },
+  { "version", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_print_version,
+    "Print version number and exit", NULL },
+  { NULL }
+};
+
+static gboolean
+run (int argc,
+     char **argv,
+     GError **error)
+{
+  g_autoptr(GOptionContext) option_context = NULL;
+  g_autoptr(SrtInputDeviceMonitor) monitor = NULL;
+  guint int_handler;
+
+  _srt_setenv_disable_gio_modules ();
+
+  option_context = g_option_context_new ("");
+  g_option_context_add_main_entries (option_context, option_entries, NULL);
+
+  if (!g_option_context_parse (option_context, &argc, &argv, error))
+    return FALSE;
+
+  if (opt_print_version)
+    {
+      /* Output version number as YAML for machine-readability,
+       * inspired by `ostree --version` and `docker version` */
+      g_print (
+          "%s:\n"
+          " Package: steam-runtime-tools\n"
+          " Version: %s\n",
+          g_get_prgname (), VERSION);
+      return TRUE;
+    }
+
+  /* stdout is reserved for machine-readable output, so avoid having
+   * things like g_debug() pollute it. */
+  original_stdout = _srt_divert_stdout_to_stderr (error);
+
+  if (original_stdout == NULL)
+    return FALSE;
+
+  int_handler = g_unix_signal_add (SIGINT, interrupt_cb, &monitor);
+  monitor = srt_input_device_monitor_new (opt_mode | opt_flags);
+
+  if (opt_evdev)
+    srt_input_device_monitor_request_evdev (monitor);
+
+  if (opt_hidraw)
+    srt_input_device_monitor_request_raw_hid (monitor);
+
+  if (opt_evdev + opt_hidraw == 0)
+    {
+      /* Subscribe to everything by default */
+      srt_input_device_monitor_request_evdev (monitor);
+      srt_input_device_monitor_request_raw_hid (monitor);
+    }
+
+  g_signal_connect (monitor, "added", G_CALLBACK (added), NULL);
+  g_signal_connect (monitor, "removed", G_CALLBACK (removed), NULL);
+  g_signal_connect (monitor, "all-for-now", G_CALLBACK (all_for_now), &monitor);
+
+  if (!srt_input_device_monitor_start (monitor, error))
+    return FALSE;
+
+  while (monitor != NULL)
+    g_main_context_iteration (NULL, TRUE);
+
+  if (fclose (original_stdout) != 0)
+    g_warning ("Unable to close stdout: %s", g_strerror (errno));
+
+  g_source_remove (int_handler);
+  return TRUE;
+}
+
+int
+main (int argc,
+      char **argv)
+{
+  g_autoptr(GError) error = NULL;
+
+  if (run (argc, argv, &error))
+    return 0;
+
+  if (error == NULL)
+    g_set_error (&error, G_IO_ERROR, G_IO_ERROR_FAILED, "Assertion failure");
+
+  g_printerr ("%s: %s\n", g_get_prgname (), error->message);
+
+  if (error->domain == G_OPTION_ERROR)
+    return 2;
+
+  return 1;
+}
diff --git a/bin/input-monitor.md b/bin/input-monitor.md
new file mode 100644
index 0000000000000000000000000000000000000000..5066fd1c077a5561b19677486308f348a6aa5f8f
--- /dev/null
+++ b/bin/input-monitor.md
@@ -0,0 +1,109 @@
+---
+title: steam-runtime-input-monitor
+section: 1
+...
+
+# NAME
+
+steam-runtime-input-monitor - list input devices
+
+# SYNOPSIS
+
+**steam-runtime-input-monitor** [*OPTIONS*]
+
+# DESCRIPTION
+
+**steam-runtime-input-monitor** lists input devices.
+
+# OPTIONS
+
+**--direct**
+:   List input devices by reading `/dev` and `/sys`.
+    This currently only lists input devices for which at least read access
+    to the underlying device node is available.
+
+**--evdev**
+:   List all input event devices.
+
+**--hidraw**
+:   List all raw HID devices.
+
+**--once**
+:   Discover the available input devices, print their details and exit.
+
+**--one-line**
+:   Print one event per line ("newline-delimited JSON", unless **--seq**
+    is also used). By default they are pretty-printed with
+    readable indentation, which is convenient for a human reader but
+    less suitable for machine-readable use.
+
+**--seq**
+:   Print events in **application/json-seq** format (RFC 7464):
+    each event is printed as U+001E RECORD SEPARATOR, followed by
+    a JSON object, followed by U+000A LINE FEED.
+
+**--udev**
+:   List input devices by contacting udevd. This is usually not
+    expected to work inside a container.
+
+**--version**
+:   Print the version number as YAML and exit.
+
+# OUTPUT
+
+**steam-runtime-input-monitor** prints one or more *events* on
+standard output, in JSON format. The precise format is determined
+by the **--one-line** and **--seq** options.
+
+By default, all known input devices are listed. Either
+**--evdev** or **--hidraw** can be used to request only a subset of
+input devices. For convenience, if no options requesting a subset of
+input devices are given, this is treated as equivalent to using options
+that request *all* input devices.
+
+Options to limit devices more selectively (for example, only evdev
+devices that have the **ID_INPUT_JOYSTICK** udev property) are likely
+to be added in future versions.
+
+By default, an implementation is chosen automatically. The **--direct**
+and **--udev** options override this, with the last one used
+taking precedence.
+
+Each event is a JSON object with a single key. Consumers should ignore
+unknown keys to allow for future expansion.
+
+**all-for-now**
+:   The initial device enumeration has finished. If the **--once** option
+    was used, **steam-runtime-input-monitor** will exit. If not, it will
+    contine to monitor device hotplug events.
+
+    The value is **true**.
+
+**added**
+:   A device was added. The value is an object describing the device,
+    with the following keys and values:
+
+    **dev_node**
+    :   The device node in `/dev` for this device.
+
+    **sys_path**
+    :   The device directory in `/sys` for this device.
+
+    **subsystem**
+    :   The Linux kernel subsystem, either **input** or **hidraw**.
+
+    Additional keys and values are likely to be added in future versions.
+
+**removed**
+:   A device was removed. The value is an object with keys **dev_node**
+    and **sys_path**, as above.
+
+# EXIT STATUS
+
+0
+:   Success.
+
+Non-zero
+:   A fatal error occurred.
+
+<!-- vim:set sw=4 sts=4 et: -->
diff --git a/bin/meson.build b/bin/meson.build
index 897d3c989a8c840e35be47f2352373f30eeb3493..d9b667ccbd838c3a23c010fcd4b7a265ba0f66f7 100644
--- a/bin/meson.build
+++ b/bin/meson.build
@@ -41,9 +41,20 @@ executable(
   install_rpath : bin_rpath,
 )
 
+executable(
+  'steam-runtime-input-monitor',
+  'input-monitor.c',
+  dependencies : [gio_unix, glib, gobject, json_glib, libglnx_dep, libsteamrt_static_dep],
+  install : true,
+  # Use the adjacent json-glib, ignoring LD_LIBRARY_PATH even if set
+  build_rpath : bin_rpath,
+  install_rpath : bin_rpath,
+)
+
 if get_option('man')
   foreach bin_name : [
     'check-requirements',
+    'input-monitor',
     'system-info',
   ]
     custom_target(
diff --git a/debian/control b/debian/control
index 4afc66f5889f02f79db1001cb0461b1e72cbd66c..9cda84983dceea1081e115b12bca603cb5178e84 100644
--- a/debian/control
+++ b/debian/control
@@ -14,6 +14,7 @@ Build-Depends:
  libglib2.0-dev,
  libjson-glib-dev (>= 1.0),
  libtheora0 <!nocheck>,
+ libudev1 <!nocheck> | libudev0 <!nocheck>,
  libva-dev,
  libvdpau-dev,
  libvulkan-dev,
diff --git a/steam-runtime-tools/direct-input-device-internal.h b/steam-runtime-tools/direct-input-device-internal.h
new file mode 100644
index 0000000000000000000000000000000000000000..310b6b9805ca62e90f3beb6ca94a1cfcfd06e5aa
--- /dev/null
+++ b/steam-runtime-tools/direct-input-device-internal.h
@@ -0,0 +1,67 @@
+/*<private_header>*/
+/*
+ * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org>
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: Zlib
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ */
+
+#pragma once
+
+#include "steam-runtime-tools/glib-backports-internal.h"
+#include "steam-runtime-tools/input-device.h"
+
+#define SRT_TYPE_DIRECT_INPUT_DEVICE (_srt_direct_input_device_get_type ())
+
+typedef struct _SrtDirectInputDevice SrtDirectInputDevice;
+typedef struct _SrtDirectInputDeviceClass SrtDirectInputDeviceClass;
+
+#define SRT_TYPE_DIRECT_INPUT_DEVICE (_srt_direct_input_device_get_type ())
+#define SRT_DIRECT_INPUT_DEVICE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SRT_TYPE_DIRECT_INPUT_DEVICE, SrtDirectInputDevice))
+#define SRT_IS_DIRECT_INPUT_DEVICE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SRT_TYPE_DIRECT_INPUT_DEVICE))
+#define SRT_DIRECT_INPUT_DEVICE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), SRT_TYPE_DIRECT_INPUT_DEVICE, SrtDirectInputDeviceClass))
+#define SRT_DIRECT_INPUT_DEVICE_CLASS(c) (G_TYPE_CHECK_CLASS_CAST ((c), SRT_TYPE_DIRECT_INPUT_DEVICE, SrtDirectInputDeviceClass))
+#define SRT_IS_DIRECT_INPUT_DEVICE_CLASS(c) (G_TYPE_CHECK_CLASS_TYPE ((c), SRT_TYPE_DIRECT_INPUT_DEVICE))
+
+G_GNUC_INTERNAL
+GType _srt_direct_input_device_get_type (void);
+
+#define SRT_TYPE_DIRECT_INPUT_DEVICE_MONITOR (_srt_direct_input_device_monitor_get_type ())
+#define SRT_DIRECT_INPUT_DEVICE_MONITOR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SRT_TYPE_DIRECT_INPUT_DEVICE_MONITOR, SrtDirectInputDeviceMonitor))
+#define SRT_IS_DIRECT_INPUT_DEVICE_MONITOR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SRT_TYPE_DIRECT_INPUT_DEVICE_MONITOR))
+#define SRT_DIRECT_INPUT_DEVICE_MONITOR_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), SRT_TYPE_DIRECT_INPUT_DEVICE_MONITOR, SrtDirectInputDeviceMonitorClass))
+#define SRT_DIRECT_INPUT_DEVICE_MONITOR_CLASS(c) (G_TYPE_CHECK_CLASS_CAST ((c), SRT_TYPE_DIRECT_INPUT_DEVICE_MONITOR, SrtDirectInputDeviceMonitorClass))
+#define SRT_IS_DIRECT_INPUT_DEVICE_MONITOR_CLASS(c) (G_TYPE_CHECK_CLASS_TYPE ((c), SRT_TYPE_DIRECT_INPUT_DEVICE_MONITOR))
+
+typedef struct _SrtDirectInputDeviceMonitor SrtDirectInputDeviceMonitor;
+typedef struct _SrtDirectInputDeviceMonitorClass SrtDirectInputDeviceMonitorClass;
+
+G_GNUC_INTERNAL
+GType _srt_direct_input_device_monitor_get_type (void);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtDirectInputDevice, g_object_unref)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtDirectInputDeviceMonitor, g_object_unref)
+
+static inline SrtInputDeviceMonitor *
+srt_direct_input_device_monitor_new (SrtInputDeviceMonitorFlags flags)
+{
+  return g_object_new (SRT_TYPE_DIRECT_INPUT_DEVICE_MONITOR,
+                       "flags", flags,
+                       NULL);
+}
diff --git a/steam-runtime-tools/direct-input-device.c b/steam-runtime-tools/direct-input-device.c
new file mode 100644
index 0000000000000000000000000000000000000000..6c9c1a82aa8e307472e52e1ace145a7ae76e082b
--- /dev/null
+++ b/steam-runtime-tools/direct-input-device.c
@@ -0,0 +1,1109 @@
+/*
+ * Direct /dev,/sys-based input device monitor, very loosely based on SDL code.
+ *
+ * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org>
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: Zlib
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ */
+
+#include "steam-runtime-tools/direct-input-device-internal.h"
+
+#include <linux/input.h>
+#include <sys/inotify.h>
+#include <sys/ioctl.h>
+
+#include <glib-unix.h>
+#include <libglnx.h>
+
+#include "steam-runtime-tools/input-device.h"
+
+#include "steam-runtime-tools/glib-backports-internal.h"
+#include "steam-runtime-tools/input-device-internal.h"
+#include "steam-runtime-tools/utils-internal.h"
+
+static void srt_direct_input_device_iface_init (SrtInputDeviceInterface *iface);
+static void srt_direct_input_device_monitor_iface_init (SrtInputDeviceMonitorInterface *iface);
+
+static void
+remove_last_component_in_place (char *path)
+{
+  char *slash = strrchr (path, '/');
+
+  if (slash == NULL)
+    *path = '\0';
+  else
+    *slash = '\0';
+}
+
+/*
+ * @path: The path to a device directory in /sys
+ *
+ * Returns: (nullable) (transfer full): The closest ancestor of @path
+ *  that is an input device with evdev capabilities, or %NULL if not found
+ */
+static gchar *
+find_input_ancestor (const char *path)
+{
+  g_autofree char *ancestor = NULL;
+
+  for (ancestor = g_strdup (path);
+       g_str_has_prefix (ancestor, "/sys/");
+       remove_last_component_in_place (ancestor))
+    {
+      g_autofree char *subsys = g_build_filename (ancestor, "subsystem", NULL);
+      g_autofree char *caps = g_build_filename (ancestor, "capabilities", "ev", NULL);
+      g_autofree char *target = glnx_readlinkat_malloc (AT_FDCWD, subsys,
+                                                        NULL, NULL);
+      const char *slash = NULL;
+
+      if (!g_file_test (caps, G_FILE_TEST_IS_REGULAR))
+        continue;
+
+      if (target != NULL)
+        slash = strrchr (target, '/');
+
+      if (slash == NULL || strcmp (slash + 1, "input") != 0)
+        continue;
+
+      return g_steal_pointer (&ancestor);
+    }
+
+  return NULL;
+}
+
+/*
+ * @path: The path to a device directory in /sys
+ * @subsystem: A desired subsystem, such as "hid" or "usb", or %NULL to accept any
+ * @devtype: A desired device type, such as "usb_device", or %NULL to accept any
+ * @uevent_out: (out) (optional): Optionally return the text of /sys/.../uevent
+ *
+ * Returns: (nullable) (transfer full): The closest ancestor of @path
+ *  that has a subsystem of @subsystem, or %NULL if not found.
+ */
+static gchar *
+get_ancestor_with_subsystem_devtype (const char *path,
+                                     const char *subsystem,
+                                     const char *devtype,
+                                     gchar **uevent_out)
+{
+  g_autofree char *ancestor = NULL;
+
+  for (ancestor = g_strdup (path);
+       g_str_has_prefix (ancestor, "/sys/");
+       remove_last_component_in_place (ancestor))
+    {
+      g_autofree char *uevent = g_build_filename (ancestor, "uevent", NULL);
+      g_autofree char *text = NULL;
+
+      /* If it doesn't have a uevent file, it isn't a real device */
+      if (!g_file_get_contents (uevent, &text, NULL, NULL))
+        continue;
+
+      if (subsystem != NULL)
+        {
+          g_autofree char *subsys = g_build_filename (ancestor, "subsystem", NULL);
+          g_autofree char *target = glnx_readlinkat_malloc (AT_FDCWD, subsys,
+                                                            NULL, NULL);
+          const char *slash = NULL;
+
+          if (target != NULL)
+            slash = strrchr (target, '/');
+
+          if (slash == NULL || strcmp (slash + 1, subsystem) != 0)
+            continue;
+        }
+
+      if (devtype != NULL
+          && !_srt_input_device_uevent_field_equals (text, "DEVTYPE", devtype))
+        continue;
+
+      if (uevent_out != NULL)
+        *uevent_out = g_steal_pointer (&text);
+
+      return g_steal_pointer (&ancestor);
+    }
+
+  return NULL;
+}
+
+static GQuark quark_hidraw = 0;
+static GQuark quark_input = 0;
+
+struct _SrtDirectInputDevice
+{
+  GObject parent;
+
+  char *sys_path;
+  gchar *dev_node;
+  GQuark subsystem;
+
+  struct
+  {
+    gchar *sys_path;
+  } hid_ancestor;
+
+  struct
+  {
+    gchar *sys_path;
+  } input_ancestor;
+
+  struct
+  {
+    gchar *sys_path;
+    gchar *manufacturer;
+    gchar *product;
+    gchar *serial;
+    guint32 product_id;
+    guint32 vendor_id;
+    guint32 device_version;
+  } usb_device_ancestor;
+
+  struct
+  {
+    SrtEvdevCapabilities caps;
+    gchar *name;
+    gchar *phys;
+    gchar *uniq;
+    guint32 bus_type;
+    guint32 product_id;
+    guint32 vendor_id;
+    guint32 version;
+  } evdev;
+
+  struct
+  {
+    gchar *name;
+    gchar *phys;
+    gchar *uniq;
+    guint32 bus_type;
+    guint32 product_id;
+    guint32 vendor_id;
+  } hid;
+
+  SrtInputDeviceInterfaceFlags iface_flags;
+};
+
+struct _SrtDirectInputDeviceClass
+{
+  GObjectClass parent;
+};
+
+struct _SrtDirectInputDeviceMonitor
+{
+  GObject parent;
+  GHashTable *devices;
+  GMainContext *monitor_context;
+  GSource *monitor_source;
+  SrtInputDeviceMonitorFlags flags;
+  gboolean want_evdev;
+  gboolean want_hidraw;
+  int inotify_fd;
+  int dev_watch;
+  int devinput_watch;
+  enum
+  {
+    NOT_STARTED = 0,
+    STARTED,
+    STOPPED
+  } state;
+};
+
+struct _SrtDirectInputDeviceMonitorClass
+{
+  GObjectClass parent;
+};
+
+G_DEFINE_TYPE_WITH_CODE (SrtDirectInputDevice,
+                         _srt_direct_input_device,
+                         G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (SRT_TYPE_INPUT_DEVICE,
+                                                srt_direct_input_device_iface_init))
+
+G_DEFINE_TYPE_WITH_CODE (SrtDirectInputDeviceMonitor,
+                         _srt_direct_input_device_monitor,
+                         G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (SRT_TYPE_INPUT_DEVICE_MONITOR,
+                                                srt_direct_input_device_monitor_iface_init))
+
+static void
+_srt_direct_input_device_init (SrtDirectInputDevice *self)
+{
+}
+
+static void
+srt_direct_input_device_finalize (GObject *object)
+{
+  SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (object);
+
+  g_clear_pointer (&self->sys_path, free);
+  g_clear_pointer (&self->dev_node, g_free);
+  g_clear_pointer (&self->hid_ancestor.sys_path, g_free);
+  g_clear_pointer (&self->input_ancestor.sys_path, g_free);
+  g_clear_pointer (&self->usb_device_ancestor.sys_path, g_free);
+  g_clear_pointer (&self->usb_device_ancestor.manufacturer, g_free);
+  g_clear_pointer (&self->usb_device_ancestor.product, g_free);
+  g_clear_pointer (&self->usb_device_ancestor.serial, g_free);
+  g_clear_pointer (&self->hid.name, g_free);
+  g_clear_pointer (&self->hid.phys, g_free);
+  g_clear_pointer (&self->hid.uniq, g_free);
+  g_clear_pointer (&self->evdev.name, g_free);
+  g_clear_pointer (&self->evdev.phys, g_free);
+  g_clear_pointer (&self->evdev.uniq, g_free);
+
+  G_OBJECT_CLASS (_srt_direct_input_device_parent_class)->finalize (object);
+}
+
+static void
+_srt_direct_input_device_class_init (SrtDirectInputDeviceClass *cls)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (cls);
+
+  object_class->finalize = srt_direct_input_device_finalize;
+}
+
+static SrtInputDeviceInterfaceFlags
+srt_direct_input_device_get_interface_flags (SrtInputDevice *device)
+{
+  SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device);
+
+  return self->iface_flags;
+}
+
+static const char *
+srt_direct_input_device_get_dev_node (SrtInputDevice *device)
+{
+  SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device);
+
+  return self->dev_node;
+}
+
+static const char *
+srt_direct_input_device_get_subsystem (SrtInputDevice *device)
+{
+  SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device);
+
+  return g_quark_to_string (self->subsystem);
+}
+
+static const char *
+srt_direct_input_device_get_sys_path (SrtInputDevice *device)
+{
+  SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device);
+
+  return self->sys_path;
+}
+
+static const SrtEvdevCapabilities *
+srt_direct_input_device_peek_event_capabilities (SrtInputDevice *device)
+{
+  SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device);
+
+  return &self->evdev.caps;
+}
+
+static const char *
+srt_direct_input_device_get_hid_sys_path (SrtInputDevice *device)
+{
+  SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device);
+
+  return self->hid_ancestor.sys_path;
+}
+
+static gboolean
+srt_direct_input_device_get_hid_identity (SrtInputDevice *device,
+                                          unsigned int *bus_type,
+                                          unsigned int *vendor_id,
+                                          unsigned int *product_id,
+                                          const char **name,
+                                          const char **phys,
+                                          const char **uniq)
+{
+  SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device);
+
+  if (self->hid_ancestor.sys_path == NULL
+      && (self->iface_flags & SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID) == 0)
+    return FALSE;
+
+  if (bus_type != NULL)
+    *bus_type = self->hid.bus_type;
+
+  if (vendor_id != NULL)
+    *vendor_id = self->hid.vendor_id;
+
+  if (product_id != NULL)
+    *product_id = self->hid.product_id;
+
+  if (name != NULL)
+    *name = self->hid.name;
+
+  if (phys != NULL)
+    *phys = self->hid.phys;
+
+  if (uniq != NULL)
+    *uniq = self->hid.uniq;
+
+  return TRUE;
+}
+
+static const char *
+srt_direct_input_device_get_input_sys_path (SrtInputDevice *device)
+{
+  SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device);
+
+  return self->input_ancestor.sys_path;
+}
+
+static gboolean
+srt_direct_input_device_get_input_identity (SrtInputDevice *device,
+                                            unsigned int *bus_type,
+                                            unsigned int *vendor_id,
+                                            unsigned int *product_id,
+                                            unsigned int *version,
+                                            const char **name,
+                                            const char **phys,
+                                            const char **uniq)
+{
+  SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device);
+
+  if (self->input_ancestor.sys_path == NULL
+      && (self->iface_flags & SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT) == 0)
+    return FALSE;
+
+  if (bus_type != NULL)
+    *bus_type = self->evdev.bus_type;
+
+  if (vendor_id != NULL)
+    *vendor_id = self->evdev.vendor_id;
+
+  if (product_id != NULL)
+    *product_id = self->evdev.product_id;
+
+  if (version != NULL)
+    *version = self->evdev.version;
+
+  if (name != NULL)
+    *name = self->evdev.name;
+
+  if (phys != NULL)
+    *phys = self->evdev.phys;
+
+  if (uniq != NULL)
+    *uniq = self->evdev.uniq;
+
+  return TRUE;
+}
+
+static const char *
+srt_direct_input_device_get_usb_device_sys_path (SrtInputDevice *device)
+{
+  SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device);
+
+  return self->usb_device_ancestor.sys_path;
+}
+
+static gboolean
+srt_direct_input_device_get_usb_device_identity (SrtInputDevice *device,
+                                                 unsigned int *vendor_id,
+                                                 unsigned int *product_id,
+                                                 unsigned int *device_version,
+                                                 const char **manufacturer,
+                                                 const char **product,
+                                                 const char **serial)
+{
+  SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device);
+
+  if (self->usb_device_ancestor.sys_path == NULL)
+    return FALSE;
+
+  if (vendor_id != NULL)
+    *vendor_id = self->usb_device_ancestor.vendor_id;
+
+  if (product_id != NULL)
+    *product_id = self->usb_device_ancestor.product_id;
+
+  if (device_version != NULL)
+    *device_version = self->usb_device_ancestor.device_version;
+
+  if (manufacturer != NULL)
+    *manufacturer = self->usb_device_ancestor.manufacturer;
+
+  if (product != NULL)
+    *product = self->usb_device_ancestor.product;
+
+  if (serial != NULL)
+    *serial = self->usb_device_ancestor.serial;
+
+  return TRUE;
+}
+
+static void
+srt_direct_input_device_iface_init (SrtInputDeviceInterface *iface)
+{
+#define IMPLEMENT(x) iface->x = srt_direct_input_device_ ## x
+
+  IMPLEMENT (get_interface_flags);
+  IMPLEMENT (get_dev_node);
+  IMPLEMENT (get_sys_path);
+  IMPLEMENT (get_subsystem);
+  IMPLEMENT (peek_event_capabilities);
+
+  IMPLEMENT (get_hid_sys_path);
+  IMPLEMENT (get_input_sys_path);
+  IMPLEMENT (get_usb_device_sys_path);
+
+  IMPLEMENT (get_hid_identity);
+  IMPLEMENT (get_input_identity);
+  IMPLEMENT (get_usb_device_identity);
+
+#undef IMPLEMENT
+}
+
+typedef enum
+{
+  PROP_0,
+  PROP_FLAGS,
+  PROP_IS_ACTIVE,
+  N_PROPERTIES
+} Property;
+
+static void
+_srt_direct_input_device_monitor_init (SrtDirectInputDeviceMonitor *self)
+{
+  self->monitor_context = g_main_context_ref_thread_default ();
+  self->state = NOT_STARTED;
+  self->devices = g_hash_table_new_full (g_str_hash,
+                                         g_str_equal,
+                                         NULL,
+                                         g_object_unref);
+  self->inotify_fd = -1;
+  self->dev_watch = -1;
+  self->devinput_watch = -1;
+}
+
+static void
+srt_direct_input_device_monitor_get_property (GObject *object,
+                                            guint prop_id,
+                                            GValue *value,
+                                            GParamSpec *pspec)
+{
+  SrtDirectInputDeviceMonitor *self = SRT_DIRECT_INPUT_DEVICE_MONITOR (object);
+
+  switch ((Property) prop_id)
+    {
+      case PROP_IS_ACTIVE:
+        g_value_set_boolean (value, (self->state == STARTED));
+        break;
+
+      case PROP_FLAGS:
+        g_value_set_flags (value, self->flags);
+        break;
+
+      case PROP_0:
+      case N_PROPERTIES:
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+srt_direct_input_device_monitor_set_property (GObject *object,
+                                            guint prop_id,
+                                            const GValue *value,
+                                            GParamSpec *pspec)
+{
+  SrtDirectInputDeviceMonitor *self = SRT_DIRECT_INPUT_DEVICE_MONITOR (object);
+
+  switch ((Property) prop_id)
+    {
+      case PROP_FLAGS:
+        /* Construct-only */
+        g_return_if_fail (self->flags == 0);
+        self->flags = g_value_get_flags (value);
+        break;
+
+      case PROP_IS_ACTIVE:
+      case PROP_0:
+      case N_PROPERTIES:
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+srt_direct_input_device_monitor_dispose (GObject *object)
+{
+  srt_input_device_monitor_stop (SRT_INPUT_DEVICE_MONITOR (object));
+  G_OBJECT_CLASS (_srt_direct_input_device_monitor_parent_class)->dispose (object);
+}
+
+static void
+srt_direct_input_device_monitor_finalize (GObject *object)
+{
+  srt_input_device_monitor_stop (SRT_INPUT_DEVICE_MONITOR (object));
+  G_OBJECT_CLASS (_srt_direct_input_device_monitor_parent_class)->finalize (object);
+}
+
+static void
+_srt_direct_input_device_monitor_class_init (SrtDirectInputDeviceMonitorClass *cls)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (cls);
+
+  object_class->get_property = srt_direct_input_device_monitor_get_property;
+  object_class->set_property = srt_direct_input_device_monitor_set_property;
+  object_class->dispose = srt_direct_input_device_monitor_dispose;
+  object_class->finalize = srt_direct_input_device_monitor_finalize;
+
+  g_object_class_override_property (object_class, PROP_FLAGS, "flags");
+  g_object_class_override_property (object_class, PROP_IS_ACTIVE, "is-active");
+
+  quark_hidraw = g_quark_from_static_string ("hidraw");
+  quark_input = g_quark_from_static_string ("input");
+}
+
+/*
+ * Get a sysfs attribute that is a string.
+ *
+ * On success, set *out and return TRUE.
+ * On failure, leave *out untouched and return FALSE.
+ */
+static gboolean
+dup_string (const char *sys_path,
+            const char *attribute,
+            gchar **out)
+{
+  g_autofree gchar *text = NULL;
+  g_autofree gchar *child = NULL;
+
+  if (sys_path == NULL)
+    return FALSE;
+
+  child = g_build_filename (sys_path, attribute, NULL);
+
+  if (!g_file_get_contents (child, &text, NULL, NULL))
+    return FALSE;
+
+  g_strchomp (text);
+
+  if (out != NULL)
+    *out = g_steal_pointer (&text);
+
+  return TRUE;
+}
+
+/*
+ * Get a sysfs attribute that is a uint32 (or smaller) in hexadecimal
+ * (with or without 0x prefix).
+ *
+ * On success, set *out and return TRUE.
+ * On failure, leave *out untouched and return FALSE.
+ */
+static gboolean
+get_uint32_hex (const char *sys_path,
+                const char *attribute,
+                guint32 *out)
+{
+  g_autofree gchar *buf = NULL;
+  const char *tmp;
+  guint64 ret;
+  gchar *endptr;
+
+  if (!dup_string (sys_path, attribute, &buf))
+    return FALSE;
+
+  tmp = buf;
+
+  if (tmp[0] == '0' && (tmp[1] == 'x' || tmp[1] == 'X'))
+    tmp += 2;
+
+  ret = g_ascii_strtoull (tmp, &endptr, 16);
+
+  if (endptr == NULL
+      || (*endptr != '\0' && *endptr != '\n')
+      || ret > G_MAXUINT32)
+    return FALSE;
+
+  if (out != NULL)
+    *out = (guint32) ret;
+
+  return TRUE;
+}
+
+static void
+read_hid_ancestor (SrtDirectInputDevice *device)
+{
+  g_autofree gchar *uevent = NULL;
+
+  if (device->hid_ancestor.sys_path == NULL)
+    return;
+
+  if (!dup_string (device->hid_ancestor.sys_path, "uevent", &uevent))
+    return;
+
+  _srt_get_identity_from_hid_uevent (uevent,
+                                     &device->hid.bus_type,
+                                     &device->hid.vendor_id,
+                                     &device->hid.product_id,
+                                     &device->hid.name,
+                                     &device->hid.phys,
+                                     &device->hid.uniq);
+}
+
+static void
+read_input_ancestor (SrtDirectInputDevice *device)
+{
+  if (device->input_ancestor.sys_path == NULL)
+    return;
+
+  get_uint32_hex (device->input_ancestor.sys_path, "id/bustype",
+                  &device->evdev.bus_type);
+  get_uint32_hex (device->input_ancestor.sys_path, "id/vendor",
+                  &device->evdev.vendor_id);
+  get_uint32_hex (device->input_ancestor.sys_path, "id/product",
+                  &device->evdev.product_id);
+  get_uint32_hex (device->input_ancestor.sys_path, "id/version",
+                  &device->evdev.version);
+  dup_string (device->input_ancestor.sys_path, "name", &device->evdev.name);
+  dup_string (device->input_ancestor.sys_path, "phys", &device->evdev.phys);
+  dup_string (device->input_ancestor.sys_path, "uniq", &device->evdev.uniq);
+}
+
+static void
+read_usb_device_ancestor (SrtDirectInputDevice *device)
+{
+  if (device->usb_device_ancestor.sys_path == NULL)
+    return;
+
+  get_uint32_hex (device->usb_device_ancestor.sys_path, "idVendor",
+                  &device->usb_device_ancestor.vendor_id);
+  get_uint32_hex (device->usb_device_ancestor.sys_path, "idProduct",
+                  &device->usb_device_ancestor.product_id);
+  get_uint32_hex (device->usb_device_ancestor.sys_path, "bcdDevice",
+                  &device->usb_device_ancestor.device_version);
+  dup_string (device->usb_device_ancestor.sys_path, "manufacturer",
+              &device->usb_device_ancestor.manufacturer);
+  dup_string (device->usb_device_ancestor.sys_path, "product",
+              &device->usb_device_ancestor.product);
+}
+
+static void
+add_device (SrtDirectInputDeviceMonitor *self,
+            const char *devnode,
+            GQuark subsystem)
+{
+  SrtDirectInputDevice *device = NULL;
+  const char *slash = strrchr (devnode, '/');
+  g_autofree char *sys_symlink = NULL;
+  int fd;
+
+  /* Never add a device for a second time */
+  if (g_hash_table_contains (self->devices, devnode))
+    return;
+
+  if (slash == NULL || slash[1] == '\0')
+    return;
+
+  device = g_object_new (SRT_TYPE_DIRECT_INPUT_DEVICE, NULL);
+  device->dev_node = g_strdup (devnode);
+  device->subsystem = subsystem;
+
+  sys_symlink = g_build_filename ("/sys/class",
+                                  g_quark_to_string (subsystem),
+                                  slash + 1, NULL);
+  device->sys_path = realpath (sys_symlink, NULL);
+
+  if (device->sys_path == NULL)
+    {
+      g_debug ("unable to get real path of %s: %s",
+               sys_symlink, g_strerror (errno));
+      g_object_unref (device);
+      return;
+    }
+
+  if (subsystem == quark_hidraw)
+    device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID;
+
+  g_debug ("Trying to open %s", devnode);
+
+  fd = open (devnode, O_RDONLY | O_NONBLOCK | _SRT_INPUT_DEVICE_ALWAYS_OPEN_FLAGS);
+
+  if (fd >= 0)
+    {
+      /* The permissions are already OK for use */
+      g_debug ("Opened %s", devnode);
+      device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE;
+
+      if (_srt_get_identity_from_raw_hid (fd,
+                                          &device->hid.bus_type,
+                                          &device->hid.vendor_id,
+                                          &device->hid.product_id,
+                                          &device->hid.name,
+                                          &device->hid.phys,
+                                          &device->hid.uniq))
+        {
+          g_debug ("%s is raw HID: bus type 0x%04x, vendor 0x%04x, product 0x%04x, \"%s\"",
+                   devnode,
+                   device->hid.bus_type,
+                   device->hid.vendor_id,
+                   device->hid.product_id,
+                   device->hid.name);
+          device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID;
+        }
+
+      if (_srt_get_identity_from_evdev (fd,
+                                        &device->evdev.bus_type,
+                                        &device->evdev.vendor_id,
+                                        &device->evdev.product_id,
+                                        &device->evdev.version,
+                                        &device->evdev.name,
+                                        &device->evdev.phys,
+                                        &device->evdev.uniq))
+        {
+          g_debug ("%s is evdev: bus type 0x%04x, vendor 0x%04x, product 0x%04x, version 0x%04x, \"%s\"",
+                   devnode,
+                   device->evdev.bus_type,
+                   device->evdev.vendor_id,
+                   device->evdev.product_id,
+                   device->evdev.version,
+                   device->evdev.name);
+          device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT;
+        }
+
+      _srt_evdev_capabilities_set_from_evdev (&device->evdev.caps, fd);
+      close (fd);
+    }
+  else
+    {
+      /* We'll get another chance after the permissions get updated */
+      g_debug ("Unable to open %s to identify it: %s", devnode, g_strerror (errno));
+      g_object_unref (device);
+      return;
+    }
+
+  fd = open (devnode, O_RDWR | O_NONBLOCK | _SRT_INPUT_DEVICE_ALWAYS_OPEN_FLAGS);
+
+  if (fd >= 0)
+    {
+      g_debug ("Opened %s rw", devnode);
+      device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_READ_WRITE;
+      close (fd);
+    }
+
+  if ((device->iface_flags & (SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT
+                              | SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID)) == 0)
+    {
+      g_debug ("%s is neither evdev nor raw HID, ignoring", devnode);
+      g_object_unref (device);
+      return;
+    }
+
+  device->hid_ancestor.sys_path = get_ancestor_with_subsystem_devtype (device->sys_path,
+                                                                       "hid", NULL,
+                                                                       NULL);
+  read_hid_ancestor (device);
+  device->input_ancestor.sys_path = find_input_ancestor (device->sys_path);
+  read_input_ancestor (device);
+
+  if (device->hid.bus_type == BUS_USB || device->evdev.bus_type == BUS_USB)
+    {
+      device->usb_device_ancestor.sys_path = get_ancestor_with_subsystem_devtype (device->sys_path,
+                                                                                  "usb",
+                                                                                  "usb_device",
+                                                                                  NULL);
+      read_usb_device_ancestor (device);
+    }
+
+  g_hash_table_replace (self->devices, device->dev_node, device);
+  _srt_input_device_monitor_emit_added (SRT_INPUT_DEVICE_MONITOR (self),
+                                        SRT_INPUT_DEVICE (device));
+}
+
+static void
+remove_device (SrtDirectInputDeviceMonitor *self,
+               const char *devnode)
+{
+  void *device = NULL;
+
+  g_debug ("Removing device %s", devnode);
+
+  if (g_hash_table_lookup_extended (self->devices, devnode, NULL, &device))
+    {
+      g_hash_table_steal (self->devices, devnode);
+      _srt_input_device_monitor_emit_removed (SRT_INPUT_DEVICE_MONITOR (self),
+                                              device);
+      g_object_unref (device);
+    }
+}
+
+static gboolean
+srt_direct_input_device_monitor_cb (int fd,
+                                    GIOCondition condition,
+                                    void *user_data)
+{
+  SrtDirectInputDeviceMonitor *self = SRT_DIRECT_INPUT_DEVICE_MONITOR (user_data);
+  union
+  {
+    struct inotify_event event;
+    char storage[4096];
+    char enough_for_inotify[sizeof (struct inotify_event) + NAME_MAX + 1];
+  } buf;
+  ssize_t bytes;
+  size_t remain = 0;
+  size_t len;
+
+  g_return_val_if_fail (SRT_IS_DIRECT_INPUT_DEVICE_MONITOR (self), G_SOURCE_REMOVE);
+
+  bytes = read (self->inotify_fd, &buf, sizeof (buf));
+
+  if (bytes > 0)
+    remain = (size_t) bytes;
+
+  while (remain > 0)
+    {
+      g_return_val_if_fail (remain >= sizeof (struct inotify_event), G_SOURCE_REMOVE);
+
+      if (buf.event.len > 0)
+        {
+          if (buf.event.wd == self->dev_watch)
+            {
+              if (g_str_has_prefix (buf.event.name, "hidraw")
+                  && _srt_str_is_integer (buf.event.name + strlen ("hidraw")))
+                {
+                  g_autofree gchar *path = g_build_filename ("/dev",
+                                                             buf.event.name,
+                                                             NULL);
+
+                  if (buf.event.mask & (IN_CREATE | IN_MOVED_TO | IN_ATTRIB))
+                    add_device (self, path, quark_hidraw);
+                  else if (buf.event.mask & (IN_DELETE | IN_MOVED_FROM))
+                    remove_device (self, path);
+                }
+            }
+          else if (buf.event.wd == self->devinput_watch)
+            {
+              if (g_str_has_prefix (buf.event.name, "event")
+                  && _srt_str_is_integer (buf.event.name + strlen ("event")))
+                {
+                  g_autofree gchar *path = g_build_filename ("/dev/input",
+                                                             buf.event.name,
+                                                             NULL);
+
+                  if (buf.event.mask & (IN_CREATE | IN_MOVED_TO | IN_ATTRIB))
+                    add_device (self, path, quark_input);
+                  else if (buf.event.mask & (IN_DELETE | IN_MOVED_FROM))
+                    remove_device (self, path);
+                }
+            }
+        }
+
+      len = sizeof (struct inotify_event) + buf.event.len;
+      remain -= len;
+
+      if (remain != 0)
+        memmove (&buf.storage[0], &buf.storage[len], remain);
+    }
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+srt_direct_input_device_monitor_request_raw_hid (SrtInputDeviceMonitor *monitor)
+{
+  SrtDirectInputDeviceMonitor *self = SRT_DIRECT_INPUT_DEVICE_MONITOR (monitor);
+
+  g_return_if_fail (SRT_IS_DIRECT_INPUT_DEVICE_MONITOR (monitor));
+  g_return_if_fail (self->state == NOT_STARTED);
+
+  self->want_hidraw = TRUE;
+}
+
+static void
+srt_direct_input_device_monitor_request_evdev (SrtInputDeviceMonitor *monitor)
+{
+  SrtDirectInputDeviceMonitor *self = SRT_DIRECT_INPUT_DEVICE_MONITOR (monitor);
+
+  g_return_if_fail (SRT_IS_DIRECT_INPUT_DEVICE_MONITOR (monitor));
+  g_return_if_fail (self->state == NOT_STARTED);
+
+  self->want_evdev = TRUE;
+}
+
+static gboolean
+enumerate_cb (gpointer user_data)
+{
+  SrtDirectInputDeviceMonitor *self = SRT_DIRECT_INPUT_DEVICE_MONITOR (user_data);
+
+  if (self->want_hidraw)
+    {
+      g_auto(GLnxDirFdIterator) iter = { FALSE };
+      g_autoptr(GError) error = NULL;
+
+      if (!glnx_dirfd_iterator_init_at (AT_FDCWD, "/dev", TRUE, &iter, &error))
+        {
+          g_debug ("Unable to open /dev/: %s", error->message);
+        }
+
+      while (error == NULL)
+        {
+          struct dirent *dent;
+
+          if (!glnx_dirfd_iterator_next_dent (&iter, &dent, NULL, &error))
+            {
+              g_debug ("Unable to iterate over /dev/: %s",
+                       error->message);
+              break;
+            }
+
+          if (dent == NULL)
+            break;
+
+          if (g_str_has_prefix (dent->d_name, "hidraw")
+              && _srt_str_is_integer (dent->d_name + strlen ("hidraw")))
+            {
+              g_autofree gchar *path = g_build_filename ("/dev", dent->d_name, NULL);
+              add_device (self, path, quark_hidraw);
+            }
+        }
+    }
+
+  if (self->want_evdev)
+    {
+      g_auto(GLnxDirFdIterator) iter = { FALSE };
+      g_autoptr(GError) error = NULL;
+
+      if (!glnx_dirfd_iterator_init_at (AT_FDCWD, "/dev/input", TRUE, &iter,
+                                        &error))
+        {
+          g_debug ("Unable to open /dev/input/: %s", error->message);
+        }
+
+      while (error == NULL)
+        {
+          struct dirent *dent;
+
+          if (!glnx_dirfd_iterator_next_dent (&iter, &dent, NULL, &error))
+            {
+              g_debug ("Unable to iterate over /dev/input/: %s",
+                       error->message);
+              break;
+            }
+
+          if (dent == NULL)
+            break;
+
+          if (g_str_has_prefix (dent->d_name, "event")
+              && _srt_str_is_integer (dent->d_name + strlen ("event")))
+            {
+              g_autofree gchar *path = g_build_filename ("/dev/input", dent->d_name, NULL);
+              add_device (self, path, quark_input);
+            }
+        }
+    }
+
+  _srt_input_device_monitor_emit_all_for_now (SRT_INPUT_DEVICE_MONITOR (self));
+  return G_SOURCE_REMOVE;
+}
+
+static gboolean
+srt_direct_input_device_monitor_start (SrtInputDeviceMonitor *monitor,
+                                       GError **error)
+{
+  SrtDirectInputDeviceMonitor *self = SRT_DIRECT_INPUT_DEVICE_MONITOR (monitor);
+
+  g_return_val_if_fail (SRT_IS_DIRECT_INPUT_DEVICE_MONITOR (monitor), FALSE);
+  g_return_val_if_fail (self->state == NOT_STARTED, FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+  self->state = STARTED;
+
+  if ((self->flags & SRT_INPUT_DEVICE_MONITOR_FLAGS_ONCE) == 0
+      && (self->want_hidraw || self->want_evdev))
+    {
+      self->inotify_fd = inotify_init1 (IN_NONBLOCK | IN_CLOEXEC);
+
+      if (self->inotify_fd < 0)
+        return glnx_throw_errno_prefix (error, "inotify_init1");
+
+      if (self->want_hidraw)
+        {
+          self->dev_watch = inotify_add_watch (self->inotify_fd, "/dev",
+                                               IN_CREATE | IN_DELETE | IN_MOVE | IN_ATTRIB);
+
+          if (self->dev_watch < 0)
+            return glnx_throw_errno_prefix (error, "inotify_add_watch");
+        }
+
+      if (self->want_evdev)
+        {
+          self->devinput_watch = inotify_add_watch (self->inotify_fd, "/dev/input",
+                                                    IN_CREATE | IN_DELETE | IN_MOVE | IN_ATTRIB);
+
+          if (self->devinput_watch < 0)
+            return glnx_throw_errno_prefix (error, "inotify_add_watch");
+        }
+
+      self->monitor_source = g_unix_fd_source_new (self->inotify_fd, G_IO_IN);
+      g_source_set_callback (self->monitor_source,
+                             (GSourceFunc) G_CALLBACK (srt_direct_input_device_monitor_cb),
+                             self, NULL);
+      g_source_set_priority (self->monitor_source, G_PRIORITY_DEFAULT);
+      g_source_attach (self->monitor_source, self->monitor_context);
+    }
+
+  /* Make sure the signals for the initial batch of devices are emitted in
+   * the correct main-context */
+  g_main_context_invoke_full (self->monitor_context, G_PRIORITY_DEFAULT,
+                              enumerate_cb, g_object_ref (self),
+                              g_object_unref);
+  return TRUE;
+}
+
+static void
+srt_direct_input_device_monitor_stop (SrtInputDeviceMonitor *monitor)
+{
+  SrtDirectInputDeviceMonitor *self = SRT_DIRECT_INPUT_DEVICE_MONITOR (monitor);
+
+  self->state = STOPPED;
+
+  if (self->monitor_source != NULL)
+    g_source_destroy (self->monitor_source);
+
+  g_clear_pointer (&self->monitor_source, g_source_unref);
+  g_clear_pointer (&self->monitor_context, g_main_context_unref);
+  g_clear_pointer (&self->devices, g_hash_table_unref);
+
+  if (self->inotify_fd >= 0)
+    {
+      close (self->inotify_fd);
+      self->dev_watch = -1;
+      self->devinput_watch = -1;
+      self->inotify_fd = -1;
+    }
+}
+
+static void
+srt_direct_input_device_monitor_iface_init (SrtInputDeviceMonitorInterface *iface)
+{
+#define IMPLEMENT(x) iface->x = srt_direct_input_device_monitor_ ## x
+  IMPLEMENT (request_evdev);
+  IMPLEMENT (request_raw_hid);
+  IMPLEMENT (start);
+  IMPLEMENT (stop);
+#undef IMPLEMENT
+}
diff --git a/steam-runtime-tools/input-device-internal.h b/steam-runtime-tools/input-device-internal.h
new file mode 100644
index 0000000000000000000000000000000000000000..c18c8ea9e9dd0fd4f95c2402af554470ab8d90c6
--- /dev/null
+++ b/steam-runtime-tools/input-device-internal.h
@@ -0,0 +1,251 @@
+/*<private_header>*/
+/*
+ * Input device internals, with parts based on SDL code.
+ *
+ * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org>
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: Zlib
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ */
+
+#pragma once
+
+#include "steam-runtime-tools/input-device.h"
+
+#include <linux/input.h>
+
+#define _SRT_INPUT_DEVICE_ALWAYS_OPEN_FLAGS (O_CLOEXEC | O_NOCTTY)
+
+/* Macros for the bitfield encoding used by the EVIOCGBIT ioctl. */
+#define BITS_PER_LONG           (sizeof (unsigned long) * CHAR_BIT)
+#define LONGS_FOR_BITS(x)       ((((x)-1)/BITS_PER_LONG)+1)
+#define CHOOSE_BIT(x)           ((x)%BITS_PER_LONG)
+#define CHOOSE_LONG(x)          ((x)/BITS_PER_LONG)
+
+/* Note that this always returns 0 or 1, so it can be considered to return
+ * a gboolean */
+#define test_bit(bit, ulongs)   ((ulongs[CHOOSE_LONG(bit)] >> CHOOSE_BIT(bit)) & 1)
+
+#define set_bit(bit, ulongs)    do { ulongs[CHOOSE_LONG(bit)] |= (1UL << CHOOSE_BIT(bit)); } while (0)
+
+/*
+ * @bit_number: A bit number, where 0 is the least significant bit of bits[0]
+ * @bits: (in) (array length=n_longs): Bitfields encoded as longs, where
+ *  the most significant bit of bits[0] is one place less significant than
+ *  the least significant bit of bits[1]
+ * @n_longs: The number of elements in @bits
+ *
+ * Decode the bitfield encoding used by the EVIOCGBIT ioctl.
+ */
+static inline gboolean
+test_bit_checked (unsigned int bit_number,
+                  const unsigned long *bits,
+                  size_t n_longs)
+{
+  if (CHOOSE_LONG (bit_number) >= n_longs)
+    return FALSE;
+
+  return test_bit (bit_number, bits);
+}
+
+/* We assume a buffer large enough for all the keyboard/button codes is
+ * also sufficient for all the less numerous event types. */
+#define HIGHEST_EVENT_CODE (KEY_MAX)
+G_STATIC_ASSERT (KEY_MAX >= EV_MAX);
+G_STATIC_ASSERT (KEY_MAX >= ABS_MAX);
+G_STATIC_ASSERT (KEY_MAX >= REL_MAX);
+G_STATIC_ASSERT (KEY_MAX >= FF_MAX);
+
+/* Some of the event codes we're interested in weren't in in older kernels,
+ * like the one whose headers we use for SteamRT 1 'scout'. */
+
+#ifndef ABS_RESERVED
+# define ABS_RESERVED 0x2e
+#endif
+
+G_STATIC_ASSERT (ABS_RESERVED < KEY_MAX);
+
+#ifndef REL_RESERVED
+# define REL_RESERVED 0x0a
+#endif
+
+#ifndef REL_WHEEL_HI_RES
+# define REL_WHEEL_HI_RES 0x0b
+#endif
+
+#ifndef REL_HWHEEL_HI_RES
+# define REL_HWHEEL_HI_RES 0x0c
+#endif
+
+G_STATIC_ASSERT (REL_HWHEEL_HI_RES < KEY_MAX);
+
+#ifndef BTN_DPAD_UP
+  /* We assume that these were all defined together */
+# define BTN_DPAD_UP 0x220
+# define BTN_DPAD_DOWN 0x221
+# define BTN_DPAD_LEFT 0x222
+# define BTN_DPAD_RIGHT 0x223
+#endif
+
+#ifndef KEY_MACRO1
+# define KEY_MACRO1 0x290
+#endif
+
+G_STATIC_ASSERT (KEY_MACRO1 < KEY_MAX);
+
+#ifndef INPUT_PROP_TOPBUTTONPAD
+# define INPUT_PROP_TOPBUTTONPAD 0x04
+#endif
+#ifndef INPUT_PROP_POINTING_STICK
+# define INPUT_PROP_POINTING_STICK 0x05
+#endif
+#ifndef INPUT_PROP_ACCELEROMETER
+# define INPUT_PROP_ACCELEROMETER 0x06
+#endif
+
+G_STATIC_ASSERT (INPUT_PROP_ACCELEROMETER < INPUT_PROP_MAX);
+
+typedef struct
+{
+  unsigned long ev[LONGS_FOR_BITS (EV_MAX)];
+  unsigned long keys[LONGS_FOR_BITS (KEY_MAX)];
+  unsigned long abs[LONGS_FOR_BITS (ABS_MAX)];
+  unsigned long rel[LONGS_FOR_BITS (REL_MAX)];
+  unsigned long ff[LONGS_FOR_BITS (FF_MAX)];
+  unsigned long props[LONGS_FOR_BITS (INPUT_PROP_MAX)];
+} SrtEvdevCapabilities;
+
+const unsigned long *_srt_evdev_capabilities_get_bits (const SrtEvdevCapabilities *caps,
+                                                       unsigned int type,
+                                                       size_t *n_longs);
+gboolean _srt_evdev_capabilities_set_from_evdev (SrtEvdevCapabilities *caps,
+                                                 int fd);
+void _srt_evdev_capabilities_dump (const SrtEvdevCapabilities *caps);
+SrtInputDeviceTypeFlags _srt_evdev_capabilities_guess_type (const SrtEvdevCapabilities *caps);
+
+struct _SrtInputDeviceInterface
+{
+  GTypeInterface parent;
+
+  SrtInputDeviceTypeFlags (*get_type_flags) (SrtInputDevice *device);
+  SrtInputDeviceInterfaceFlags (*get_interface_flags) (SrtInputDevice *device);
+
+  const char * (*get_dev_node) (SrtInputDevice *device);
+  const char * (*get_sys_path) (SrtInputDevice *device);
+  const char * (*get_subsystem) (SrtInputDevice *device);
+  gchar ** (*dup_udev_properties) (SrtInputDevice *device);
+  gchar * (*dup_uevent) (SrtInputDevice *device);
+  gboolean (*get_identity) (SrtInputDevice *device,
+                            unsigned int *bus_type,
+                            unsigned int *vendor_id,
+                            unsigned int *product_id,
+                            unsigned int *version);
+  const SrtEvdevCapabilities *(*peek_event_capabilities) (SrtInputDevice *device);
+
+  const char * (*get_hid_sys_path) (SrtInputDevice *device);
+  gchar * (*dup_hid_uevent) (SrtInputDevice *device);
+  gboolean (*get_hid_identity) (SrtInputDevice *device,
+                                unsigned int *bus_type,
+                                unsigned int *vendor_id,
+                                unsigned int *product_id,
+                                const char **name,
+                                const char **phys,
+                                const char **uniq);
+
+  const char * (*get_input_sys_path) (SrtInputDevice *device);
+  gchar * (*dup_input_uevent) (SrtInputDevice *device);
+  gboolean (*get_input_identity) (SrtInputDevice *device,
+                                  unsigned int *bus_type,
+                                  unsigned int *vendor_id,
+                                  unsigned int *product_id,
+                                  unsigned int *version,
+                                  const char **name,
+                                  const char **phys,
+                                  const char **uniq);
+
+  const char * (*get_usb_device_sys_path) (SrtInputDevice *device);
+  gchar * (*dup_usb_device_uevent) (SrtInputDevice *device);
+  gboolean (*get_usb_device_identity) (SrtInputDevice *device,
+                                       unsigned int *vendor_id,
+                                       unsigned int *product_id,
+                                       unsigned int *device_version,
+                                       const char **manufacturer,
+                                       const char **product,
+                                       const char **serial);
+
+  int (*open_device) (SrtInputDevice *device,
+                      int mode_and_flags,
+                      GError **error);
+};
+
+gboolean _srt_input_device_check_open_flags (int mode_and_flags,
+                                             GError **error);
+
+struct _SrtInputDeviceMonitorInterface
+{
+  GTypeInterface parent;
+
+  /* Signals */
+  void (*added) (SrtInputDeviceMonitor *monitor,
+                 SrtInputDevice *device);
+  void (*removed) (SrtInputDeviceMonitor *monitor,
+                   SrtInputDevice *device);
+  void (*all_for_now) (SrtInputDeviceMonitor *monitor);
+
+  /* Virtual methods */
+  void (*request_raw_hid) (SrtInputDeviceMonitor *monitor);
+  void (*request_evdev) (SrtInputDeviceMonitor *monitor);
+  gboolean (*start) (SrtInputDeviceMonitor *monitor,
+                     GError **error);
+  void (*stop) (SrtInputDeviceMonitor *monitor);
+};
+
+void _srt_input_device_monitor_emit_added (SrtInputDeviceMonitor *monitor,
+                                           SrtInputDevice *device);
+void _srt_input_device_monitor_emit_removed (SrtInputDeviceMonitor *monitor,
+                                             SrtInputDevice *device);
+void _srt_input_device_monitor_emit_all_for_now (SrtInputDeviceMonitor *monitor);
+
+gboolean _srt_get_identity_from_evdev (int fd,
+                                       guint32 *bus_type,
+                                       guint32 *vendor,
+                                       guint32 *product,
+                                       guint32 *version,
+                                       gchar **name,
+                                       gchar **phys,
+                                       gchar **uniq);
+gboolean _srt_get_identity_from_raw_hid (int fd,
+                                         guint32 *bus_type,
+                                         guint32 *vendor,
+                                         guint32 *product,
+                                         gchar **name,
+                                         gchar **phys,
+                                         gchar **uniq);
+gchar *_srt_input_device_uevent_field (const char *text,
+                                       const char *key);
+gboolean _srt_input_device_uevent_field_equals (const char *text,
+                                                const char *key,
+                                                const char *want_value);
+gboolean _srt_get_identity_from_hid_uevent (const char *text,
+                                            guint32 *bus_type,
+                                            guint32 *product_id,
+                                            guint32 *vendor_id,
+                                            gchar **name,
+                                            gchar **phys,
+                                            gchar **uniq);
diff --git a/steam-runtime-tools/input-device.c b/steam-runtime-tools/input-device.c
new file mode 100644
index 0000000000000000000000000000000000000000..0ef2ca891b6864d0b9da30c72192e9b9cc1f24ff
--- /dev/null
+++ b/steam-runtime-tools/input-device.c
@@ -0,0 +1,2021 @@
+/*
+ * Input device internals, with parts based on SDL code.
+ *
+ * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org>
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: Zlib
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ */
+
+#include "steam-runtime-tools/input-device.h"
+#include "steam-runtime-tools/input-device-internal.h"
+
+#include <linux/hidraw.h>
+#include <linux/input.h>
+
+#include <libglnx.h>
+
+#include "steam-runtime-tools/enums.h"
+
+#include "steam-runtime-tools/direct-input-device-internal.h"
+#include "steam-runtime-tools/udev-input-device-internal.h"
+
+#ifndef HIDIOCGRAWUNIQ
+/* added in Linux 5.6, will fail on older kernels; this should
+ * be fine, we'll just report a NULL unique ID */
+#define HIDIOCGRAWUNIQ(len)     _IOC(_IOC_READ, 'H', 0x08, len)
+#endif
+
+/*
+ * Set @error and return %FALSE if @mode_and_flags is unsupported.
+ */
+gboolean
+_srt_input_device_check_open_flags (int mode_and_flags,
+                                    GError **error)
+{
+  int mode;
+  int unhandled_flags;
+
+  mode = mode_and_flags & (O_RDONLY|O_WRONLY|O_RDWR);
+  unhandled_flags = mode_and_flags & ~mode;
+
+  if (unhandled_flags & O_NONBLOCK)
+    unhandled_flags &= ~O_NONBLOCK;
+
+  if (unhandled_flags != 0)
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
+                   "Flags not supported: 0x%x", unhandled_flags);
+      return FALSE;
+    }
+
+  switch (mode)
+    {
+      case O_RDONLY:
+      case O_WRONLY:
+      case O_RDWR:
+        break;
+
+      default:
+        g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
+                     "Mode not supported: 0x%x", mode);
+        return FALSE;
+    }
+
+  return TRUE;
+}
+
+/**
+ * SrtInputDevice:
+ *
+ * An input device.
+ *
+ * Take additional references with g_object_ref(), release references
+ * with g_object_unref().
+ */
+/**
+ * SrtInputDeviceInterface:
+ *
+ * The interface implemented by each SrtInputDevice.
+ */
+G_DEFINE_INTERFACE (SrtInputDevice, srt_input_device, G_TYPE_OBJECT)
+
+static const char *
+srt_input_device_default_get_null_string (SrtInputDevice *device)
+{
+  return NULL;
+}
+
+static gchar **
+srt_input_device_default_get_null_strv (SrtInputDevice *device)
+{
+  return NULL;
+}
+
+/**
+ * srt_input_device_get_interface_flags:
+ * @device: An object implementing #SrtInputDeviceInterface
+ *
+ * Return flags describing how the input device can be used.
+ *
+ * Returns: Flags describing the input device.
+ */
+SrtInputDeviceInterfaceFlags
+srt_input_device_get_interface_flags (SrtInputDevice *device)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, SRT_INPUT_DEVICE_INTERFACE_FLAGS_NONE);
+
+  return iface->get_interface_flags (device);
+}
+
+static SrtInputDeviceInterfaceFlags
+srt_input_device_default_get_interface_flags (SrtInputDevice *device)
+{
+  return SRT_INPUT_DEVICE_INTERFACE_FLAGS_NONE;
+}
+
+/**
+ * srt_input_device_get_type_flags:
+ * @device: An object implementing #SrtInputDeviceInterface
+ *
+ * Return flags describing what sort of device this is.
+ * If possible, these will be taken from a data source such as udev's
+ * input_id builtin, which will be treated as authoritative.
+ *
+ * Returns: Flags describing the input device.
+ */
+SrtInputDeviceTypeFlags
+srt_input_device_get_type_flags (SrtInputDevice *device)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, SRT_INPUT_DEVICE_TYPE_FLAGS_NONE);
+
+  return iface->get_type_flags (device);
+}
+
+/**
+ * srt_input_device_gues_type_flags_from_event_capabilities:
+ * @device: An object implementing #SrtInputDeviceInterface
+ *
+ * Return flags describing what sort of device this is.
+ * Unlike srt_input_device_get_type_flags(), this function always
+ * tries to guess the type flags from the event capabilities, which
+ * can be used in diagnostic tools to highlight devices that might be
+ * misidentified when only their event capabilities are available.
+ *
+ * Returns: Flags describing the input device.
+ */
+SrtInputDeviceTypeFlags
+srt_input_device_guess_type_flags_from_event_capabilities (SrtInputDevice *device)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+  const SrtEvdevCapabilities *caps;
+
+  g_return_val_if_fail (iface != NULL, 0);
+
+  caps = iface->peek_event_capabilities (device);
+
+  if (caps == NULL)
+    return SRT_INPUT_DEVICE_TYPE_FLAGS_NONE;
+
+  return _srt_evdev_capabilities_guess_type (caps);
+}
+
+#define srt_input_device_default_get_type_flags \
+  srt_input_device_guess_type_flags_from_event_capabilities
+
+/**
+ * srt_input_device_get_dev_node:
+ * @device: An object implementing #SrtInputDeviceInterface
+ *
+ * Return the path of the device node in /dev that is implemented by this
+ * input device, or %NULL if not known or if the device does not have a
+ * corresponding device node.
+ *
+ * For processes in a container, it is not guaranteed that this path will
+ * exist in the container's /dev.
+ *
+ * The returned string will not be freed as long as @device is referenced,
+ * but the device node in /dev might be deleted, or even reused for a different
+ * device.
+ *
+ * Returns: (nullable) (transfer none): A path in /dev that will not be
+ *  freed as long as a reference to @device is held, or %NULL
+ */
+const char *
+srt_input_device_get_dev_node (SrtInputDevice *device)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, NULL);
+
+  return iface->get_dev_node (device);
+}
+
+/**
+ * srt_input_device_get_sys_path:
+ * @device: An object implementing #SrtInputDeviceInterface
+ *
+ * Return the path of the device directory in /sys that represents this
+ * input device.
+ *
+ * For processes in a container, it is not guaranteed that this path will
+ * exist in the container's /sys.
+ *
+ * The returned string will not be freed as long as @device is referenced,
+ * but the directory in /sys might be deleted, or even reused for a different
+ * device.
+ *
+ * Returns: (nullable) (transfer none): A path in /sys that will not be
+ *  freed as long as a reference to @device is held, or %NULL
+ */
+const char *
+srt_input_device_get_sys_path (SrtInputDevice *device)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, NULL);
+
+  return iface->get_sys_path (device);
+}
+
+/**
+ * srt_input_device_get_subsystem:
+ * @device: An object implementing #SrtInputDeviceInterface
+ *
+ * Return the subsystem in which this device exists, typically input
+ * or hidraw, or %NULL if not known.
+ *
+ * Returns: (nullable) (transfer none): A short string that will not be
+ *  freed as long as a reference to @device is held, or %NULL
+ */
+const char *
+srt_input_device_get_subsystem (SrtInputDevice *device)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, NULL);
+
+  return iface->get_subsystem (device);
+}
+
+/**
+ * srt_input_device_dup_uevent:
+ * @device: An object implementing #SrtInputDeviceInterface
+ *
+ * Return the `uevent` data from the kernel.
+ *
+ * Returns: (nullable) (transfer full): A multi-line string, or %NULL.
+ *  Free with g_free().
+ */
+gchar *
+srt_input_device_dup_uevent (SrtInputDevice *device)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, NULL);
+
+  return iface->dup_uevent (device);
+}
+
+static gchar *
+read_uevent (const char *sys_path)
+{
+  g_autofree gchar *uevent_path = NULL;
+  g_autofree gchar *contents = NULL;
+
+  if (sys_path == NULL)
+    return NULL;
+
+  uevent_path = g_build_filename (sys_path, "uevent", NULL);
+
+  if (g_file_get_contents (uevent_path, &contents, NULL, NULL))
+    return g_steal_pointer (&contents);
+
+  return NULL;
+}
+
+/*
+ * Default implementation: read the file in /sys. The kernel provides this,
+ * so it should always be present, except in containers.
+ */
+static gchar *
+srt_input_device_default_dup_uevent (SrtInputDevice *device)
+{
+  return read_uevent (srt_input_device_get_sys_path (device));
+}
+
+static gchar *
+srt_input_device_default_dup_hid_uevent (SrtInputDevice *device)
+{
+  return read_uevent (srt_input_device_get_hid_sys_path (device));
+}
+
+static gchar *
+srt_input_device_default_dup_input_uevent (SrtInputDevice *device)
+{
+  return read_uevent (srt_input_device_get_input_sys_path (device));
+}
+
+static gchar *
+srt_input_device_default_dup_usb_device_uevent (SrtInputDevice *device)
+{
+  return read_uevent (srt_input_device_get_usb_device_sys_path (device));
+}
+
+/**
+ * srt_input_device_dup_udev_properties:
+ * @device: An object implementing #SrtInputDeviceInterface
+ *
+ * Return the udev properties of this input device, if available, in
+ * the same format as `environ`. g_environ_getenv() can be used to
+ * process them.
+ *
+ * Returns: (nullable) (transfer full): udev properties, or %NULL.
+ *  Free with g_strfreev().
+ */
+gchar **
+srt_input_device_dup_udev_properties (SrtInputDevice *device)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, NULL);
+
+  return iface->dup_udev_properties (device);
+}
+
+/**
+ * srt_input_device_get_identity:
+ * @device: An object implementing #SrtInputDeviceInterface
+ * @bus_type: (out): Used to return the bus type from `<linux/input.h>`,
+ *  usually `BUS_USB` or `BUS_BLUETOOTH`
+ * @vendor_id: (out): Used to return the vendor ID, namespaced by
+ *  the @bus_type, or 0 if unavailable
+ * @product_id: (out): Used to return the product ID, namespaced by
+ *  the @vendor_id, or 0 if unavailable
+ * @version: (out): Used to return the device version, or 0 if unavailable
+ *
+ * Attempt to identify the device. If available, return the bus type,
+ * the vendor ID, the product ID and/or the device version via "out"
+ * parameters.
+ *
+ * The source of the information is unspecified. Use
+ * srt_input_device_get_hid_identity(),
+ * srt_input_device_get_input_identity() and/or
+ * srt_input_device_get_usb_device_identity() if a specific source
+ * is desired.
+ *
+ * Returns: %TRUE if information was available
+ */
+gboolean
+srt_input_device_get_identity (SrtInputDevice *device,
+                               unsigned int *bus_type,
+                               unsigned int *vendor_id,
+                               unsigned int *product_id,
+                               unsigned int *version)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, FALSE);
+
+  return iface->get_identity (device, bus_type, vendor_id, product_id, version);
+}
+
+static gboolean
+srt_input_device_default_get_identity (SrtInputDevice *device,
+                                       unsigned int *bus_type,
+                                       unsigned int *vendor_id,
+                                       unsigned int *product_id,
+                                       unsigned int *version)
+{
+  if (srt_input_device_get_input_identity (device,
+                                           bus_type, vendor_id, product_id,
+                                           version, NULL, NULL, NULL))
+    return TRUE;
+
+  if (srt_input_device_get_hid_identity (device,
+                                         bus_type, vendor_id, product_id,
+                                         NULL, NULL, NULL))
+    {
+      if (version != NULL)
+        {
+          *version = 0;
+
+          srt_input_device_get_usb_device_identity (device, NULL, NULL, version,
+                                                    NULL, NULL, NULL);
+        }
+
+      return TRUE;
+    }
+
+  if (srt_input_device_get_usb_device_identity (device,
+                                                vendor_id, product_id, version,
+                                                NULL, NULL, NULL))
+    {
+      if (bus_type != NULL)
+        *bus_type = BUS_USB;
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+const unsigned long *
+_srt_evdev_capabilities_get_bits (const SrtEvdevCapabilities *caps,
+                                  unsigned int type,
+                                  size_t *n_longs)
+{
+  const unsigned long *buf = NULL;
+  size_t n = 0;
+
+  switch (type)
+    {
+      case 0:
+        buf = &caps->ev[0];
+        n = G_N_ELEMENTS (caps->ev);
+        break;
+
+      case EV_KEY:
+        buf = &caps->keys[0];
+        n = G_N_ELEMENTS (caps->keys);
+        break;
+
+      case EV_ABS:
+        buf = &caps->abs[0];
+        n = G_N_ELEMENTS (caps->abs);
+        break;
+
+      case EV_REL:
+        buf = &caps->rel[0];
+        n = G_N_ELEMENTS (caps->rel);
+        break;
+
+      case EV_FF:
+        buf = &caps->ff[0];
+        n = G_N_ELEMENTS (caps->ff);
+        break;
+
+      case EV_MSC:
+      case EV_SW:
+      case EV_LED:
+      case EV_SND:
+      case EV_REP:
+      case EV_PWR:
+      case EV_FF_STATUS:
+      case EV_MAX:
+      default:
+        break;
+    }
+
+  *n_longs = n;
+  return buf;
+}
+
+/* Format to print an unsigned long in hex, zero-padded to full size
+ * if possible. */
+#if G_MAXULONG == 0xffffffffUL
+# define HEX_LONG_FORMAT "08lx"
+#elif defined(__LP64__)
+# define HEX_LONG_FORMAT "016lx"
+#else
+# warning Unsupported architecture, assuming ILP32
+# define HEX_LONG_FORMAT "08lx"
+#endif
+
+void
+_srt_evdev_capabilities_dump (const SrtEvdevCapabilities *caps)
+{
+  gsize i;
+
+  for (i = 0; i < G_N_ELEMENTS (caps->ev); i++)
+    g_debug ("ev[%zu]: %" HEX_LONG_FORMAT, i, caps->ev[i]);
+
+  for (i = 0; i < G_N_ELEMENTS (caps->keys); i++)
+    g_debug ("keys[%zu]: %" HEX_LONG_FORMAT, i, caps->keys[i]);
+
+  for (i = 0; i < G_N_ELEMENTS (caps->abs); i++)
+    g_debug ("abs[%zu]: %" HEX_LONG_FORMAT, i, caps->abs[i]);
+
+  for (i = 0; i < G_N_ELEMENTS (caps->rel); i++)
+    g_debug ("rel[%zu]: %" HEX_LONG_FORMAT, i, caps->rel[i]);
+
+  for (i = 0; i < G_N_ELEMENTS (caps->props); i++)
+    g_debug ("props[%zu]: %" HEX_LONG_FORMAT, i, caps->props[i]);
+}
+
+/**
+ * srt_input_device_get_event_capabilities:
+ * @device: An object implementing #SrtInputDeviceInterface
+ * @type: An event type from `<linux/input.h>` such as `EV_KEY`,
+ *  or 0 to get the supported event types
+ * @storage: (optional) (out caller-allocates) (array length=n_longs): An
+ *  array of @n_longs unsigned long values that will receive the bitfield,
+ *  allocated by the caller. Bit number 0 is the least significant bit
+ *  of storage[0] and so on up to the most significant bit of storage[0],
+ *  which is one place less significant than the least significant bit
+ *  of storage[1] if present.
+ * @n_longs: The length of @storage, or 0 if @storage is %NULL
+ *
+ * Fill a buffer with the event capabilities in the same encoding used
+ * for the EVIOCGBIT ioctl, or query how large that buffer would have to be.
+ *
+ * Bits in @storage above the highest known event value will be zeroed.
+ *
+ * If @storage is too small, high event values will not be represented.
+ * For example, if @type is `EV_KEY` and @n_longs is 1, then @storage
+ * will only indicate whether the first 32 or 64 key event codes are
+ * supported, and will not indicate anything about the level of support
+ * for `KEY_RIGHTALT` (event code 100).
+ *
+ * If @type is not a supported type, all of @storage will be zeroed and
+ * 0 will be returned.
+ *
+ * Returns: The number of unsigned long values that would have been
+ *  required for the highest possible event of type @type, which might
+ *  be greater than @n_longs
+ */
+size_t
+srt_input_device_get_event_capabilities (SrtInputDevice *device,
+                                         unsigned int type,
+                                         unsigned long *storage,
+                                         size_t n_longs)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+  const SrtEvdevCapabilities *caps;
+  const unsigned long *buf = NULL;
+  size_t n = 0;
+
+  g_return_val_if_fail (iface != NULL, 0);
+  g_return_val_if_fail (storage != NULL || n_longs == 0, 0);
+
+  if (n_longs != 0)
+    memset (storage, '\0', n_longs * sizeof (long));
+
+  caps = iface->peek_event_capabilities (device);
+
+  if (caps != NULL)
+    buf = _srt_evdev_capabilities_get_bits (caps, type, &n);
+
+  if (storage != NULL && buf != NULL && n_longs != 0 && n != 0)
+    memcpy (storage, buf, MIN (n_longs, n) * sizeof (long));
+
+  return n;
+}
+
+static const SrtEvdevCapabilities *
+srt_input_device_default_peek_event_capabilities (SrtInputDevice *device)
+{
+  return NULL;
+}
+
+/**
+ * srt_input_device_has_event_type:
+ * @device: An object implementing #SrtInputDeviceInterface
+ * @type: %EV_KEY, %EV_ABS, %EV_REL, %EV_FF or another evdev event type
+ *
+ * If the @device is an evdev device implementing the given event
+ * type, return %TRUE. Otherwise return %FALSE.
+ *
+ * Returns: %TRUE if the object implements the given event type.
+ */
+gboolean
+srt_input_device_has_event_type (SrtInputDevice *device,
+                                 unsigned int type)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+  const SrtEvdevCapabilities *caps;
+
+  g_return_val_if_fail (iface != NULL, 0);
+
+  caps = iface->peek_event_capabilities (device);
+
+  return (caps != NULL
+          && type <= EV_MAX
+          && test_bit_checked (type, caps->ev, G_N_ELEMENTS (caps->ev)));
+}
+
+/**
+ * srt_input_device_get_event_types:
+ * @device: An object implementing #SrtInputDeviceInterface
+ * @storage: (optional) (out caller-allocates) (array length=n_longs): An
+ *  array of @n_longs unsigned long values that will receive the bitfield,
+ *  allocated by the caller. Bit number 0 is the least significant bit
+ *  of storage[0] and so on up to the most significant bit of storage[0],
+ *  which is one place less significant than the least significant bit
+ *  of storage[1] if present.
+ * @n_longs: The length of @storage, or 0 if @storage is %NULL
+ *
+ * Fill a buffer with the supported event types in the same encoding used
+ * for the EVIOCGBIT ioctl, or query how large that buffer would have to be.
+ * This is the same as
+ * `srt_input_device_get_event_capabilities (device, 0, ...)`,
+ * except that bit numbers in @storage reflect event types, for example
+ * bit number 3 (`storage[0] & (1 << 3)`) represents
+ * event type 3 (`EV_ABS`).
+ *
+ * Returns: The number of unsigned long values that would have been
+ *  required for the highest possible event of type @type, which might
+ *  be greater than @n_longs
+ */
+size_t
+srt_input_device_get_event_types (SrtInputDevice *device,
+                                  unsigned long *storage,
+                                  size_t n_longs)
+{
+  return srt_input_device_get_event_capabilities (device, 0, storage, n_longs);
+}
+
+/**
+ * srt_input_device_has_input_property:
+ * @device: An object implementing #SrtInputDeviceInterface
+ * @input_prop: An input property such as `INPUT_PROP_POINTER`
+ *
+ * If the @device is an evdev device with the given input
+ * property, return %TRUE. Otherwise return %FALSE.
+ *
+ * Returns: %TRUE if the object has the given input property.
+ */
+gboolean
+srt_input_device_has_input_property (SrtInputDevice *device,
+                                     unsigned int input_prop)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+  const SrtEvdevCapabilities *caps;
+
+  g_return_val_if_fail (iface != NULL, 0);
+
+  caps = iface->peek_event_capabilities (device);
+
+  return (caps != NULL
+          && input_prop <= INPUT_PROP_MAX
+          && test_bit_checked (input_prop, caps->props, G_N_ELEMENTS (caps->props)));
+}
+
+/**
+ * srt_input_device_get_input_properties:
+ * @device: An object implementing #SrtInputDeviceInterface
+ * @storage: (optional) (out caller-allocates) (array length=n_longs): An
+ *  array of @n_longs unsigned long values that will receive the bitfield,
+ *  allocated by the caller. Bit number 0 is the least significant bit
+ *  of storage[0] and so on up to the most significant bit of storage[0],
+ *  which is one place less significant than the least significant bit
+ *  of storage[1] if present.
+ * @n_longs: The length of @storage, or 0 if @storage is %NULL
+ *
+ * Fill a buffer with the input device properties in the same encoding used
+ * for the EVIOCGPROP ioctl, or query how large that buffer would have to be.
+ *
+ * Bit numbers in @storage reflect input properties, for example
+ * bit number 6 (`storage[0] & (1 << 6)`) represents
+ * input property 6 (`INPUT_PROP_ACCELEROMETER`).
+ *
+ * Returns: The number of unsigned long values that would have been
+ *  required for the highest possible event of type @type, which might
+ *  be greater than @n_longs
+ */
+size_t
+srt_input_device_get_input_properties (SrtInputDevice *device,
+                                       unsigned long *storage,
+                                       size_t n_longs)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+  const SrtEvdevCapabilities *caps;
+
+  g_return_val_if_fail (iface != NULL, 0);
+  g_return_val_if_fail (storage != NULL || n_longs == 0, 0);
+
+  if (n_longs != 0)
+    memset (storage, '\0', n_longs * sizeof (long));
+
+  caps = iface->peek_event_capabilities (device);
+
+  if (caps == NULL)
+    return 0;
+
+  if (storage != NULL && n_longs != 0)
+    memcpy (storage, caps->props,
+            MIN (n_longs, G_N_ELEMENTS (caps->props)) * sizeof (long));
+
+  return G_N_ELEMENTS (caps->props);
+}
+
+/**
+ * srt_input_device_has_event_capability:
+ * @device: An object implementing #SrtInputDeviceInterface
+ * @type: `EV_KEY`, `EV_ABS`, `EV_REL`, `EV_FF` or another evdev event type
+ * @code: A bit appropriate for the given evdev event type;
+ *  for example, if @type is `EV_KEY`, then @code might be `KEY_BACKSPACE`
+ *  or `BTN_X`
+ *
+ * If the @device is an evdev device implementing the given event
+ * type and code, return %TRUE. Otherwise return %FALSE.
+ *
+ * This is currently only implemented for `EV_KEY`, `EV_ABS`, `EV_REL`
+ * and `EV_FF` (the interesting event types for game controllers), and
+ * will return %FALSE for more exotic event types.
+ *
+ * Returns: %TRUE if the object implements the given event type.
+ */
+gboolean
+srt_input_device_has_event_capability (SrtInputDevice *device,
+                                       unsigned int type,
+                                       unsigned int code)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+  const SrtEvdevCapabilities *caps;
+  const unsigned long *buf = NULL;
+  size_t n = 0;
+
+  g_return_val_if_fail (iface != NULL, 0);
+
+  caps = iface->peek_event_capabilities (device);
+
+  if (caps == NULL)
+    return FALSE;
+
+  buf = _srt_evdev_capabilities_get_bits (caps, type, &n);
+  return test_bit_checked (code, buf, n);
+}
+
+/**
+ * srt_input_device_get_hid_sys_path:
+ * @device: An object implementing #SrtInputDeviceInterface
+ *
+ * Return the path of the device directory in /sys that represents this
+ * input device's closest ancestor that is a Human Interface Device.
+ * Many, but not all, input devices have a HID ancestor. If there is no
+ * applicable HID device, return %NULL.
+ *
+ * For processes in a container, it is not guaranteed that this path will
+ * exist in the container's /sys.
+ *
+ * The returned string will not be freed as long as @device is referenced,
+ * but the directory in /sys might be deleted, or even reused for a different
+ * device.
+ *
+ * Returns: (nullable) (transfer none): A path in /sys that will not be
+ *  freed as long as a reference to @device is held, or %NULL
+ */
+const char *
+srt_input_device_get_hid_sys_path (SrtInputDevice *device)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, NULL);
+
+  return iface->get_hid_sys_path (device);
+}
+
+/**
+ * srt_input_device_dup_hid_uevent:
+ * @device: An object implementing #SrtInputDeviceInterface
+ *
+ * Return the uevent data structure similar to
+ * srt_input_device_dup_uevent(), but for the ancestor device returned
+ * by srt_input_device_get_hid_sys_path().
+ *
+ * Returns: (nullable) (transfer full): A multi-line string, or %NULL.
+ *  Free with g_free().
+ */
+gchar *
+srt_input_device_dup_hid_uevent (SrtInputDevice *device)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, NULL);
+
+  return iface->dup_hid_uevent (device);
+}
+
+/**
+ * srt_input_device_get_hid_identity:
+ * @device: An object implementing #SrtInputDeviceInterface
+ * @bus_type: (out): Used to return the bus type from `<linux/input.h>`,
+ *  usually `BUS_USB` or `BUS_BLUETOOTH`
+ * @vendor_id: (out): Used to return the vendor ID, namespaced by
+ *  the @bus_type, or 0 if unavailable
+ * @product_id: (out): Used to return the product ID, namespaced by
+ *  the @vendor_id, or 0 if unavailable
+ * @name: (out) (transfer none): Used to return a human-readable name
+ *  that usually combines the vendor and product, or empty or %NULL
+ *  if unavailable
+ * @phys: (out) (transfer none): Used to return how the device is
+ *  physically attached, or empty or %NULL if unavailable
+ * @uniq: (out) (transfer none): Used to return the device's serial number
+ *  or other unique identifier, or empty or %NULL if unavailable
+ *
+ * Attempt to identify the device. If available, return details via
+ * "out" parameters.
+ *
+ * Returns: %TRUE if this is an evdev device and information was available
+ */
+gboolean
+srt_input_device_get_hid_identity (SrtInputDevice *device,
+                                   unsigned int *bus_type,
+                                   unsigned int *vendor_id,
+                                   unsigned int *product_id,
+                                   const char **name,
+                                   const char **phys,
+                                   const char **uniq)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, FALSE);
+
+  return iface->get_hid_identity (device, bus_type, vendor_id, product_id,
+                                  name, phys, uniq);
+}
+
+static gboolean
+srt_input_device_default_get_hid_identity (SrtInputDevice *device,
+                                           unsigned int *bus_type,
+                                           unsigned int *vendor_id,
+                                           unsigned int *product_id,
+                                           const char **name,
+                                           const char **phys,
+                                           const char **uniq)
+{
+  return FALSE;
+}
+
+/**
+ * srt_input_device_get_usb_device_sys_path:
+ * @device: An object implementing #SrtInputDeviceInterface
+ *
+ * If the @device is associated with a USB device, return the path in
+ * /sys representing the Linux `usb_device`. If not, return %NULL.
+ *
+ * For processes in a container, it is not guaranteed that this path will
+ * exist in the container's /sys.
+ *
+ * The returned string will not be freed as long as @device is referenced,
+ * but the directory in /sys might be deleted, or even reused for a different
+ * device.
+ *
+ * Returns: (nullable) (transfer none): A path in /sys that will not be
+ *  freed as long as a reference to @device is held, or %NULL
+ */
+const char *
+srt_input_device_get_usb_device_sys_path (SrtInputDevice *device)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, NULL);
+
+  return iface->get_usb_device_sys_path (device);
+}
+
+/**
+ * srt_input_device_dup_usb_device_uevent:
+ * @device: An object implementing #SrtInputDeviceInterface
+ *
+ * Return the uevent data structure similar to
+ * srt_input_device_dup_uevent(), but for the ancestor device returned
+ * by srt_input_device_get_usb_device_sys_path().
+ *
+ * Returns: (nullable) (transfer full): A multi-line string, or %NULL.
+ *  Free with g_free().
+ */
+gchar *
+srt_input_device_dup_usb_device_uevent (SrtInputDevice *device)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, NULL);
+
+  return iface->dup_usb_device_uevent (device);
+}
+
+/**
+ * srt_input_device_get_usb_device_identity:
+ * @device: An object implementing #SrtInputDeviceInterface
+ * @bus_type: (out): Used to return the bus type from `<linux/input.h>`,
+ *  usually `BUS_USB` or `BUS_BLUETOOTH`
+ * @vendor_id: (out): Used to return the vendor ID, namespaced by
+ *  the @bus_type, or 0 if unavailable
+ * @product_id: (out): Used to return the product ID, namespaced by
+ *  the @vendor_id, or 0 if unavailable
+ * @device_version: (out): Used to return the product version, or 0 if unavailable
+ * @manufacturer: (out) (transfer none): Used to return the human-readable name
+ *  of the manufacturer, or empty or %NULL if unavailable
+ * @product: (out) (transfer none): Used to return the human-readable name
+ *  of the product, or empty or %NULL if unavailable
+ * @serial: (out) (transfer none): Used to return the device's serial number
+ *  or other unique identifier, or empty or %NULL if unavailable
+ *
+ * Attempt to identify the device. If available, return details via
+ * "out" parameters.
+ *
+ * Returns: %TRUE if this is a USB device and information was available
+ */
+gboolean
+srt_input_device_get_usb_device_identity (SrtInputDevice *device,
+                                          unsigned int *vendor_id,
+                                          unsigned int *product_id,
+                                          unsigned int *device_version,
+                                          const char **manufacturer,
+                                          const char **product,
+                                          const char **serial)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, FALSE);
+
+  return iface->get_usb_device_identity (device,
+                                         vendor_id, product_id, device_version,
+                                         manufacturer, product, serial);
+}
+
+static gboolean
+srt_input_device_default_get_usb_device_identity (SrtInputDevice *device,
+                                                  unsigned int *vendor_id,
+                                                  unsigned int *product_id,
+                                                  unsigned int *device_version,
+                                                  const char **manufacturer,
+                                                  const char **product,
+                                                  const char **serial)
+{
+  return FALSE;
+}
+
+/**
+ * srt_input_device_get_input_sys_path:
+ * @device: An object implementing #SrtInputDeviceInterface
+ *
+ * If the @device has an ancestor device that advertises evdev input
+ * capabilities, return the path in /sys for that device. Otherwise
+ * return %NULL.
+ *
+ * For processes in a container, it is not guaranteed that this path will
+ * exist in the container's /sys.
+ *
+ * The returned string will not be freed as long as @device is referenced,
+ * but the directory in /sys might be deleted, or even reused for a different
+ * device.
+ *
+ * Returns: (nullable) (transfer none): A path in /sys that will not be
+ *  freed as long as a reference to @device is held, or %NULL
+ */
+const char *
+srt_input_device_get_input_sys_path (SrtInputDevice *device)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, NULL);
+
+  return iface->get_input_sys_path (device);
+}
+
+/**
+ * srt_input_device_dup_input_uevent:
+ * @device: An object implementing #SrtInputDeviceInterface
+ *
+ * Return the uevent data structure similar to
+ * srt_input_device_dup_uevent(), but for the ancestor device returned
+ * by srt_input_device_get_input_sys_path().
+ *
+ * Returns: (nullable) (transfer full): A multi-line string, or %NULL.
+ *  Free with g_free().
+ */
+gchar *
+srt_input_device_dup_input_uevent (SrtInputDevice *device)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, NULL);
+
+  return iface->dup_input_uevent (device);
+}
+
+/**
+ * srt_input_device_get_input_identity:
+ * @device: An object implementing #SrtInputDeviceInterface
+ * @bus_type: (out): Used to return the bus type from `<linux/input.h>`,
+ *  usually `BUS_USB` or `BUS_BLUETOOTH`
+ * @vendor_id: (out): Used to return the vendor ID, namespaced by
+ *  the @bus_type, or 0 if unavailable
+ * @product_id: (out): Used to return the product ID, namespaced by
+ *  the @vendor_id, or 0 if unavailable
+ * @version: (out): Used to return the product version, or 0 if unavailable
+ * @name: (out) (transfer none): Used to return a human-readable name
+ *  that usually combines the vendor and product, or empty or %NULL
+ *  if unavailable
+ * @phys: (out) (transfer none): Used to return how the device is
+ *  physically attached, or empty or %NULL if unavailable
+ * @uniq: (out) (transfer none): Used to return the device's serial number
+ *  or other unique identifier, or empty or %NULL if unavailable
+ *
+ * Attempt to identify the device. If available, return details via
+ * "out" parameters.
+ *
+ * Returns: %TRUE if this is an evdev device and information was available
+ */
+gboolean
+srt_input_device_get_input_identity (SrtInputDevice *device,
+                                     unsigned int *bus_type,
+                                     unsigned int *vendor_id,
+                                     unsigned int *product_id,
+                                     unsigned int *version,
+                                     const char **name,
+                                     const char **phys,
+                                     const char **uniq)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, FALSE);
+
+  return iface->get_input_identity (device,
+                                    bus_type, vendor_id, product_id, version,
+                                    name, phys, uniq);
+}
+
+static gboolean
+srt_input_device_default_get_input_identity (SrtInputDevice *device,
+                                             unsigned int *bus_type,
+                                             unsigned int *vendor_id,
+                                             unsigned int *product_id,
+                                             unsigned int *version,
+                                             const char **name,
+                                             const char **phys,
+                                             const char **uniq)
+{
+  return FALSE;
+}
+
+/**
+ * srt_input_device_open:
+ * @device: An object implementing #SrtInputDeviceInterface
+ * @flags: Flags affecting how the device is opened
+ * @error: Used to report the error on failure
+ *
+ * @flags must include one of: O_RDONLY, O_WRONLY, or O_RDWR.
+ *
+ * @flags may include zero or more of: O_NONBLOCK.
+ *
+ * The file descriptor is always opened with O_CLOEXEC and O_NOCTTY.
+ * Explicitly specifying those flags is not allowed.
+ *
+ * Returns: A file descriptor owned by the caller, which must be closed
+ *  with close(2) after use, or a negative number on error.
+ */
+int
+srt_input_device_open (SrtInputDevice *device,
+                       int flags,
+                       GError **error)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, -1);
+  g_return_val_if_fail (error == NULL || *error == NULL, -1);
+
+  if (!_srt_input_device_check_open_flags (flags, error))
+    return -1;
+
+  return iface->open_device (device, flags, error);
+}
+
+static int
+srt_input_device_default_open_device (SrtInputDevice *device,
+                                      int flags,
+                                      GError **error)
+{
+  const char *devnode = NULL;
+  int fd = -1;
+
+  if (!_srt_input_device_check_open_flags (flags, error))
+    return -1;
+
+  devnode = srt_input_device_get_dev_node (device);
+
+  if (devnode == NULL)
+    {
+      glnx_throw (error, "Device has no device node");
+      return -1;
+    }
+
+  fd = open (devnode, flags | _SRT_INPUT_DEVICE_ALWAYS_OPEN_FLAGS);
+
+  if (fd < 0)
+    {
+      glnx_throw_errno_prefix (error,
+                               "Unable to open device node \"%s\"",
+                               devnode);
+      return -1;
+    }
+
+  return fd;
+}
+
+static void
+srt_input_device_default_init (SrtInputDeviceInterface *iface)
+{
+#define IMPLEMENT2(x, y) iface->x = srt_input_device_default_ ## y
+#define IMPLEMENT(x) IMPLEMENT2 (x, x)
+
+  IMPLEMENT (get_type_flags);
+  IMPLEMENT (get_interface_flags);
+  IMPLEMENT2 (get_dev_node, get_null_string);
+  IMPLEMENT2 (get_sys_path, get_null_string);
+  IMPLEMENT2 (get_subsystem, get_null_string);
+  IMPLEMENT (get_identity);
+  IMPLEMENT (peek_event_capabilities);
+
+  IMPLEMENT2 (get_hid_sys_path, get_null_string);
+  IMPLEMENT (get_hid_identity);
+  IMPLEMENT2 (get_input_sys_path, get_null_string);
+  IMPLEMENT (get_input_identity);
+  IMPLEMENT2 (get_usb_device_sys_path, get_null_string);
+  IMPLEMENT (get_usb_device_identity);
+
+  IMPLEMENT2 (dup_udev_properties, get_null_strv);
+
+  IMPLEMENT (dup_uevent);
+  IMPLEMENT (dup_hid_uevent);
+  IMPLEMENT (dup_input_uevent);
+  IMPLEMENT (dup_usb_device_uevent);
+
+  IMPLEMENT (open_device);
+
+#undef IMPLEMENT
+#undef IMPLEMENT2
+}
+
+/**
+ * SrtInputDeviceMonitor:
+ *
+ * An object to enumerate and monitor input devices.
+ *
+ * Take additional references with g_object_ref(), release references
+ * with g_object_unref().
+ *
+ * New input devices are signalled by the #SrtInputDeviceMonitor::added signal.
+ * An input device being removed is signalled by the
+ * #SrtInputDeviceMonitor::removed signal. The end of the initial device
+ * enumeration is signalled by the #SrtInputDeviceMonitor::all-for-now
+ * signal.
+ *
+ * All of these signals are emitted in whatever #GMainContext was
+ * the thread-default main context at the time the #SrtInputDeviceMonitor
+ * was created (in other words, their callbacks are called in the thread
+ * that is iterating the chosen #GMainContext). If you are using threads
+ * other than the main thread, use g_main_context_push_thread_default()
+ * to ensure that an appropriate main context will be used.
+ */
+/**
+ * SrtInputDeviceMonitorInterface:
+ *
+ * The interface implemented by each SrtInputDeviceMonitor.
+ */
+G_DEFINE_INTERFACE (SrtInputDeviceMonitor, srt_input_device_monitor, G_TYPE_OBJECT)
+
+static guint monitor_signal_added = 0;
+static guint monitor_signal_removed = 0;
+static guint monitor_signal_all_for_now = 0;
+
+static const SrtInputDeviceMonitorFlags mode_flags = (SRT_INPUT_DEVICE_MONITOR_FLAGS_UDEV
+                                                      | SRT_INPUT_DEVICE_MONITOR_FLAGS_DIRECT);
+
+/*
+ * @monitor_out: (out) (not optional):
+ * @error: (inout) (not optional):
+ */
+static gboolean
+try_udev (SrtInputDeviceMonitorFlags flags,
+          SrtInputDeviceMonitor **monitor_out,
+          GError **error)
+{
+  /* Don't try it again if we already failed, to avoid another warning */
+  if (*error != NULL)
+    return FALSE;
+
+  *monitor_out = srt_udev_input_device_monitor_new (flags, error);
+
+  if (*monitor_out != NULL)
+    return TRUE;
+
+  /* We usually expect this to succeed, so log a warning if it fails */
+  g_warning ("Unable to initialize udev input device monitor: %s",
+             (*error)->message);
+  return FALSE;
+}
+
+/**
+ * srt_input_device_monitor_new:
+ * @flags: Flags affecting the behaviour of the input device monitor.
+ *
+ * Return an object that can be used to enumerate and monitor input
+ * devices.
+ *
+ * Take additional references with g_object_ref(), release references
+ * with g_object_unref().
+ *
+ * Returns: the input device monitor
+ */
+SrtInputDeviceMonitor *
+srt_input_device_monitor_new (SrtInputDeviceMonitorFlags flags)
+{
+  SrtInputDeviceMonitor *udev = NULL;
+  g_autoptr(GError) udev_error = NULL;
+
+  if (__builtin_popcount (flags & mode_flags) > 1)
+    g_warning ("Requesting more than one of UDEV and DIRECT "
+               "monitoring has undefined results: 0x%x", flags);
+
+  /* First see whether the caller expressed a preference */
+
+  if (flags & SRT_INPUT_DEVICE_MONITOR_FLAGS_UDEV)
+    {
+      if (try_udev (flags, &udev, &udev_error))
+        return udev;
+    }
+
+  if (flags & SRT_INPUT_DEVICE_MONITOR_FLAGS_DIRECT)
+    return srt_direct_input_device_monitor_new (flags);
+
+  /* Prefer a direct monitor if we're in a container */
+  if (g_file_test ("/.flatpak-info", G_FILE_TEST_EXISTS)
+      || g_file_test ("/run/pressure-vessel", G_FILE_TEST_EXISTS)
+      || g_file_test ("/run/host", G_FILE_TEST_EXISTS))
+    return srt_direct_input_device_monitor_new (flags);
+
+  if (try_udev (flags, &udev, &udev_error))
+    return udev;
+
+  /* Fall back to direct monitoring if we don't have libudev */
+  return srt_direct_input_device_monitor_new (flags);
+}
+
+static void
+srt_input_device_monitor_default_do_nothing (SrtInputDeviceMonitor *monitor)
+{
+}
+
+/**
+ * srt_input_device_monitor_get_flags:
+ * @device: An object implementing #SrtInputDeviceMonitorInterface
+ *
+ * Return flags describing the input device monitor.
+ *
+ * Returns: Flags describing the input device monitor.
+ */
+SrtInputDeviceMonitorFlags
+srt_input_device_monitor_get_flags (SrtInputDeviceMonitor *monitor)
+{
+  SrtInputDeviceMonitorFlags flags = SRT_INPUT_DEVICE_MONITOR_FLAGS_NONE;
+  SrtInputDeviceMonitorInterface *iface = SRT_INPUT_DEVICE_MONITOR_GET_INTERFACE (monitor);
+
+  g_return_val_if_fail (iface != NULL, flags);
+
+  g_object_get (monitor,
+                "flags", &flags,
+                NULL);
+  return flags;
+}
+
+/**
+ * srt_input_device_monitor_is_active:
+ * @monitor: The input device monitor
+ *
+ * Return %TRUE if srt_input_device_monitor_start() has been called
+ * successfully, and srt_input_device_monitor_stop() has not subsequently
+ * been called.
+ *
+ * Returns: %TRUE if active
+ */
+gboolean
+srt_input_device_monitor_is_active (SrtInputDeviceMonitor *monitor)
+{
+  gboolean ret = FALSE;
+  SrtInputDeviceMonitorInterface *iface = SRT_INPUT_DEVICE_MONITOR_GET_INTERFACE (monitor);
+
+  g_return_val_if_fail (iface != NULL, FALSE);
+
+  g_object_get (monitor,
+                "is-active", &ret,
+                NULL);
+  return ret;
+}
+
+/**
+ * srt_input_device_monitor_request_raw_hid:
+ * @monitor: The input device monitor
+ *
+ * Tell the input device monitor to return all raw HID devices.
+ *
+ * If neither this method nor srt_input_device_monitor_request_evdev() is
+ * called, no devices will be found.
+ *
+ * This function cannot be called if srt_input_device_monitor_start()
+ * or srt_input_device_monitor_stop() have already been called.
+ */
+void
+srt_input_device_monitor_request_raw_hid (SrtInputDeviceMonitor *monitor)
+{
+  SrtInputDeviceMonitorInterface *iface = SRT_INPUT_DEVICE_MONITOR_GET_INTERFACE (monitor);
+
+  g_return_if_fail (iface != NULL);
+
+  iface->request_raw_hid (monitor);
+}
+
+/**
+ * srt_input_device_monitor_request_evdev:
+ * @monitor: The input device monitor
+ *
+ * Tell the input device monitor to return all evdev (event) devices.
+ *
+ * If neither this method nor srt_input_device_monitor_request_raw_hid() is
+ * called, no devices will be found.
+ *
+ * This function cannot be called if srt_input_device_monitor_start()
+ * or srt_input_device_monitor_stop() have already been called.
+ */
+void
+srt_input_device_monitor_request_evdev (SrtInputDeviceMonitor *monitor)
+{
+  SrtInputDeviceMonitorInterface *iface = SRT_INPUT_DEVICE_MONITOR_GET_INTERFACE (monitor);
+
+  g_return_if_fail (iface != NULL);
+
+  iface->request_evdev (monitor);
+}
+
+/**
+ * srt_input_device_monitor_start:
+ * @monitor: The input device monitor
+ *
+ * Start to watch for input devices.
+ *
+ * The device monitor will emit signals in the thread-default main
+ * context of the thread where this function was called
+ * (see g_main_context_push_thread_default() for details).
+ *
+ * The SrtInputDeviceMonitor::added signal will be emitted when
+ * a matching input device is detected.
+ * If the monitor is watching for both %SRT_INPUT_DEVICE_FLAGS_EVENT
+ * and %SRT_INPUT_DEVICE_FLAGS_RAW_HID devices, one signal will be
+ * emitted for each one.
+ *
+ * The SrtInputDeviceMonitor::removed signal will be emitted when
+ * a matching input device is removed.
+ *
+ * This function cannot be called if srt_input_device_monitor_start()
+ * or srt_input_device_monitor_stop() have already been called.
+ */
+gboolean
+srt_input_device_monitor_start (SrtInputDeviceMonitor *monitor,
+                                GError **error)
+{
+  SrtInputDeviceMonitorInterface *iface = SRT_INPUT_DEVICE_MONITOR_GET_INTERFACE (monitor);
+
+  g_return_val_if_fail (iface != NULL, FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+  return iface->start (monitor, error);
+}
+
+static gboolean
+srt_input_device_monitor_default_start (SrtInputDeviceMonitor *monitor,
+                                        GError **error)
+{
+  g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "Not implemented");
+  return FALSE;
+}
+
+/**
+ * srt_input_device_monitor_stop:
+ * @monitor: The input device monitor
+ *
+ * Stop the input device monitor. It still exists in memory until all
+ * references are released, but will stop signalling new events.
+ */
+void
+srt_input_device_monitor_stop (SrtInputDeviceMonitor *monitor)
+{
+  SrtInputDeviceMonitorInterface *iface = SRT_INPUT_DEVICE_MONITOR_GET_INTERFACE (monitor);
+
+  g_return_if_fail (iface != NULL);
+
+  iface->stop (monitor);
+}
+
+static void
+srt_input_device_monitor_default_init (SrtInputDeviceMonitorInterface *iface)
+{
+#define IMPLEMENT2(x, y) iface->x = srt_input_device_monitor_default_ ## y
+#define IMPLEMENT(x) IMPLEMENT2 (x, x)
+  IMPLEMENT2 (request_raw_hid, do_nothing);
+  IMPLEMENT2 (request_evdev, do_nothing);
+  IMPLEMENT (start);
+  IMPLEMENT2 (stop, do_nothing);
+#undef IMPLEMENT
+
+  /**
+   * SrtInputDeviceMonitor::added:
+   * @monitor: The input device monitor
+   * @device: The device
+   *
+   * Emitted when an input device is added.
+   */
+  monitor_signal_added =
+    g_signal_new ("added",
+                  SRT_TYPE_INPUT_DEVICE_MONITOR,
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (SrtInputDeviceMonitorInterface, added),
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE, 1,
+                  SRT_TYPE_INPUT_DEVICE);
+
+  /**
+   * SrtInputDeviceMonitor::removed:
+   * @monitor: The input device monitor
+   * @device: The device
+   *
+   * Emitted when an input device is removed.
+   */
+  monitor_signal_removed =
+    g_signal_new ("removed",
+                  SRT_TYPE_INPUT_DEVICE_MONITOR,
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (SrtInputDeviceMonitorInterface, removed),
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE, 1,
+                  SRT_TYPE_INPUT_DEVICE);
+
+  /**
+   * SrtInputDeviceMonitor::all-for-now:
+   * @monitor: The input device monitor
+   *
+   * Emitted when the initial batch of input devices has been discovered.
+   */
+  monitor_signal_all_for_now =
+    g_signal_new ("all-for-now",
+                  SRT_TYPE_INPUT_DEVICE_MONITOR,
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (SrtInputDeviceMonitorInterface, all_for_now),
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE, 0);
+
+  /**
+   * SrtInputDeviceMonitor::flags:
+   *
+   * Flags affecting the input device monitor.
+   */
+  g_object_interface_install_property (iface,
+      g_param_spec_flags ("flags",
+                          "Flags",
+                          "Flags affecting the input device monitor",
+                          SRT_TYPE_INPUT_DEVICE_MONITOR_FLAGS,
+                          SRT_INPUT_DEVICE_MONITOR_FLAGS_NONE,
+                          G_PARAM_READWRITE
+                          | G_PARAM_CONSTRUCT_ONLY
+                          | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * SrtInputDeviceMonitor::is-active:
+   *
+   * The value of srt_input_device_monitor_is_active().
+   */
+  g_object_interface_install_property (iface,
+      g_param_spec_boolean ("is-active",
+                            "Is active",
+                            "TRUE if started and not stopped",
+                            FALSE,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+}
+
+void
+_srt_input_device_monitor_emit_added (SrtInputDeviceMonitor *monitor,
+                                      SrtInputDevice *device)
+{
+  g_debug ("Added %p", device);
+  g_signal_emit (monitor, monitor_signal_added, 0, device);
+}
+
+void
+_srt_input_device_monitor_emit_removed (SrtInputDeviceMonitor *monitor,
+                                        SrtInputDevice *device)
+{
+  g_debug ("Removed %p", device);
+  g_signal_emit (monitor, monitor_signal_removed, 0, device);
+}
+
+void
+_srt_input_device_monitor_emit_all_for_now (SrtInputDeviceMonitor *monitor)
+{
+  g_debug ("All for now");
+  g_signal_emit (monitor, monitor_signal_all_for_now, 0);
+}
+
+static gboolean
+_srt_get_caps_from_evdev (int fd,
+                          unsigned int type,
+                          unsigned long *bitmask,
+                          size_t bitmask_len_longs)
+{
+  size_t bitmask_len_bytes = bitmask_len_longs * sizeof (*bitmask);
+
+  memset (bitmask, 0, bitmask_len_bytes);
+
+  if (ioctl (fd, EVIOCGBIT (type, bitmask_len_bytes), bitmask) < 0)
+    return FALSE;
+
+  return TRUE;
+}
+
+gboolean
+_srt_evdev_capabilities_set_from_evdev (SrtEvdevCapabilities *caps,
+                                        int fd)
+{
+  if (_srt_get_caps_from_evdev (fd, 0, caps->ev, G_N_ELEMENTS (caps->ev)))
+    {
+      _srt_get_caps_from_evdev (fd, EV_KEY, caps->keys, G_N_ELEMENTS (caps->keys));
+      _srt_get_caps_from_evdev (fd, EV_ABS, caps->abs, G_N_ELEMENTS (caps->abs));
+      _srt_get_caps_from_evdev (fd, EV_REL, caps->rel, G_N_ELEMENTS (caps->rel));
+      _srt_get_caps_from_evdev (fd, EV_FF, caps->ff, G_N_ELEMENTS (caps->ff));
+      ioctl (fd, EVIOCGPROP (sizeof (caps->props)), caps->props);
+      return TRUE;
+    }
+
+  memset (caps, 0, sizeof (*caps));
+  return FALSE;
+}
+
+gboolean
+_srt_get_identity_from_evdev (int fd,
+                              guint32 *bus_type,
+                              guint32 *vendor,
+                              guint32 *product,
+                              guint32 *version,
+                              gchar **name,
+                              gchar **phys,
+                              gchar **uniq)
+{
+  struct input_id iid = {};
+  char buf[256] = { '\0' };
+
+  if (bus_type != NULL)
+    *bus_type = 0;
+
+  if (vendor != NULL)
+    *vendor = 0;
+
+  if (product != NULL)
+    *product = 0;
+
+  if (ioctl (fd, EVIOCGID, &iid) < 0)
+    {
+      g_debug ("EVIOCGID: %s", g_strerror (errno));
+      return FALSE;
+    }
+
+  if (name != NULL
+      && ioctl (fd, EVIOCGNAME (sizeof (buf) - 1), buf) == 0)
+    *name = g_strdup (buf);
+
+  memset (buf, '\0', sizeof (buf));
+
+  if (phys != NULL
+      && ioctl (fd, EVIOCGPHYS (sizeof (buf) - 1), buf) == 0)
+    *phys = g_strdup (buf);
+
+  memset (buf, '\0', sizeof (buf));
+
+  if (uniq != NULL
+      && ioctl (fd, EVIOCGUNIQ (sizeof (buf) - 1), buf) == 0)
+    *uniq = g_strdup (buf);
+
+  if (bus_type != NULL)
+    *bus_type = iid.bustype;
+
+  if (vendor != NULL)
+    *vendor = iid.vendor;
+
+  if (product != NULL)
+    *product = iid.product;
+
+  if (version != NULL)
+    *version = iid.version;
+
+  return TRUE;
+}
+
+gboolean
+_srt_get_identity_from_raw_hid (int fd,
+                                guint32 *bus_type,
+                                guint32 *vendor,
+                                guint32 *product,
+                                gchar **name,
+                                gchar **phys,
+                                gchar **uniq)
+{
+  char buf[256] = { '\0' };
+  struct hidraw_devinfo devinfo = {};
+
+  if (bus_type != NULL)
+    *bus_type = 0;
+
+  if (vendor != NULL)
+    *vendor = 0;
+
+  if (product != NULL)
+    *product = 0;
+
+  if (ioctl (fd, HIDIOCGRAWINFO, &devinfo) < 0)
+    {
+      g_debug ("HIDIOCGRAWINFO: %s", g_strerror (errno));
+      return FALSE;
+    }
+
+  if (name != NULL
+      && ioctl (fd, HIDIOCGRAWNAME (sizeof (buf) - 1), buf) == 0)
+    *name = g_strdup (buf);
+
+  memset (buf, '\0', sizeof (buf));
+
+  if (phys != NULL
+      && ioctl (fd, HIDIOCGRAWPHYS (sizeof (buf) - 1), buf) == 0)
+    *phys = g_strdup (buf);
+
+  memset (buf, '\0', sizeof (buf));
+
+  if (uniq != NULL
+      && ioctl (fd, HIDIOCGRAWUNIQ (sizeof (buf) - 1), buf) == 0)
+    *uniq = g_strdup (buf);
+
+  if (bus_type != NULL)
+    *bus_type = devinfo.bustype;
+
+  if (vendor != NULL)
+    *vendor = devinfo.vendor;
+
+  if (product != NULL)
+    *product = devinfo.product;
+
+  return TRUE;
+}
+
+/*
+ * Returns: The field @key from @text, or %NULL if not found.
+ */
+gchar *
+_srt_input_device_uevent_field (const char *text,
+                                const char *key)
+{
+  const char *line = text;
+  size_t key_len = strlen (key);
+
+  for (line = text;
+       line != NULL && *line != '\0';
+       line = strchr (line, '\n'))
+    {
+      while (*line == '\n')
+        line++;
+
+      /* If the line starts with KEY=, we're looking at the right field */
+      if (strncmp (line, key, key_len) == 0
+          && line[key_len] == '=')
+        {
+          const char *value = line + key_len + 1;
+          const char *after = strchrnul (line, '\n');
+
+          g_assert (after != NULL);
+          g_assert (after >= value);
+          g_assert (*after == '\n' || *after == '\0');
+          return g_strndup (value, after - value);
+        }
+    }
+
+  return NULL;
+}
+
+/*
+ * Returns: %TRUE if @text contains KEY=WANT_VALUE, preceded by
+ *  beginning-of-string or a newline, and followed by a newline
+ *  or end-of-string.
+ */
+gboolean
+_srt_input_device_uevent_field_equals (const char *text,
+                                       const char *key,
+                                       const char *want_value)
+{
+  const char *line = text;
+  size_t key_len = strlen (key);
+  size_t val_len = strlen (want_value);
+
+  for (line = text;
+       line != NULL && *line != '\0';
+       line = strchr (line, '\n'))
+    {
+      while (*line == '\n')
+        line++;
+
+      /* If the line starts with KEY=, we're looking at the right field */
+      if (strncmp (line, key, key_len) == 0
+          && line[key_len] == '=')
+        {
+          const char *real_value = line + key_len + 1;
+          char after;
+
+          if (strncmp (real_value, want_value, val_len) != 0)
+            return FALSE;
+
+          after = real_value[val_len];
+
+          return (after == '\0' || after == '\n');
+        }
+    }
+
+  return FALSE;
+}
+
+gboolean
+_srt_get_identity_from_hid_uevent (const char *text,
+                                   guint32 *bus_type,
+                                   guint32 *vendor_id,
+                                   guint32 *product_id,
+                                   gchar **name,
+                                   gchar **phys,
+                                   gchar **uniq)
+{
+  g_autofree gchar *id = _srt_input_device_uevent_field (text, "HID_ID");
+  struct
+  {
+    guint bus_type;
+    guint vendor_id;
+    guint product_id;
+  } tmp;
+  int used;
+
+  if (id == NULL)
+    return FALSE;
+
+  if (sscanf (id, "%x:%x:%x%n",
+              &tmp.bus_type, &tmp.vendor_id, &tmp.product_id, &used) != 3
+      || ((size_t) used) != strlen (id))
+    return FALSE;
+
+  if (name != NULL)
+    *name = _srt_input_device_uevent_field (text, "HID_NAME");
+
+  if (phys != NULL)
+    *phys = _srt_input_device_uevent_field (text, "HID_PHYS");
+
+  if (uniq != NULL)
+    *uniq = _srt_input_device_uevent_field (text, "HID_UNIQ");
+
+  if (bus_type != NULL)
+    *bus_type = tmp.bus_type;
+
+  if (vendor_id != NULL)
+    *vendor_id = tmp.vendor_id;
+
+  if (product_id != NULL)
+    *product_id = tmp.product_id;
+
+  return TRUE;
+}
+
+/* _srt_evdev_capabilities_guess_type relies on all the joystick axes
+ * being in the first unsigned long. */
+G_STATIC_ASSERT (ABS_HAT3Y < BITS_PER_LONG);
+#define JOYSTICK_ABS_AXES \
+  ((1 << ABS_X) | (1 << ABS_Y) \
+   | (1 << ABS_RX) | (1 << ABS_RY) \
+   | (1 << ABS_THROTTLE) | (1 << ABS_RUDDER) \
+   | (1 << ABS_WHEEL) | (1 << ABS_GAS) | (1 << ABS_BRAKE) \
+   | (1 << ABS_HAT0X) | (1 << ABS_HAT0Y) \
+   | (1 << ABS_HAT1X) | (1 << ABS_HAT1Y) \
+   | (1 << ABS_HAT2X) | (1 << ABS_HAT2Y) \
+   | (1 << ABS_HAT3X) | (1 << ABS_HAT3Y))
+
+static const unsigned int first_mouse_button = BTN_MOUSE;
+static const unsigned int last_mouse_button = BTN_JOYSTICK - 1;
+
+static const unsigned int first_joystick_button = BTN_JOYSTICK;
+static const unsigned int last_joystick_button = BTN_GAMEPAD - 1;
+
+static const unsigned int first_gamepad_button = BTN_GAMEPAD;
+static const unsigned int last_gamepad_button = BTN_DIGI - 1;
+
+static const unsigned int first_dpad_button = BTN_DPAD_UP;
+static const unsigned int last_dpad_button = BTN_DPAD_RIGHT;
+
+static const unsigned int first_extra_joystick_button = BTN_TRIGGER_HAPPY;
+static const unsigned int last_extra_joystick_button = BTN_TRIGGER_HAPPY40;
+
+/*
+ * Guess the type of device from the input capabilities.
+ *
+ * We cannot copy what udev does for licensing reasons (it's GPL-licensed),
+ * so this is a reimplementation, variously taking inspiration from:
+ *
+ * - kernel documentation (https://www.kernel.org/doc/Documentation/input/)
+ * - libmanette
+ * - SDL
+ * - Wine dlls/winebus.sys
+ * - udev
+ */
+SrtInputDeviceTypeFlags
+_srt_evdev_capabilities_guess_type (const SrtEvdevCapabilities *caps)
+{
+  SrtInputDeviceTypeFlags flags = SRT_INPUT_DEVICE_TYPE_FLAGS_NONE;
+  unsigned int i;
+  gboolean has_joystick_axes = FALSE;
+  gboolean has_joystick_buttons = FALSE;
+
+  /* Some properties let us be fairly sure about a device */
+  if (test_bit (INPUT_PROP_ACCELEROMETER, caps->props))
+    {
+      g_debug ("INPUT_PROP_ACCELEROMETER => is accelerometer");
+      flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_ACCELEROMETER;
+    }
+
+  if (test_bit (INPUT_PROP_POINTING_STICK, caps->props))
+    {
+      g_debug ("INPUT_PROP_POINTING_STICK => is pointing stick");
+      flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_POINTING_STICK;
+    }
+
+  if (test_bit (INPUT_PROP_BUTTONPAD, caps->props)
+      || test_bit (INPUT_PROP_TOPBUTTONPAD, caps->props))
+    {
+      g_debug ("INPUT_PROP_[TOP]BUTTONPAD => is touchpad");
+      flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_TOUCHPAD;
+    }
+
+  /* Devices with a stylus or pen are assumed to be graphics tablets */
+  if (test_bit (BTN_STYLUS, caps->keys)
+      || test_bit (BTN_TOOL_PEN, caps->keys))
+    {
+      g_debug ("Stylus or pen => is tablet");
+      flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_TABLET;
+    }
+
+  /* Devices that accept a finger touch are assumed to be touchpads or
+   * touchscreens.
+   *
+   * In Steam we mostly only care about these as a way to
+   * reject non-joysticks, so we're not very precise here yet.
+   *
+   * SDL assumes that TOUCH means a touchscreen and FINGER
+   * means a touchpad. */
+  if (flags == SRT_INPUT_DEVICE_TYPE_FLAGS_NONE
+      && (test_bit (BTN_TOOL_FINGER, caps->keys)
+          || test_bit (BTN_TOUCH, caps->keys)
+          || test_bit (INPUT_PROP_SEMI_MT, caps->props)))
+    {
+      g_debug ("Finger or touch or semi-MT => is touchpad or touchscreen");
+
+      if (test_bit (INPUT_PROP_POINTER, caps->props))
+        flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_TOUCHPAD;
+      else
+        flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_TOUCHSCREEN;
+    }
+
+  /* Devices with mouse buttons are ... probably mice? */
+  if (flags == SRT_INPUT_DEVICE_TYPE_FLAGS_NONE)
+    {
+      for (i = first_mouse_button; i <= last_mouse_button; i++)
+        {
+          if (test_bit (i, caps->keys))
+            {
+              g_debug ("Mouse button => mouse");
+              flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_MOUSE;
+            }
+        }
+    }
+
+  if (flags == SRT_INPUT_DEVICE_TYPE_FLAGS_NONE)
+    {
+      for (i = ABS_X; i < ABS_Z; i++)
+        {
+          if (!test_bit (i, caps->abs))
+            break;
+        }
+
+      /* If it has 3 axes and no buttons it's probably an accelerometer. */
+      if (i == ABS_Z && !test_bit (EV_KEY, caps->ev))
+        {
+          g_debug ("3 left axes and no buttons => accelerometer");
+          flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_ACCELEROMETER;
+        }
+
+      /* Same for RX..RZ (e.g. Wiimote) */
+      for (i = ABS_RX; i < ABS_RZ; i++)
+        {
+          if (!test_bit (i, caps->abs))
+            break;
+        }
+
+      if (i == ABS_RZ && !test_bit (EV_KEY, caps->ev))
+        {
+          g_debug ("3 right axes and no buttons => accelerometer");
+          flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_ACCELEROMETER;
+        }
+    }
+
+  /* Bits 1 to 31 are ESC, numbers and Q to D, which SDL and udev both
+   * consider to be enough to count as a fully-functioned keyboard. */
+  if ((caps->keys[0] & 0xfffffffe) == 0xfffffffe)
+    {
+      g_debug ("First few keys => keyboard");
+      flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_KEYBOARD;
+    }
+
+  /* If we have *any* keys, consider it to be something a bit
+   * keyboard-like. Bits 0 to 63 are all keyboard keys.
+   * Make sure we stop before reaching KEY_UP which is sometimes
+   * used on game controller mappings, e.g. for the Wiimote. */
+  for (i = 0; i < (64 / BITS_PER_LONG); i++)
+    {
+      if (caps->keys[i] != 0)
+        flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS;
+    }
+
+  if (caps->abs[0] & JOYSTICK_ABS_AXES)
+    has_joystick_axes = TRUE;
+
+  /* Flight stick buttons */
+  for (i = first_joystick_button; i <= last_joystick_button; i++)
+    {
+      if (test_bit (i, caps->keys))
+        has_joystick_buttons = TRUE;
+    }
+
+  /* Gamepad buttons (Xbox, PS3, etc.) */
+  for (i = first_gamepad_button; i <= last_gamepad_button; i++)
+    {
+      if (test_bit (i, caps->keys))
+        has_joystick_buttons = TRUE;
+    }
+
+  /* Gamepad digital dpad */
+  for (i = first_dpad_button; i <= last_dpad_button; i++)
+    {
+      if (test_bit (i, caps->keys))
+        has_joystick_buttons = TRUE;
+    }
+
+  /* Steering wheel gear-change buttons */
+  for (i = BTN_GEAR_DOWN; i <= BTN_GEAR_UP; i++)
+    {
+      if (test_bit (i, caps->keys))
+        has_joystick_buttons = TRUE;
+    }
+
+  /* Reserved space for extra game-controller buttons, e.g. on Corsair
+   * gaming keyboards */
+  for (i = first_extra_joystick_button; i <= last_extra_joystick_button; i++)
+    {
+      if (test_bit (i, caps->keys))
+        has_joystick_buttons = TRUE;
+    }
+
+  if (test_bit (last_mouse_button, caps->keys))
+    {
+      /* Mice with a very large number of buttons can apparently
+       * overflow into the joystick-button space, but they're still not
+       * joysticks. */
+      has_joystick_buttons = FALSE;
+    }
+
+  /* TODO: Do we want to consider BTN_0 up to BTN_9 to be joystick buttons?
+   * libmanette and SDL look for BTN_1, udev does not.
+   *
+   * They're used by some game controllers, like BTN_1 and BTN_2 for the
+   * Wiimote, BTN_1..BTN_9 for the SpaceTec SpaceBall and BTN_0..BTN_3
+   * for Playstation dance pads, but they're also used by
+   * non-game-controllers like Logitech mice. For now we entirely ignore
+   * these buttons: they are not evidence that it's a joystick, but
+   * neither are they evidence that it *isn't* a joystick. */
+
+  /* We consider it to be a joystick if there is some evidence that it is,
+   * and no evidence that it's something else.
+   *
+   * Unlike SDL, we accept devices with only axes and no buttons as a
+   * possible joystick, unless they have X/Y/Z axes in which case we
+   * assume they're accelerometers. */
+  if ((has_joystick_buttons || has_joystick_axes)
+      && (flags == SRT_INPUT_DEVICE_TYPE_FLAGS_NONE))
+    {
+      g_debug ("Looks like a joystick");
+      flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK;
+    }
+
+  /* If we have *any* keys below BTN_MISC, consider it to be something
+   * a bit keyboard-like, but don't rule out *also* being considered
+   * to be a joystick (again for e.g. the Wiimote). */
+  for (i = 0; i < (BTN_MISC / BITS_PER_LONG); i++)
+    {
+      if (caps->keys[i] != 0)
+        flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS;
+    }
+
+  /* Also non-exclusive: don't rule out a device being a joystick and
+   * having a switch */
+  if (test_bit (EV_SW, caps->ev))
+    flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_SWITCH;
+
+  return flags;
+}
diff --git a/steam-runtime-tools/input-device.h b/steam-runtime-tools/input-device.h
new file mode 100644
index 0000000000000000000000000000000000000000..c2d1ca79644abf9fbcfba29c7b1a932d8617e907
--- /dev/null
+++ b/steam-runtime-tools/input-device.h
@@ -0,0 +1,258 @@
+/*
+ * Input device internals, with parts based on SDL code.
+ *
+ * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org>
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: Zlib
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ */
+
+#pragma once
+
+#if !defined(_SRT_IN_SINGLE_HEADER) && !defined(_SRT_COMPILATION)
+#error "Do not include directly, use <steam-runtime-tools/steam-runtime-tools.h>"
+#endif
+
+#include <gio/gio.h>
+
+#include "steam-runtime-tools/macros.h"
+
+/**
+ * SrtInputDeviceTypeFlags:
+ * @SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK: A joystick, gamepad, steering wheel
+ *  or other game controller (udev ID_INPUT_JOYSTICK)
+ * @SRT_INPUT_DEVICE_TYPE_FLAGS_ACCELEROMETER: An accelerometer, either motion
+ *  controls in a game controller such as Playstation 3 "sixaxis" controllers
+ *  or in the computer itself (udev ID_INPUT_ACCELEROMETER).
+ *  Note that unlike SDL, SrtInputDeviceMonitor always considers
+ *  accelerometers to be their own device type distinct from joysticks;
+ *  there is no equivalent of SDL_HINT_ACCELEROMETER_AS_JOYSTICK.
+ * @SRT_INPUT_DEVICE_TYPE_FLAGS_KEYBOARD: Keyboards with a somewhat full set
+ *  of keys (udev ID_INPUT_KEYBOARD)
+ * @SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS: Any device with keyboard keys, however
+ *  incomplete (udev ID_INPUT_KEY)
+ * @SRT_INPUT_DEVICE_TYPE_FLAGS_MOUSE: A mouse or mouse-like pointer
+ *  controller (udev ID_INPUT_MOUSE)
+ * @SRT_INPUT_DEVICE_TYPE_FLAGS_TOUCHPAD: A touchpad, perhaps built in to a
+ *  game controller like the Playstation 4 controller, or perhaps used
+ *  as a mouse replacement as in most laptops (udev ID_INPUT_TOUCHPAD)
+ * @SRT_INPUT_DEVICE_TYPE_FLAGS_TOUCHSCREEN: A touchscreen (udev ID_INPUT_TOUCHSCREEN)
+ * @SRT_INPUT_DEVICE_TYPE_FLAGS_TABLET: A graphics tablet (udev ID_INPUT_TABLET)
+ * @SRT_INPUT_DEVICE_TYPE_FLAGS_TABLET_PAD: A graphics tablet with buttons
+ *  (udev ID_INPUT_TABLET_PAD)
+ * @SRT_INPUT_DEVICE_TYPE_FLAGS_POINTING_STICK: A mouse-like control similar to
+ *  the IBM/Lenovo Trackpoint (udev ID_INPUT_POINTINGSTICK)
+ * @SRT_INPUT_DEVICE_TYPE_FLAGS_SWITCH: A switch, such as a laptop lid being
+ *  opened (udev ID_INPUT_SWITCH)
+ *
+ * Flags describing a type of input device. An input device can fall into
+ * one or more of these categories.
+ */
+typedef enum
+{
+  SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK = (1 << 0),
+  SRT_INPUT_DEVICE_TYPE_FLAGS_ACCELEROMETER = (1 << 1),
+  SRT_INPUT_DEVICE_TYPE_FLAGS_KEYBOARD = (1 << 2),
+  SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS = (1 << 3),
+  SRT_INPUT_DEVICE_TYPE_FLAGS_MOUSE = (1 << 4),
+  SRT_INPUT_DEVICE_TYPE_FLAGS_TOUCHPAD = (1 << 5),
+  SRT_INPUT_DEVICE_TYPE_FLAGS_TOUCHSCREEN = (1 << 6),
+  SRT_INPUT_DEVICE_TYPE_FLAGS_TABLET = (1 << 7),
+  SRT_INPUT_DEVICE_TYPE_FLAGS_TABLET_PAD = (1 << 8),
+  SRT_INPUT_DEVICE_TYPE_FLAGS_POINTING_STICK = (1 << 9),
+  SRT_INPUT_DEVICE_TYPE_FLAGS_SWITCH = (1 << 10),
+  SRT_INPUT_DEVICE_TYPE_FLAGS_NONE = 0
+} SrtInputDeviceTypeFlags;
+
+/**
+ * SrtInputDeviceInterfaceFlags:
+ * @SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT: evdev event device nodes,
+ *  typically /dev/input/event*
+ * @SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID: Raw USB or Bluetooth HID
+ *  device nodes, typically /dev/hidraw*
+ * @SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE: Only report device nodes that appear to be
+ *  openable in read-only mode
+ * @SRT_INPUT_DEVICE_INTERFACE_FLAGS_READ_WRITE: Only report device nodes that appear to be
+ *  openable in read/write mode
+ *
+ * Flags describing the interface offered by an input device.
+ */
+typedef enum
+{
+  SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT = (1 << 0),
+  SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID = (1 << 1),
+  SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE = (1 << 2),
+  SRT_INPUT_DEVICE_INTERFACE_FLAGS_READ_WRITE = (1 << 3),
+  SRT_INPUT_DEVICE_INTERFACE_FLAGS_NONE = 0
+} SrtInputDeviceInterfaceFlags;
+
+#define SRT_TYPE_INPUT_DEVICE (srt_input_device_get_type ())
+#define SRT_INPUT_DEVICE(x) (G_TYPE_CHECK_INSTANCE_CAST ((x), SRT_TYPE_INPUT_DEVICE, SrtInputDevice))
+#define SRT_IS_INPUT_DEVICE(x) (G_TYPE_CHECK_INSTANCE_TYPE ((x), SRT_TYPE_INPUT_DEVICE))
+#define SRT_INPUT_DEVICE_GET_INTERFACE(x) (G_TYPE_INSTANCE_GET_INTERFACE ((x), SRT_TYPE_INPUT_DEVICE, SrtInputDeviceInterface))
+
+typedef struct _SrtInputDevice SrtInputDevice;
+typedef struct _SrtInputDeviceInterface SrtInputDeviceInterface;
+
+_SRT_PUBLIC
+GType srt_input_device_get_type (void);
+
+_SRT_PUBLIC
+SrtInputDeviceInterfaceFlags srt_input_device_get_interface_flags (SrtInputDevice *device);
+_SRT_PUBLIC
+SrtInputDeviceTypeFlags srt_input_device_get_type_flags (SrtInputDevice *device);
+_SRT_PUBLIC
+const char *srt_input_device_get_dev_node (SrtInputDevice *device);
+_SRT_PUBLIC
+const char *srt_input_device_get_sys_path (SrtInputDevice *device);
+_SRT_PUBLIC
+const char *srt_input_device_get_subsystem (SrtInputDevice *device);
+_SRT_PUBLIC
+gchar **srt_input_device_dup_udev_properties (SrtInputDevice *device);
+_SRT_PUBLIC
+gchar *srt_input_device_dup_uevent (SrtInputDevice *device);
+_SRT_PUBLIC
+gboolean srt_input_device_get_identity (SrtInputDevice *device,
+                                        unsigned int *bus_type,
+                                        unsigned int *vendor_id,
+                                        unsigned int *product_id,
+                                        unsigned int *version);
+_SRT_PUBLIC
+size_t srt_input_device_get_event_capabilities (SrtInputDevice *device,
+                                                unsigned int type,
+                                                unsigned long *storage,
+                                                size_t n_longs);
+_SRT_PUBLIC
+size_t srt_input_device_get_event_types (SrtInputDevice *device,
+                                         unsigned long *storage,
+                                         size_t n_longs);
+_SRT_PUBLIC
+gboolean srt_input_device_has_event_type (SrtInputDevice *device,
+                                          unsigned int type);
+_SRT_PUBLIC
+gboolean srt_input_device_has_event_capability (SrtInputDevice *device,
+                                                unsigned int type,
+                                                unsigned int code);
+_SRT_PUBLIC
+gboolean srt_input_device_has_input_property (SrtInputDevice *device,
+                                              unsigned int input_prop);
+_SRT_PUBLIC
+size_t srt_input_device_get_input_properties (SrtInputDevice *device,
+                                              unsigned long *storage,
+                                              size_t n_longs);
+_SRT_PUBLIC
+SrtInputDeviceTypeFlags srt_input_device_guess_type_flags_from_event_capabilities (SrtInputDevice *device);
+
+_SRT_PUBLIC
+const char *srt_input_device_get_hid_sys_path (SrtInputDevice *device);
+_SRT_PUBLIC
+gchar *srt_input_device_dup_hid_uevent (SrtInputDevice *device);
+_SRT_PUBLIC
+gboolean srt_input_device_get_hid_identity (SrtInputDevice *device,
+                                            unsigned int *bus_type,
+                                            unsigned int *vendor_id,
+                                            unsigned int *product_id,
+                                            const char **name,
+                                            const char **phys,
+                                            const char **uniq);
+
+_SRT_PUBLIC
+const char *srt_input_device_get_input_sys_path (SrtInputDevice *device);
+_SRT_PUBLIC
+gchar *srt_input_device_dup_input_uevent (SrtInputDevice *device);
+_SRT_PUBLIC
+gboolean srt_input_device_get_input_identity (SrtInputDevice *device,
+                                              unsigned int *bus_type,
+                                              unsigned int *vendor_id,
+                                              unsigned int *product_id,
+                                              unsigned int *version,
+                                              const char **name,
+                                              const char **phys,
+                                              const char **uniq);
+
+_SRT_PUBLIC
+const char *srt_input_device_get_usb_device_sys_path (SrtInputDevice *device);
+_SRT_PUBLIC
+gchar *srt_input_device_dup_usb_device_uevent (SrtInputDevice *device);
+_SRT_PUBLIC
+gboolean srt_input_device_get_usb_device_identity (SrtInputDevice *device,
+                                                   unsigned int *vendor_id,
+                                                   unsigned int *product_id,
+                                                   unsigned int *device_version,
+                                                   const char **manufacturer,
+                                                   const char **product,
+                                                   const char **serial);
+
+_SRT_PUBLIC
+int srt_input_device_open (SrtInputDevice *device,
+                           int flags,
+                           GError **error);
+
+/**
+ * SrtInputDeviceMonitorFlags:
+ * @SRT_INPUT_DEVICE_MONITOR_FLAGS_ONCE: Enumerate the devices that were
+ *  available when monitoring starts, and then stop monitoring.
+ * @SRT_INPUT_DEVICE_MONITOR_FLAGS_UDEV: Prefer to get devices from udev.
+ * @SRT_INPUT_DEVICE_MONITOR_FLAGS_DIRECT: Prefer to get devices by
+ *  monitoring /dev, /sys directly.
+ * @SRT_INPUT_DEVICE_MONITOR_FLAGS_NONE: No special behaviour.
+ *
+ * Flags affecting the behaviour of the input device monitor.
+ */
+typedef enum
+{
+  SRT_INPUT_DEVICE_MONITOR_FLAGS_ONCE = (1 << 0),
+  SRT_INPUT_DEVICE_MONITOR_FLAGS_UDEV = (1 << 1),
+  SRT_INPUT_DEVICE_MONITOR_FLAGS_DIRECT = (1 << 2),
+  SRT_INPUT_DEVICE_MONITOR_FLAGS_NONE = 0
+} SrtInputDeviceMonitorFlags;
+
+#define SRT_TYPE_INPUT_DEVICE_MONITOR (srt_input_device_monitor_get_type ())
+#define SRT_INPUT_DEVICE_MONITOR(x) (G_TYPE_CHECK_INSTANCE_CAST ((x), SRT_TYPE_INPUT_DEVICE_MONITOR, SrtInputDeviceMonitor))
+#define SRT_IS_INPUT_DEVICE_MONITOR(x) (G_TYPE_CHECK_INSTANCE_TYPE ((x), SRT_TYPE_INPUT_DEVICE_MONITOR))
+#define SRT_INPUT_DEVICE_MONITOR_GET_INTERFACE(x) (G_TYPE_INSTANCE_GET_INTERFACE ((x), SRT_TYPE_INPUT_DEVICE_MONITOR, SrtInputDeviceMonitorInterface))
+
+typedef struct _SrtInputDeviceMonitor SrtInputDeviceMonitor;
+typedef struct _SrtInputDeviceMonitorInterface SrtInputDeviceMonitorInterface;
+
+_SRT_PUBLIC
+GType srt_input_device_monitor_get_type (void);
+
+_SRT_PUBLIC
+SrtInputDeviceMonitor *srt_input_device_monitor_new (SrtInputDeviceMonitorFlags flags);
+
+_SRT_PUBLIC
+SrtInputDeviceMonitorFlags srt_input_device_monitor_get_flags (SrtInputDeviceMonitor *monitor);
+_SRT_PUBLIC
+void srt_input_device_monitor_request_raw_hid (SrtInputDeviceMonitor *monitor);
+_SRT_PUBLIC
+void srt_input_device_monitor_request_evdev (SrtInputDeviceMonitor *monitor);
+
+_SRT_PUBLIC
+gboolean srt_input_device_monitor_is_active (SrtInputDeviceMonitor *monitor);
+_SRT_PUBLIC
+gboolean srt_input_device_monitor_start (SrtInputDeviceMonitor *monitor,
+                                         GError **error);
+_SRT_PUBLIC
+void srt_input_device_monitor_stop (SrtInputDeviceMonitor *monitor);
+
+#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtInputDevice, g_object_unref)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtInputDeviceMonitor, g_object_unref)
+#endif
diff --git a/steam-runtime-tools/meson.build b/steam-runtime-tools/meson.build
index 6c0acce1c1f5ecc6f5a60ad4595b13942f88d6c4..f92ef4792c4e63d16b36494b46d6e7bb65045a3d 100644
--- a/steam-runtime-tools/meson.build
+++ b/steam-runtime-tools/meson.build
@@ -28,10 +28,14 @@ libsteamrt_sources = [
     'cpu-feature.c',
     'desktop-entry-internal.h',
     'desktop-entry.c',
+    'direct-input-device-internal.h',
+    'direct-input-device.c',
     'glib-backports.c',
     'glib-backports-internal.h',
     'graphics-internal.h',
     'graphics.c',
+    'input-device-internal.h',
+    'input-device.c',
     'json-glib-backports.c',
     'json-glib-backports-internal.h',
     'json-utils.c',
@@ -46,9 +50,13 @@ libsteamrt_sources = [
     'resolve-in-sysroot.c',
     'runtime-internal.h',
     'runtime.c',
+    'simple-input-device-internal.h',
+    'simple-input-device.c',
     'steam-internal.h',
     'steam.c',
     'system-info.c',
+    'udev-input-device-internal.h',
+    'udev-input-device.c',
     'utils-internal.h',
     'utils.c',
     'xdg-portal.c',
@@ -59,6 +67,7 @@ libsteamrt_public_headers = [
     'cpu-feature.h',
     'desktop-entry.h',
     'graphics.h',
+    'input-device.h',
     'library.h',
     'locale.h',
     'macros.h',
diff --git a/steam-runtime-tools/simple-input-device-internal.h b/steam-runtime-tools/simple-input-device-internal.h
new file mode 100644
index 0000000000000000000000000000000000000000..ecdd4bdc5e83d902dd3e0488b19bf4891f1899a5
--- /dev/null
+++ b/steam-runtime-tools/simple-input-device-internal.h
@@ -0,0 +1,110 @@
+/*<private_header>*/
+/*
+ * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org>
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: Zlib
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ */
+
+#pragma once
+
+#include "steam-runtime-tools/glib-backports-internal.h"
+#include "steam-runtime-tools/input-device-internal.h"
+
+#include <json-glib/json-glib.h>
+
+#define SRT_TYPE_SIMPLE_INPUT_DEVICE (_srt_simple_input_device_get_type ())
+
+typedef struct _SrtSimpleInputDevice SrtSimpleInputDevice;
+typedef struct _SrtSimpleInputDeviceClass SrtSimpleInputDeviceClass;
+
+#define SRT_TYPE_SIMPLE_INPUT_DEVICE (_srt_simple_input_device_get_type ())
+#define SRT_SIMPLE_INPUT_DEVICE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SRT_TYPE_SIMPLE_INPUT_DEVICE, SrtSimpleInputDevice))
+#define SRT_IS_SIMPLE_INPUT_DEVICE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SRT_TYPE_SIMPLE_INPUT_DEVICE))
+#define SRT_SIMPLE_INPUT_DEVICE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), SRT_TYPE_SIMPLE_INPUT_DEVICE, SrtSimpleInputDeviceClass))
+#define SRT_SIMPLE_INPUT_DEVICE_CLASS(c) (G_TYPE_CHECK_CLASS_CAST ((c), SRT_TYPE_SIMPLE_INPUT_DEVICE, SrtSimpleInputDeviceClass))
+#define SRT_IS_SIMPLE_INPUT_DEVICE_CLASS(c) (G_TYPE_CHECK_CLASS_TYPE ((c), SRT_TYPE_SIMPLE_INPUT_DEVICE))
+
+G_GNUC_INTERNAL
+GType _srt_simple_input_device_get_type (void);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtSimpleInputDevice, g_object_unref)
+
+struct _SrtSimpleInputDevice
+{
+  GObject parent;
+
+  gchar *dev_node;
+  gchar *sys_path;
+  gchar *subsystem;
+  gchar **udev_properties;
+  gchar *uevent;
+
+  struct
+  {
+    gchar *sys_path;
+    gchar *uevent;
+    gchar *name;
+    gchar *phys;
+    gchar *uniq;
+    guint32 bus_type;
+    guint32 product_id;
+    guint32 vendor_id;
+  } hid_ancestor;
+
+  struct
+  {
+    gchar *sys_path;
+    gchar *uevent;
+    gchar *name;
+    gchar *phys;
+    gchar *uniq;
+    guint32 bus_type;
+    guint32 product_id;
+    guint32 vendor_id;
+    guint32 version;
+  } input_ancestor;
+
+  struct
+  {
+    gchar *sys_path;
+    gchar *uevent;
+    gchar *manufacturer;
+    gchar *product;
+    gchar *serial;
+    guint32 product_id;
+    guint32 vendor_id;
+    guint32 device_version;
+  } usb_device_ancestor;
+
+  SrtEvdevCapabilities evdev_caps;
+  SrtInputDeviceInterfaceFlags iface_flags;
+  SrtInputDeviceTypeFlags type_flags;
+  guint32 bus_type;
+  guint32 vendor_id;
+  guint32 product_id;
+  guint32 version;
+};
+
+struct _SrtSimpleInputDeviceClass
+{
+  GObjectClass parent;
+};
+
+SrtSimpleInputDevice *_srt_simple_input_device_new_from_json (JsonObject *obj);
diff --git a/steam-runtime-tools/simple-input-device.c b/steam-runtime-tools/simple-input-device.c
new file mode 100644
index 0000000000000000000000000000000000000000..a45ca41ca952870394bb1422da52f38ac8fe66e1
--- /dev/null
+++ b/steam-runtime-tools/simple-input-device.c
@@ -0,0 +1,548 @@
+/*
+ * An input device loaded from JSON or similar.
+ *
+ * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org>
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: Zlib
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ */
+
+#include "steam-runtime-tools/simple-input-device-internal.h"
+
+#include <libglnx.h>
+
+#include "steam-runtime-tools/enums.h"
+#include "steam-runtime-tools/glib-backports-internal.h"
+#include "steam-runtime-tools/input-device.h"
+#include "steam-runtime-tools/input-device-internal.h"
+#include "steam-runtime-tools/json-utils-internal.h"
+#include "steam-runtime-tools/utils-internal.h"
+
+static void srt_simple_input_device_iface_init (SrtInputDeviceInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (SrtSimpleInputDevice,
+                         _srt_simple_input_device,
+                         G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (SRT_TYPE_INPUT_DEVICE,
+                                                srt_simple_input_device_iface_init))
+
+static void
+_srt_simple_input_device_init (SrtSimpleInputDevice *self)
+{
+}
+
+static void
+srt_simple_input_device_finalize (GObject *object)
+{
+  SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (object);
+
+  g_free (self->dev_node);
+  g_free (self->sys_path);
+  g_free (self->subsystem);
+  g_strfreev (self->udev_properties);
+  g_free (self->uevent);
+
+  g_free (self->hid_ancestor.sys_path);
+  g_free (self->hid_ancestor.uevent);
+  g_free (self->hid_ancestor.name);
+  g_free (self->hid_ancestor.phys);
+  g_free (self->hid_ancestor.uniq);
+
+  g_free (self->input_ancestor.sys_path);
+  g_free (self->input_ancestor.uevent);
+  g_free (self->input_ancestor.name);
+  g_free (self->input_ancestor.phys);
+  g_free (self->input_ancestor.uniq);
+
+  g_free (self->usb_device_ancestor.sys_path);
+  g_free (self->usb_device_ancestor.uevent);
+  g_free (self->usb_device_ancestor.manufacturer);
+  g_free (self->usb_device_ancestor.product);
+  g_free (self->usb_device_ancestor.serial);
+
+  G_OBJECT_CLASS (_srt_simple_input_device_parent_class)->finalize (object);
+}
+
+static void
+_srt_simple_input_device_class_init (SrtSimpleInputDeviceClass *cls)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (cls);
+
+  object_class->finalize = srt_simple_input_device_finalize;
+}
+
+static SrtInputDeviceInterfaceFlags
+srt_simple_input_device_get_interface_flags (SrtInputDevice *device)
+{
+  SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device);
+
+  return self->iface_flags;
+}
+
+static SrtInputDeviceTypeFlags
+srt_simple_input_device_get_type_flags (SrtInputDevice *device)
+{
+  SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device);
+
+  return self->type_flags;
+}
+
+static const char *
+srt_simple_input_device_get_dev_node (SrtInputDevice *device)
+{
+  SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device);
+
+  return self->dev_node;
+}
+
+static const char *
+srt_simple_input_device_get_subsystem (SrtInputDevice *device)
+{
+  SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device);
+
+  return self->subsystem;
+}
+
+static const char *
+srt_simple_input_device_get_sys_path (SrtInputDevice *device)
+{
+  SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device);
+
+  return self->sys_path;
+}
+
+static gboolean
+srt_simple_input_device_get_identity (SrtInputDevice *device,
+                                      unsigned int *bus_type,
+                                      unsigned int *vendor_id,
+                                      unsigned int *product_id,
+                                      unsigned int *version)
+{
+  SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device);
+
+  if (self->bus_type == 0
+      && self->vendor_id == 0
+      && self->product_id == 0
+      && self->version == 0)
+    return FALSE;
+
+  if (bus_type != NULL)
+    *bus_type = self->bus_type;
+
+  if (vendor_id != NULL)
+    *vendor_id = self->vendor_id;
+
+  if (product_id != NULL)
+    *product_id = self->product_id;
+
+  if (version != NULL)
+    *version = self->version;
+
+  return TRUE;
+}
+
+static gchar **
+srt_simple_input_device_dup_udev_properties (SrtInputDevice *device)
+{
+  SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device);
+
+  return g_strdupv (self->udev_properties);
+}
+
+static gchar *
+srt_simple_input_device_dup_uevent (SrtInputDevice *device)
+{
+  SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device);
+
+  return g_strdup (self->uevent);
+}
+
+static const SrtEvdevCapabilities *
+srt_simple_input_device_peek_event_capabilities (SrtInputDevice *device)
+{
+  SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device);
+
+  return &self->evdev_caps;
+}
+
+static const char *
+srt_simple_input_device_get_hid_sys_path (SrtInputDevice *device)
+{
+  SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device);
+
+  return self->hid_ancestor.sys_path;
+}
+
+static gboolean
+srt_simple_input_device_get_hid_identity (SrtInputDevice *device,
+                                          unsigned int *bus_type,
+                                          unsigned int *vendor_id,
+                                          unsigned int *product_id,
+                                          const char **name,
+                                          const char **phys,
+                                          const char **uniq)
+{
+  SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device);
+
+  if (self->hid_ancestor.sys_path == NULL)
+    return FALSE;
+
+  if (bus_type != NULL)
+    *bus_type = self->hid_ancestor.bus_type;
+
+  if (vendor_id != NULL)
+    *vendor_id = self->hid_ancestor.vendor_id;
+
+  if (product_id != NULL)
+    *product_id = self->hid_ancestor.product_id;
+
+  if (name != NULL)
+    *name = self->hid_ancestor.name;
+
+  if (phys != NULL)
+    *phys = self->hid_ancestor.phys;
+
+  if (uniq != NULL)
+    *uniq = self->hid_ancestor.uniq;
+
+  return TRUE;
+}
+
+static gchar *
+srt_simple_input_device_dup_hid_uevent (SrtInputDevice *device)
+{
+  SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device);
+
+  return g_strdup (self->hid_ancestor.uevent);
+}
+
+static const char *
+srt_simple_input_device_get_input_sys_path (SrtInputDevice *device)
+{
+  SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device);
+
+  return self->input_ancestor.sys_path;
+}
+
+static gboolean
+srt_simple_input_device_get_input_identity (SrtInputDevice *device,
+                                            unsigned int *bus_type,
+                                            unsigned int *vendor_id,
+                                            unsigned int *product_id,
+                                            unsigned int *version,
+                                            const char **name,
+                                            const char **phys,
+                                            const char **uniq)
+{
+  SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device);
+
+  if (self->input_ancestor.sys_path == NULL)
+    return FALSE;
+
+  if (bus_type != NULL)
+    *bus_type = self->input_ancestor.bus_type;
+
+  if (vendor_id != NULL)
+    *vendor_id = self->input_ancestor.vendor_id;
+
+  if (product_id != NULL)
+    *product_id = self->input_ancestor.product_id;
+
+  if (version != NULL)
+    *version = self->input_ancestor.version;
+
+  if (name != NULL)
+    *name = self->input_ancestor.name;
+
+  if (phys != NULL)
+    *phys = self->input_ancestor.phys;
+
+  if (uniq != NULL)
+    *uniq = self->input_ancestor.uniq;
+
+  return TRUE;
+}
+
+static gchar *
+srt_simple_input_device_dup_input_uevent (SrtInputDevice *device)
+{
+  SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device);
+
+  return g_strdup (self->input_ancestor.uevent);
+}
+
+static const char *
+srt_simple_input_device_get_usb_device_sys_path (SrtInputDevice *device)
+{
+  SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device);
+
+  return self->usb_device_ancestor.sys_path;
+}
+
+static gboolean
+srt_simple_input_device_get_usb_device_identity (SrtInputDevice *device,
+                                                 unsigned int *vendor_id,
+                                                 unsigned int *product_id,
+                                                 unsigned int *version,
+                                                 const char **manufacturer,
+                                                 const char **product,
+                                                 const char **serial)
+{
+  SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device);
+
+  if (self->usb_device_ancestor.sys_path == NULL)
+    return FALSE;
+
+  if (vendor_id != NULL)
+    *vendor_id = self->usb_device_ancestor.vendor_id;
+
+  if (product_id != NULL)
+    *product_id = self->usb_device_ancestor.product_id;
+
+  if (version != NULL)
+    *version = self->usb_device_ancestor.device_version;
+
+  if (manufacturer != NULL)
+    *manufacturer = self->usb_device_ancestor.manufacturer;
+
+  if (product != NULL)
+    *product = self->usb_device_ancestor.product;
+
+  if (serial != NULL)
+    *serial = self->usb_device_ancestor.serial;
+
+  return TRUE;
+}
+
+static gchar *
+srt_simple_input_device_dup_usb_device_uevent (SrtInputDevice *device)
+{
+  SrtSimpleInputDevice *self = SRT_SIMPLE_INPUT_DEVICE (device);
+
+  return g_strdup (self->usb_device_ancestor.uevent);
+}
+
+static void
+srt_simple_input_device_iface_init (SrtInputDeviceInterface *iface)
+{
+#define IMPLEMENT(x) iface->x = srt_simple_input_device_ ## x
+
+  IMPLEMENT (get_type_flags);
+  IMPLEMENT (get_interface_flags);
+  IMPLEMENT (get_dev_node);
+  IMPLEMENT (get_sys_path);
+  IMPLEMENT (get_subsystem);
+  IMPLEMENT (get_identity);
+  IMPLEMENT (dup_udev_properties);
+  IMPLEMENT (dup_uevent);
+  IMPLEMENT (peek_event_capabilities);
+
+  IMPLEMENT (get_hid_sys_path);
+  IMPLEMENT (get_hid_identity);
+  IMPLEMENT (get_input_sys_path);
+  IMPLEMENT (get_input_identity);
+  IMPLEMENT (get_usb_device_sys_path);
+  IMPLEMENT (get_usb_device_identity);
+
+  IMPLEMENT (dup_hid_uevent);
+  IMPLEMENT (dup_input_uevent);
+  IMPLEMENT (dup_usb_device_uevent);
+
+#undef IMPLEMENT
+}
+
+static gchar *
+dup_json_string_member (JsonObject *obj,
+                        const gchar *name)
+{
+  JsonNode *node = json_object_get_member (obj, name);
+
+  if (node == NULL)
+    return NULL;
+
+  /* This returns NULL without error if it is a non-string */
+  return json_node_dup_string (node);
+}
+
+static unsigned long
+get_json_hex_member (JsonObject *obj,
+                     const gchar *name)
+{
+  JsonNode *node = json_object_get_member (obj, name);
+  const char *s;
+
+  if (node == NULL)
+    return 0;
+
+  s = json_node_get_string (node);
+
+  if (s == NULL)
+    return 0;
+
+  if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X'))
+    s += 2;
+
+  return strtoul (s, NULL, 16);
+}
+
+static JsonObject *
+get_json_object_member (JsonObject *obj,
+                        const char *name)
+{
+  JsonNode *node = json_object_get_member (obj, name);
+
+  if (node != NULL && JSON_NODE_HOLDS_OBJECT (node))
+    return json_node_get_object (node);
+
+  return NULL;
+}
+
+static gchar *
+dup_json_uevent (JsonObject *obj)
+{
+  return _srt_json_object_dup_array_of_lines_member (obj, "uevent");
+}
+
+static void
+get_json_evdev_caps (JsonObject *obj,
+                     const char *name,
+                     unsigned long *longs,
+                     size_t n_longs)
+{
+  JsonNode *node = json_object_get_member (obj, name);
+  /* The first pointer that is out of bounds for longs */
+  unsigned char *limit = (unsigned char *) &longs[n_longs];
+  /* The output position in longs */
+  unsigned char *out = (unsigned char *) longs;
+  /* The input position in the string we are parsing */
+  const char *iter;
+  size_t i;
+
+  if (node == NULL)
+    return;
+
+  iter = json_node_get_string (node);
+
+  if (iter == NULL)
+    return;
+
+  while (*iter != '\0')
+    {
+      unsigned int this_byte;
+      int used;
+
+      while (*iter == ' ')
+        iter++;
+
+      if (sscanf (iter, "%x%n", &this_byte, &used) == 1)
+        iter += used;
+      else
+        break;
+
+      if (out < limit)
+        *(out++) = (unsigned char) this_byte;
+      else
+        break;
+    }
+
+  for (i = 0; i < n_longs; i++)
+    longs[i] = GULONG_FROM_LE (longs[i]);
+}
+
+SrtSimpleInputDevice *
+_srt_simple_input_device_new_from_json (JsonObject *obj)
+{
+  SrtSimpleInputDevice *self = g_object_new (SRT_TYPE_SIMPLE_INPUT_DEVICE,
+                                             NULL);
+  JsonObject *sub;
+
+  self->sys_path = dup_json_string_member (obj, "sys_path");
+  self->dev_node = dup_json_string_member (obj, "dev_node");
+  self->subsystem = dup_json_string_member (obj, "subsystem");
+  self->bus_type  = get_json_hex_member (obj, "bus_type");
+  self->vendor_id  = get_json_hex_member (obj, "vendor_id");
+  self->product_id  = get_json_hex_member (obj, "product_id");
+  self->version  = get_json_hex_member (obj, "version");
+
+  self->iface_flags = srt_get_flags_from_json_array (SRT_TYPE_INPUT_DEVICE_INTERFACE_FLAGS,
+                                                     obj,
+                                                     "interface_flags",
+                                                     SRT_INPUT_DEVICE_INTERFACE_FLAGS_NONE);
+  self->type_flags = srt_get_flags_from_json_array (SRT_TYPE_INPUT_DEVICE_TYPE_FLAGS,
+                                                    obj,
+                                                    "type_flags",
+                                                    SRT_INPUT_DEVICE_TYPE_FLAGS_NONE);
+
+  if ((sub = get_json_object_member (obj, "evdev")) != NULL)
+    {
+      get_json_evdev_caps (sub, "raw_types", self->evdev_caps.ev,
+                           G_N_ELEMENTS (self->evdev_caps.ev));
+      get_json_evdev_caps (sub, "raw_abs", self->evdev_caps.abs,
+                           G_N_ELEMENTS (self->evdev_caps.abs));
+      get_json_evdev_caps (sub, "raw_rel", self->evdev_caps.rel,
+                           G_N_ELEMENTS (self->evdev_caps.rel));
+      get_json_evdev_caps (sub, "raw_keys", self->evdev_caps.keys,
+                           G_N_ELEMENTS (self->evdev_caps.keys));
+      get_json_evdev_caps (sub, "raw_input_properties",
+                           self->evdev_caps.props,
+                           G_N_ELEMENTS (self->evdev_caps.props));
+    }
+
+  self->udev_properties = _srt_json_object_dup_strv_member (obj, "udev_properties", NULL);
+  self->uevent = dup_json_uevent (obj);
+
+  if ((sub = get_json_object_member (obj, "hid_ancestor")) != NULL)
+    {
+      self->hid_ancestor.sys_path = dup_json_string_member (sub, "sys_path");
+      self->hid_ancestor.name = dup_json_string_member (sub, "name");
+      self->hid_ancestor.bus_type = get_json_hex_member (sub, "bus_type");
+      self->hid_ancestor.vendor_id = get_json_hex_member (sub, "vendor_id");
+      self->hid_ancestor.product_id = get_json_hex_member (sub, "product_id");
+      self->hid_ancestor.uniq = dup_json_string_member (sub, "uniq");
+      self->hid_ancestor.phys = dup_json_string_member (sub, "phys");
+      self->hid_ancestor.uevent = dup_json_uevent (sub);
+    }
+
+  if ((sub = get_json_object_member (obj, "input_ancestor")) != NULL)
+    {
+      self->input_ancestor.sys_path = dup_json_string_member (sub, "sys_path");
+      self->input_ancestor.name = dup_json_string_member (sub, "name");
+      self->input_ancestor.bus_type = get_json_hex_member (sub, "bus_type");
+      self->input_ancestor.vendor_id = get_json_hex_member (sub, "vendor_id");
+      self->input_ancestor.product_id = get_json_hex_member (sub, "product_id");
+      self->input_ancestor.version = get_json_hex_member (sub, "version");
+      self->input_ancestor.uniq = dup_json_string_member (sub, "uniq");
+      self->input_ancestor.phys = dup_json_string_member (sub, "phys");
+      self->input_ancestor.uevent = dup_json_uevent (sub);
+    }
+
+  if ((sub = get_json_object_member (obj, "usb_device_ancestor")) != NULL)
+    {
+      self->usb_device_ancestor.sys_path = dup_json_string_member (sub, "sys_path");
+      self->usb_device_ancestor.vendor_id = get_json_hex_member (sub, "vendor_id");
+      self->usb_device_ancestor.product_id = get_json_hex_member (sub, "product_id");
+      self->usb_device_ancestor.device_version = get_json_hex_member (sub, "version");
+      self->usb_device_ancestor.manufacturer = dup_json_string_member (sub, "manufacturer");
+      self->usb_device_ancestor.product = dup_json_string_member (sub, "product");
+      self->usb_device_ancestor.serial = dup_json_string_member (sub, "serial");
+      self->usb_device_ancestor.uevent = dup_json_uevent (sub);
+    }
+
+  return self;
+}
diff --git a/steam-runtime-tools/steam-runtime-tools.h b/steam-runtime-tools/steam-runtime-tools.h
index 29e02af44daeb3d28bf12958e8d6a828e884c2c3..c6009f7c81d70e17bca872da67e81f29f7542307 100644
--- a/steam-runtime-tools/steam-runtime-tools.h
+++ b/steam-runtime-tools/steam-runtime-tools.h
@@ -32,6 +32,7 @@
 #include <steam-runtime-tools/desktop-entry.h>
 #include <steam-runtime-tools/enums.h>
 #include <steam-runtime-tools/graphics.h>
+#include <steam-runtime-tools/input-device.h>
 #include <steam-runtime-tools/library.h>
 #include <steam-runtime-tools/locale.h>
 #include <steam-runtime-tools/macros.h>
diff --git a/steam-runtime-tools/udev-input-device-internal.h b/steam-runtime-tools/udev-input-device-internal.h
new file mode 100644
index 0000000000000000000000000000000000000000..9ee0d36c83568935f1c0a62620eaa30eafb3a186
--- /dev/null
+++ b/steam-runtime-tools/udev-input-device-internal.h
@@ -0,0 +1,68 @@
+/*
+ * libudev-based input device monitor, loosely based on SDL code.
+ *
+ * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org>
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: Zlib
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ */
+
+#pragma once
+
+#include "steam-runtime-tools/glib-backports-internal.h"
+#include "steam-runtime-tools/input-device.h"
+
+typedef struct _SrtUdevInputDevice SrtUdevInputDevice;
+typedef struct _SrtUdevInputDeviceClass SrtUdevInputDeviceClass;
+
+#define SRT_TYPE_UDEV_INPUT_DEVICE (_srt_udev_input_device_get_type ())
+#define SRT_UDEV_INPUT_DEVICE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SRT_TYPE_UDEV_INPUT_DEVICE, SrtUdevInputDevice))
+#define SRT_IS_UDEV_INPUT_DEVICE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SRT_TYPE_UDEV_INPUT_DEVICE))
+#define SRT_UDEV_INPUT_DEVICE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), SRT_TYPE_UDEV_INPUT_DEVICE, SrtUdevInputDeviceClass))
+#define SRT_UDEV_INPUT_DEVICE_CLASS(c) (G_TYPE_CHECK_CLASS_CAST ((c), SRT_TYPE_UDEV_INPUT_DEVICE, SrtUdevInputDeviceClass))
+#define SRT_IS_UDEV_INPUT_DEVICE_CLASS(c) (G_TYPE_CHECK_CLASS_TYPE ((c), SRT_TYPE_UDEV_INPUT_DEVICE))
+
+G_GNUC_INTERNAL
+GType _srt_udev_input_device_get_type (void);
+
+#define SRT_TYPE_UDEV_INPUT_DEVICE_MONITOR (_srt_udev_input_device_monitor_get_type ())
+#define SRT_UDEV_INPUT_DEVICE_MONITOR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SRT_TYPE_UDEV_INPUT_DEVICE_MONITOR, SrtUdevInputDeviceMonitor))
+#define SRT_IS_UDEV_INPUT_DEVICE_MONITOR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SRT_TYPE_UDEV_INPUT_DEVICE_MONITOR))
+#define SRT_UDEV_INPUT_DEVICE_MONITOR_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), SRT_TYPE_UDEV_INPUT_DEVICE_MONITOR, SrtUdevInputDeviceMonitorClass))
+#define SRT_UDEV_INPUT_DEVICE_MONITOR_CLASS(c) (G_TYPE_CHECK_CLASS_CAST ((c), SRT_TYPE_UDEV_INPUT_DEVICE_MONITOR, SrtUdevInputDeviceMonitorClass))
+#define SRT_IS_UDEV_INPUT_DEVICE_MONITOR_CLASS(c) (G_TYPE_CHECK_CLASS_TYPE ((c), SRT_TYPE_UDEV_INPUT_DEVICE_MONITOR))
+
+typedef struct _SrtUdevInputDeviceMonitor SrtUdevInputDeviceMonitor;
+typedef struct _SrtUdevInputDeviceMonitorClass SrtUdevInputDeviceMonitorClass;
+
+G_GNUC_INTERNAL
+GType _srt_udev_input_device_monitor_get_type (void);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtUdevInputDevice, g_object_unref)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtUdevInputDeviceMonitor, g_object_unref)
+
+static inline SrtInputDeviceMonitor *
+srt_udev_input_device_monitor_new (SrtInputDeviceMonitorFlags flags,
+                                   GError **error)
+{
+  return g_initable_new (SRT_TYPE_UDEV_INPUT_DEVICE_MONITOR,
+                         NULL /* cancellable */, error,
+                         "flags", flags,
+                         NULL);
+}
diff --git a/steam-runtime-tools/udev-input-device.c b/steam-runtime-tools/udev-input-device.c
new file mode 100644
index 0000000000000000000000000000000000000000..fcccc075ce9c18a97756d1ba6eee6685c3337570
--- /dev/null
+++ b/steam-runtime-tools/udev-input-device.c
@@ -0,0 +1,1337 @@
+/*
+ * libudev-based input device monitor, loosely based on SDL code.
+ *
+ * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org>
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: Zlib
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ */
+
+#include "steam-runtime-tools/udev-input-device-internal.h"
+
+#include <dlfcn.h>
+#include <linux/input.h>
+
+#include <glib-unix.h>
+#include <libglnx.h>
+
+#include "steam-runtime-tools/glib-backports-internal.h"
+#include "steam-runtime-tools/input-device.h"
+#include "steam-runtime-tools/input-device-internal.h"
+#include "steam-runtime-tools/utils-internal.h"
+
+static void srt_udev_input_device_iface_init (SrtInputDeviceInterface *iface);
+static void srt_udev_input_device_monitor_iface_init (SrtInputDeviceMonitorInterface *iface);
+static void srt_udev_input_device_monitor_initable_iface_init (GInitableIface *iface);
+
+struct udev;
+struct udev_list_entry;
+struct udev_device;
+struct udev_monitor;
+struct udev_enumerate;
+
+static struct
+{
+  void *handle;
+
+  struct udev *(*udev_new) (void);
+  struct udev *(*udev_unref) (struct udev *);
+
+  const char *(*udev_list_entry_get_name) (struct udev_list_entry *);
+  struct udev_list_entry *(*udev_list_entry_get_next) (struct udev_list_entry *);
+
+  struct udev_device *(*udev_device_new_from_syspath) (struct udev *,
+                                                       const char *);
+  const char *(*udev_device_get_action) (struct udev_device *);
+  const char *(*udev_device_get_devnode) (struct udev_device *);
+  const char *(*udev_device_get_subsystem) (struct udev_device *);
+  const char *(*udev_device_get_syspath) (struct udev_device *);
+  struct udev_list_entry *(*udev_device_get_properties_list_entry) (struct udev_device *);
+  const char *(*udev_device_get_property_value) (struct udev_device *, const char *);
+  const char *(*udev_device_get_sysattr_value) (struct udev_device *, const char *);
+  struct udev_device *(*udev_device_get_parent_with_subsystem_devtype) (struct udev_device *, const char *, const char *);
+  struct udev_device *(*udev_device_ref) (struct udev_device *);
+  struct udev_device *(*udev_device_unref) (struct udev_device *);
+
+  struct udev_monitor *(*udev_monitor_new_from_netlink) (struct udev *,
+                                                         const char *);
+  int (*udev_monitor_filter_add_match_subsystem_devtype) (struct udev_monitor *,
+                                                          const char *,
+                                                          const char *);
+  int (*udev_monitor_enable_receiving) (struct udev_monitor *);
+  struct udev_device *(*udev_monitor_receive_device) (struct udev_monitor *);
+  int (*udev_monitor_get_fd) (struct udev_monitor *);
+  struct udev_monitor *(*udev_monitor_unref) (struct udev_monitor *);
+
+  struct udev_enumerate *(*udev_enumerate_new) (struct udev *);
+  int (*udev_enumerate_add_match_subsystem) (struct udev_enumerate *,
+                                             const char *);
+  int (*udev_enumerate_scan_devices) (struct udev_enumerate *);
+  struct udev_list_entry *(*udev_enumerate_get_list_entry) (struct udev_enumerate *);
+  struct udev_enumerate *(*udev_enumerate_unref) (struct udev_enumerate *);
+} symbols;
+
+static gboolean
+has_all_flags (unsigned int have,
+               unsigned int want)
+{
+  return ((have & want) == want);
+}
+
+static gboolean
+get_evdev_caps_from_udev (struct udev_device *dev,
+                          const char *attr,
+                          unsigned long *bitmask,
+                          size_t bitmask_len_longs)
+{
+  g_autofree char *text = NULL;
+  const char *value;
+  char *word;
+  int i;
+  unsigned long v;
+
+  memset (bitmask, 0, bitmask_len_longs * sizeof (*bitmask));
+  value = symbols.udev_device_get_sysattr_value (dev, attr);
+
+  if (!value)
+    return FALSE;
+
+  text = g_strdup (value);
+  i = 0;
+
+  while ((word = strrchr (text, ' ')) != NULL)
+    {
+      v = strtoul (word + 1, NULL, 16);
+
+      if (i < bitmask_len_longs)
+        bitmask[i] = v;
+
+      ++i;
+      *word = '\0';
+    }
+
+  v = strtoul (text, NULL, 16);
+
+  if (i < bitmask_len_longs)
+    bitmask[i] = v;
+
+  return TRUE;
+}
+
+static struct udev_device *
+find_input_ancestor (struct udev_device *dev)
+{
+  struct udev_device *ancestor;
+
+  for (ancestor = dev;
+       ancestor != NULL;
+       ancestor = symbols.udev_device_get_parent_with_subsystem_devtype (ancestor, "input", NULL))
+    {
+      if (symbols.udev_device_get_sysattr_value (ancestor, "capabilities/ev") != NULL)
+        return ancestor;
+    }
+
+  return NULL;
+}
+
+struct _SrtUdevInputDevice
+{
+  GObject parent;
+
+  struct udev_device *dev;                    /* owned */
+
+  SrtEvdevCapabilities evdev_caps;
+
+  struct
+  {
+    struct udev_device *dev;                  /* borrowed from child dev */
+    gchar *name;
+    gchar *phys;
+    gchar *uniq;
+    guint32 bus_type;
+    guint32 product_id;
+    guint32 vendor_id;
+  } hid_ancestor;
+
+  struct
+  {
+    struct udev_device *dev;                  /* borrowed from child dev */
+    gchar *name;
+    gchar *phys;
+    gchar *uniq;
+    guint32 bus_type;
+    guint32 product_id;
+    guint32 vendor_id;
+    guint32 version;
+  } input_ancestor;
+
+  struct
+  {
+    struct udev_device *dev;                  /* borrowed from child dev */
+    guint32 product_id;
+    guint32 vendor_id;
+    guint32 device_version;
+  } usb_device_ancestor;
+
+  SrtInputDeviceTypeFlags type_flags;
+  SrtInputDeviceInterfaceFlags iface_flags;
+};
+
+struct _SrtUdevInputDeviceClass
+{
+  GObjectClass parent;
+};
+
+struct _SrtUdevInputDeviceMonitor
+{
+  GObject parent;
+
+  struct udev *context;
+  struct udev_monitor *monitor;
+  GHashTable *devices;
+  GMainContext *monitor_context;
+  GSource *monitor_source;
+
+  gboolean want_evdev;
+  gboolean want_hidraw;
+  SrtInputDeviceMonitorFlags flags;
+  enum
+  {
+    NOT_STARTED = 0,
+    STARTED,
+    STOPPED
+  } state;
+};
+
+struct _SrtUdevInputDeviceMonitorClass
+{
+  GObjectClass parent;
+};
+
+G_DEFINE_TYPE_WITH_CODE (SrtUdevInputDevice,
+                         _srt_udev_input_device,
+                         G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (SRT_TYPE_INPUT_DEVICE,
+                                                srt_udev_input_device_iface_init))
+
+G_DEFINE_TYPE_WITH_CODE (SrtUdevInputDeviceMonitor,
+                         _srt_udev_input_device_monitor,
+                         G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE,
+                                                srt_udev_input_device_monitor_initable_iface_init)
+                         G_IMPLEMENT_INTERFACE (SRT_TYPE_INPUT_DEVICE_MONITOR,
+                                                srt_udev_input_device_monitor_iface_init))
+
+static void
+_srt_udev_input_device_init (SrtUdevInputDevice *self)
+{
+}
+
+static void
+srt_udev_input_device_finalize (GObject *object)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (object);
+
+  g_clear_pointer (&self->dev, symbols.udev_device_unref);
+  g_free (self->hid_ancestor.name);
+  g_free (self->hid_ancestor.phys);
+  g_free (self->hid_ancestor.uniq);
+  g_free (self->input_ancestor.name);
+  g_free (self->input_ancestor.phys);
+  g_free (self->input_ancestor.uniq);
+
+  G_OBJECT_CLASS (_srt_udev_input_device_parent_class)->finalize (object);
+}
+
+static void
+_srt_udev_input_device_class_init (SrtUdevInputDeviceClass *cls)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (cls);
+
+  object_class->finalize = srt_udev_input_device_finalize;
+}
+
+/*
+ * Get a sysfs attribute that is a string.
+ *
+ * On success, set *out and return TRUE.
+ * On failure, leave *out untouched and return FALSE.
+ */
+static gboolean
+get_string (struct udev_device *dev,
+            const char *attribute,
+            const gchar **out)
+{
+  const char *tmp;
+
+  if (dev == NULL)
+    return FALSE;
+
+  tmp = symbols.udev_device_get_sysattr_value (dev, attribute);
+
+  if (tmp == NULL)
+    return FALSE;
+
+  if (out != NULL)
+    *out = tmp;
+
+  return TRUE;
+}
+
+/*
+ * Get a sysfs attribute that is a uint32 (or smaller) in hexadecimal
+ * (with or without 0x prefix).
+ *
+ * On success, set *out and return TRUE.
+ * On failure, leave *out untouched and return FALSE.
+ */
+static gboolean
+get_uint32_hex (struct udev_device *dev,
+                const char *attribute,
+                guint32 *out)
+{
+  const char *tmp;
+  guint64 ret;
+  gchar *endptr;
+
+  if (!get_string (dev, attribute, &tmp))
+    return FALSE;
+
+  if (tmp[0] == '0' && (tmp[1] == 'x' || tmp[1] == 'X'))
+    tmp += 2;
+
+  ret = g_ascii_strtoull (tmp, &endptr, 16);
+
+  if (endptr == NULL || *endptr != '\0' || ret > G_MAXUINT32)
+    return FALSE;
+
+  if (out != NULL)
+    *out = (guint32) ret;
+
+  return TRUE;
+}
+
+static SrtInputDeviceTypeFlags
+srt_udev_input_device_get_type_flags (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  return self->type_flags;
+}
+
+static SrtInputDeviceInterfaceFlags
+srt_udev_input_device_get_interface_flags (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  return self->iface_flags;
+}
+
+static const char *
+srt_udev_input_device_get_dev_node (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  return symbols.udev_device_get_devnode (self->dev);
+}
+
+static const char *
+srt_udev_input_device_get_subsystem (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  return symbols.udev_device_get_subsystem (self->dev);
+}
+
+static const char *
+srt_udev_input_device_get_sys_path (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  return symbols.udev_device_get_syspath (self->dev);
+}
+
+static gchar **
+srt_udev_input_device_dup_udev_properties (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+  struct udev_list_entry *entries;
+  struct udev_list_entry *entry;
+  g_autoptr(GPtrArray) arr = g_ptr_array_new_with_free_func (g_free);
+
+  entries = symbols.udev_device_get_properties_list_entry (self->dev);
+
+  for (entry = entries;
+       entry != NULL;
+       entry = symbols.udev_list_entry_get_next (entry))
+    {
+      const char *name = symbols.udev_list_entry_get_name (entry);
+      const char *value = symbols.udev_device_get_property_value (self->dev, name);
+
+      g_ptr_array_add (arr, g_strdup_printf ("%s=%s", name, value));
+    }
+
+  g_ptr_array_add (arr, NULL);
+  return (gchar **) g_ptr_array_free (g_steal_pointer (&arr), FALSE);
+}
+
+static gchar *
+srt_udev_input_device_dup_uevent (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  return g_strdup (symbols.udev_device_get_sysattr_value (self->dev, "uevent"));
+}
+
+static const SrtEvdevCapabilities *
+srt_udev_input_device_peek_event_capabilities (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  return &self->evdev_caps;
+}
+
+static const char *
+srt_udev_input_device_get_hid_sys_path (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  if (self->hid_ancestor.dev == NULL)
+    return NULL;
+
+  return symbols.udev_device_get_syspath (self->hid_ancestor.dev);
+}
+
+static gboolean
+srt_udev_input_device_get_hid_identity (SrtInputDevice *device,
+                                        unsigned int *bus_type,
+                                        unsigned int *vendor_id,
+                                        unsigned int *product_id,
+                                        const char **name,
+                                        const char **phys,
+                                        const char **uniq)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  if (self->hid_ancestor.dev == NULL
+      && !has_all_flags (self->iface_flags,
+                         SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID
+                         | SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE))
+    return FALSE;
+
+  if (bus_type != NULL)
+    *bus_type = self->hid_ancestor.bus_type;
+
+  if (vendor_id != NULL)
+    *vendor_id = self->hid_ancestor.vendor_id;
+
+  if (product_id != NULL)
+    *product_id = self->hid_ancestor.product_id;
+
+  if (name != NULL
+      && !get_string (self->hid_ancestor.dev, "name", name))
+    *name = NULL;
+
+  if (phys != NULL
+      && !get_string (self->hid_ancestor.dev, "phys", phys))
+    *phys = NULL;
+
+  if (uniq != NULL
+      && !get_string (self->hid_ancestor.dev, "uniq", uniq))
+    *uniq = NULL;
+
+  return TRUE;
+}
+
+static const char *
+srt_udev_input_device_get_input_sys_path (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  if (self->input_ancestor.dev == NULL)
+    return NULL;
+
+  return symbols.udev_device_get_syspath (self->input_ancestor.dev);
+}
+
+static gboolean
+srt_udev_input_device_get_input_identity (SrtInputDevice *device,
+                                          unsigned int *bus_type,
+                                          unsigned int *vendor_id,
+                                          unsigned int *product_id,
+                                          unsigned int *version,
+                                          const char **name,
+                                          const char **phys,
+                                          const char **uniq)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  if (self->input_ancestor.dev == NULL
+      && !has_all_flags (self->iface_flags,
+                         SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT
+                         | SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE))
+    return FALSE;
+
+  if (bus_type != NULL)
+    *bus_type = self->input_ancestor.bus_type;
+
+  if (vendor_id != NULL)
+    *vendor_id = self->input_ancestor.vendor_id;
+
+  if (product_id != NULL)
+    *product_id = self->input_ancestor.product_id;
+
+  if (version != NULL)
+    *version = self->input_ancestor.version;
+
+  if (name != NULL
+      && !get_string (self->input_ancestor.dev, "name", name))
+    *name = NULL;
+
+  if (phys != NULL
+      && !get_string (self->input_ancestor.dev, "phys", phys))
+    *phys = NULL;
+
+  if (uniq != NULL
+      && !get_string (self->input_ancestor.dev, "uniq", uniq))
+    *uniq = NULL;
+
+  return TRUE;
+}
+
+static const char *
+srt_udev_input_device_get_usb_device_sys_path (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  if (self->usb_device_ancestor.dev == NULL)
+    return NULL;
+
+  return symbols.udev_device_get_syspath (self->usb_device_ancestor.dev);
+}
+
+static gboolean
+srt_udev_input_device_get_usb_device_identity (SrtInputDevice *device,
+                                               unsigned int *vendor_id,
+                                               unsigned int *product_id,
+                                               unsigned int *device_version,
+                                               const char **manufacturer,
+                                               const char **product,
+                                               const char **serial)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  if (self->usb_device_ancestor.dev == NULL)
+    return FALSE;
+
+  if (vendor_id != NULL)
+    *vendor_id = self->usb_device_ancestor.vendor_id;
+
+  if (product_id != NULL)
+    *product_id = self->usb_device_ancestor.product_id;
+
+  if (device_version != NULL)
+    *device_version = self->usb_device_ancestor.device_version;
+
+  if (manufacturer != NULL
+      && !get_string (self->usb_device_ancestor.dev,
+                      "manufacturer", manufacturer))
+    *manufacturer = NULL;
+
+  if (product != NULL
+      && !get_string (self->usb_device_ancestor.dev,
+                      "product", product))
+    *product = NULL;
+
+  if (serial != NULL
+      && !get_string (self->usb_device_ancestor.dev,
+                      "serial", serial))
+    *serial = NULL;
+
+  return TRUE;
+}
+
+static gchar *
+srt_udev_input_device_dup_hid_uevent (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  if (self->hid_ancestor.dev == NULL)
+    return NULL;
+
+  return g_strdup (symbols.udev_device_get_sysattr_value (self->hid_ancestor.dev, "uevent"));
+}
+
+static gchar *
+srt_udev_input_device_dup_input_uevent (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  if (self->input_ancestor.dev == NULL)
+    return NULL;
+
+  return g_strdup (symbols.udev_device_get_sysattr_value (self->input_ancestor.dev, "uevent"));
+}
+
+static gchar *
+srt_udev_input_device_dup_usb_device_uevent (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  if (self->usb_device_ancestor.dev == NULL)
+    return NULL;
+
+  return g_strdup (symbols.udev_device_get_sysattr_value (self->usb_device_ancestor.dev, "uevent"));
+}
+
+static void
+srt_udev_input_device_iface_init (SrtInputDeviceInterface *iface)
+{
+#define IMPLEMENT(x) iface->x = srt_udev_input_device_ ## x
+
+  IMPLEMENT (get_type_flags);
+  IMPLEMENT (get_interface_flags);
+  IMPLEMENT (get_dev_node);
+  IMPLEMENT (get_sys_path);
+  IMPLEMENT (get_subsystem);
+  IMPLEMENT (dup_udev_properties);
+  IMPLEMENT (dup_uevent);
+  IMPLEMENT (peek_event_capabilities);
+
+  IMPLEMENT (get_hid_sys_path);
+  IMPLEMENT (get_input_sys_path);
+  IMPLEMENT (get_usb_device_sys_path);
+
+  IMPLEMENT (get_hid_identity);
+  IMPLEMENT (get_input_identity);
+  IMPLEMENT (get_usb_device_identity);
+
+  IMPLEMENT (dup_hid_uevent);
+  IMPLEMENT (dup_input_uevent);
+  IMPLEMENT (dup_usb_device_uevent);
+
+#undef IMPLEMENT
+}
+
+typedef enum
+{
+  PROP_0,
+  PROP_FLAGS,
+  PROP_IS_ACTIVE,
+  N_PROPERTIES
+} Property;
+
+static void
+_srt_udev_input_device_monitor_init (SrtUdevInputDeviceMonitor *self)
+{
+  self->monitor_context = g_main_context_ref_thread_default ();
+  self->state = NOT_STARTED;
+  self->devices = g_hash_table_new_full (g_str_hash,
+                                         g_str_equal,
+                                         NULL,
+                                         g_object_unref);
+}
+
+static void
+srt_udev_input_device_monitor_get_property (GObject *object,
+                                            guint prop_id,
+                                            GValue *value,
+                                            GParamSpec *pspec)
+{
+  SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (object);
+
+  switch ((Property) prop_id)
+    {
+      case PROP_FLAGS:
+        g_value_set_flags (value, self->flags);
+        break;
+
+      case PROP_IS_ACTIVE:
+        g_value_set_boolean (value, (self->state == STARTED));
+        break;
+
+      case PROP_0:
+      case N_PROPERTIES:
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+srt_udev_input_device_monitor_set_property (GObject *object,
+                                            guint prop_id,
+                                            const GValue *value,
+                                            GParamSpec *pspec)
+{
+  SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (object);
+
+  switch ((Property) prop_id)
+    {
+      case PROP_FLAGS:
+        /* Construct-only */
+        g_return_if_fail (self->flags == 0);
+        self->flags = g_value_get_flags (value);
+        break;
+
+      case PROP_IS_ACTIVE:
+      case PROP_0:
+      case N_PROPERTIES:
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+srt_udev_input_device_monitor_dispose (GObject *object)
+{
+  srt_input_device_monitor_stop (SRT_INPUT_DEVICE_MONITOR (object));
+  G_OBJECT_CLASS (_srt_udev_input_device_monitor_parent_class)->dispose (object);
+}
+
+static void
+srt_udev_input_device_monitor_finalize (GObject *object)
+{
+  srt_input_device_monitor_stop (SRT_INPUT_DEVICE_MONITOR (object));
+  G_OBJECT_CLASS (_srt_udev_input_device_monitor_parent_class)->finalize (object);
+}
+
+static void
+_srt_udev_input_device_monitor_class_init (SrtUdevInputDeviceMonitorClass *cls)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (cls);
+
+  object_class->get_property = srt_udev_input_device_monitor_get_property;
+  object_class->set_property = srt_udev_input_device_monitor_set_property;
+  object_class->dispose = srt_udev_input_device_monitor_dispose;
+  object_class->finalize = srt_udev_input_device_monitor_finalize;
+
+  g_object_class_override_property (object_class, PROP_FLAGS, "flags");
+  g_object_class_override_property (object_class, PROP_IS_ACTIVE, "is-active");
+}
+
+static gboolean
+srt_udev_input_device_monitor_initable_init (GInitable *initable,
+                                             GCancellable *cancellable G_GNUC_UNUSED,
+                                             GError **error)
+{
+  static const char * const libs[] = { "libudev.so.1", "libudev.so.0" };
+  void *handle = NULL;
+  const char *lib = NULL;
+  const char *which = NULL;
+  gsize i;
+
+  /* Only initialize once per process */
+  if (symbols.handle != NULL)
+    return TRUE;
+
+  for (i = 0; i < G_N_ELEMENTS (libs); i++)
+    {
+      lib = libs[i];
+      handle = dlopen (lib, RTLD_NOW|RTLD_LOCAL|RTLD_NODELETE);
+
+      if (handle != NULL)
+        break;
+    }
+
+  if (handle == NULL)
+    {
+      /* Try the first library again, for the side-effect of getting the
+       * error message */
+      lib = libs[0];
+      handle = dlopen (lib, RTLD_NOW|RTLD_LOCAL|RTLD_NODELETE);
+    }
+
+  if (handle == NULL)
+    return glnx_throw (error, "Unable to load %s: %s", lib, dlerror ());
+
+#define SYMBOL(name) \
+  do \
+    { \
+      void *symbol; \
+      \
+      which = #name; \
+      symbol = dlsym (handle, which); \
+      \
+      if (symbol != NULL) \
+        symbols.name = (__typeof__ (symbols.name)) G_CALLBACK (symbol); \
+      else \
+        goto fail; \
+    } \
+  while (0)
+
+  SYMBOL (udev_new);
+  SYMBOL (udev_unref);
+
+  SYMBOL (udev_list_entry_get_name);
+  SYMBOL (udev_list_entry_get_next);
+
+  SYMBOL (udev_device_new_from_syspath);
+  SYMBOL (udev_device_get_action);
+  SYMBOL (udev_device_get_devnode);
+  SYMBOL (udev_device_get_subsystem);
+  SYMBOL (udev_device_get_syspath);
+  SYMBOL (udev_device_get_properties_list_entry);
+  SYMBOL (udev_device_get_property_value);
+  SYMBOL (udev_device_get_sysattr_value);
+  SYMBOL (udev_device_get_parent_with_subsystem_devtype);
+  SYMBOL (udev_device_ref);
+  SYMBOL (udev_device_unref);
+
+  SYMBOL (udev_monitor_new_from_netlink);
+  SYMBOL (udev_monitor_filter_add_match_subsystem_devtype);
+  SYMBOL (udev_monitor_enable_receiving);
+  SYMBOL (udev_monitor_receive_device);
+  SYMBOL (udev_monitor_get_fd);
+  SYMBOL (udev_monitor_unref);
+
+  SYMBOL (udev_enumerate_new);
+  SYMBOL (udev_enumerate_add_match_subsystem);
+  SYMBOL (udev_enumerate_scan_devices);
+  SYMBOL (udev_enumerate_get_list_entry);
+  SYMBOL (udev_enumerate_unref);
+
+#undef SYMBOL
+
+  symbols.handle = handle;
+  return TRUE;
+
+fail:
+  return glnx_throw (error, "Unable to find symbol %s in %s: %s",
+                     which, lib, dlerror ());
+}
+
+static void
+read_hid_ancestor (SrtUdevInputDevice *device)
+{
+  const char *uevent;
+
+  if (device->hid_ancestor.dev == NULL)
+    return;
+
+  uevent = symbols.udev_device_get_sysattr_value (device->hid_ancestor.dev,
+                                                  "uevent");
+  _srt_get_identity_from_hid_uevent (uevent,
+                                     &device->hid_ancestor.bus_type,
+                                     &device->hid_ancestor.vendor_id,
+                                     &device->hid_ancestor.product_id,
+                                     &device->hid_ancestor.name,
+                                     &device->hid_ancestor.phys,
+                                     &device->hid_ancestor.uniq);
+}
+
+static void
+read_input_ancestor (SrtUdevInputDevice *device)
+{
+  if (device->input_ancestor.dev == NULL)
+    return;
+
+  get_uint32_hex (device->input_ancestor.dev, "id/bustype",
+                  &device->input_ancestor.bus_type);
+  get_uint32_hex (device->input_ancestor.dev, "id/vendor",
+                  &device->input_ancestor.vendor_id);
+  get_uint32_hex (device->input_ancestor.dev, "id/product",
+                  &device->input_ancestor.product_id);
+  get_uint32_hex (device->input_ancestor.dev, "id/version",
+                  &device->input_ancestor.version);
+
+  get_evdev_caps_from_udev (device->input_ancestor.dev,
+                            "capabilities/ev",
+                            device->evdev_caps.ev,
+                            G_N_ELEMENTS (device->evdev_caps.ev));
+  get_evdev_caps_from_udev (device->input_ancestor.dev,
+                            "capabilities/abs",
+                            device->evdev_caps.abs,
+                            G_N_ELEMENTS (device->evdev_caps.abs));
+  get_evdev_caps_from_udev (device->input_ancestor.dev,
+                            "capabilities/rel",
+                            device->evdev_caps.rel,
+                            G_N_ELEMENTS (device->evdev_caps.rel));
+  get_evdev_caps_from_udev (device->input_ancestor.dev,
+                            "capabilities/key",
+                            device->evdev_caps.keys,
+                            G_N_ELEMENTS (device->evdev_caps.keys));
+  get_evdev_caps_from_udev (device->input_ancestor.dev,
+                            "capabilities/ff",
+                            device->evdev_caps.ff,
+                            G_N_ELEMENTS (device->evdev_caps.ff));
+
+  /* These are not really capabilities as such, but the encoding is
+   * the same */
+  get_evdev_caps_from_udev (device->input_ancestor.dev,
+                            "properties",
+                            device->evdev_caps.props,
+                            G_N_ELEMENTS (device->evdev_caps.props));
+}
+
+static void
+read_usb_device_ancestor (SrtUdevInputDevice *device)
+{
+  if (device->usb_device_ancestor.dev == NULL)
+    return;
+
+  get_uint32_hex (device->usb_device_ancestor.dev, "idVendor",
+                  &device->usb_device_ancestor.vendor_id);
+  get_uint32_hex (device->usb_device_ancestor.dev, "idProduct",
+                  &device->usb_device_ancestor.product_id);
+  get_uint32_hex (device->usb_device_ancestor.dev, "bcdDevice",
+                  &device->usb_device_ancestor.device_version);
+}
+
+static gboolean
+get_boolean_property (struct udev_device *dev,
+                      const char *name,
+                      gboolean def)
+{
+  const char *value = symbols.udev_device_get_property_value (dev, name);
+
+  if (value == NULL)
+    return def;
+
+  if (g_strcmp0 (value, "1") == 0)
+    return TRUE;
+
+  if (g_strcmp0 (value, "0") == 0)
+    return FALSE;
+
+  g_debug ("Boolean udev property %s has unexpected value %s",
+           name, value);
+  return def;
+}
+
+static void
+add_device (SrtUdevInputDeviceMonitor *self,
+            struct udev_device *dev)
+{
+  SrtUdevInputDevice *device = NULL;
+  const char *syspath;
+  const char *devnode;
+  gboolean want = FALSE;
+  gboolean is_evdev = FALSE;
+  gboolean is_hidraw = FALSE;
+  const char *slash;
+  int fd;
+
+  syspath = symbols.udev_device_get_syspath (dev);
+  devnode = symbols.udev_device_get_devnode (dev);
+
+  if (G_UNLIKELY (syspath == NULL))
+    {
+      g_warning ("not adding udev_device with NULL syspath");
+      return;
+    }
+
+  /* If we added a device at this /sys path already, don't add it again.
+   * (This is for the "change" action, and the rarer "bind" action.) */
+  if (g_hash_table_lookup_extended (self->devices, syspath, NULL, NULL))
+    {
+      g_debug ("ignoring device \"%s\" which we already have", syspath);
+      return;
+    }
+
+  if (devnode == NULL)
+    {
+      /* We only care about the devices that could, in principle, be
+       * opened */
+      g_debug ("ignoring device \"%s\" with NULL device node", syspath);
+      return;
+    }
+
+  slash = strrchr (syspath, '/');
+
+  if (slash != NULL)
+    {
+      if (self->want_hidraw &&
+          g_str_has_prefix (slash + 1, "hidraw") &&
+          _srt_str_is_integer (slash + 1 + strlen ("hidraw")))
+        want = is_hidraw = TRUE;
+
+      if (self->want_evdev &&
+          g_str_has_prefix (slash + 1, "event") &&
+          _srt_str_is_integer (slash + 1 + strlen ("event")))
+        want = is_evdev = TRUE;
+    }
+
+  if (!want)
+    {
+      g_debug ("ignoring uninteresting udev_device \"%s\"", syspath);
+      return;
+    }
+
+  g_debug ("Adding device %s", syspath);
+
+  device = g_object_new (SRT_TYPE_UDEV_INPUT_DEVICE, NULL);
+  device->dev = symbols.udev_device_ref (dev);
+
+  if (is_evdev)
+    device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT;
+
+  if (is_hidraw)
+    device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID;
+
+  fd = open (devnode, O_RDONLY | O_NONBLOCK | _SRT_INPUT_DEVICE_ALWAYS_OPEN_FLAGS);
+
+  if (fd >= 0)
+    {
+      device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE;
+
+      if (_srt_get_identity_from_raw_hid (fd,
+                                          &device->hid_ancestor.bus_type,
+                                          &device->hid_ancestor.vendor_id,
+                                          &device->hid_ancestor.product_id,
+                                          NULL,
+                                          NULL,
+                                          NULL))
+        {
+          g_debug ("%s is raw HID: bus type 0x%04x, vendor 0x%04x, product 0x%04x",
+                   devnode,
+                   device->hid_ancestor.bus_type,
+                   device->hid_ancestor.vendor_id,
+                   device->hid_ancestor.product_id);
+          device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID;
+        }
+
+      if (_srt_get_identity_from_evdev (fd,
+                                        &device->input_ancestor.bus_type,
+                                        &device->input_ancestor.vendor_id,
+                                        &device->input_ancestor.product_id,
+                                        &device->input_ancestor.version,
+                                        NULL,
+                                        NULL,
+                                        NULL))
+        {
+          g_debug ("%s is evdev: bus type 0x%04x, vendor 0x%04x, product 0x%04x, version 0x%04x",
+                   devnode,
+                   device->input_ancestor.bus_type,
+                   device->input_ancestor.vendor_id,
+                   device->input_ancestor.product_id,
+                   device->input_ancestor.version);
+          device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT;
+        }
+
+      _srt_evdev_capabilities_set_from_evdev (&device->evdev_caps, fd);
+      close (fd);
+    }
+
+  fd = open (devnode, O_RDWR | O_NONBLOCK | _SRT_INPUT_DEVICE_ALWAYS_OPEN_FLAGS);
+
+  if (fd >= 0)
+    {
+      device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_READ_WRITE;
+      close (fd);
+    }
+
+  device->hid_ancestor.dev = symbols.udev_device_get_parent_with_subsystem_devtype (device->dev,
+                                                                                    "hid",
+                                                                                    NULL);
+  read_hid_ancestor (device);
+  device->input_ancestor.dev = find_input_ancestor (device->dev);
+  read_input_ancestor (device);
+
+  if (device->hid_ancestor.bus_type == BUS_USB
+      || device->input_ancestor.bus_type == BUS_USB)
+    {
+      device->usb_device_ancestor.dev = symbols.udev_device_get_parent_with_subsystem_devtype (device->dev,
+                                                                                               "usb",
+                                                                                               "usb_device");
+      read_usb_device_ancestor (device);
+    }
+
+  if (get_boolean_property (device->dev, "ID_INPUT_JOYSTICK", FALSE))
+    device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK;
+
+  if (get_boolean_property (device->dev, "ID_INPUT_ACCELEROMETER", FALSE))
+    device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_ACCELEROMETER;
+
+  if (get_boolean_property (device->dev, "ID_INPUT_KEYBOARD", FALSE))
+    device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_KEYBOARD;
+
+  if (get_boolean_property (device->dev, "ID_INPUT_KEY", FALSE))
+    device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS;
+
+  if (get_boolean_property (device->dev, "ID_INPUT_MOUSE", FALSE))
+    device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_MOUSE;
+
+  if (get_boolean_property (device->dev, "ID_INPUT_TOUCHPAD", FALSE))
+    device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_TOUCHPAD;
+
+  if (get_boolean_property (device->dev, "ID_INPUT_TOUCHSCREEN", FALSE))
+    device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_TOUCHSCREEN;
+
+  if (get_boolean_property (device->dev, "ID_INPUT_TABLET", FALSE))
+    device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_TABLET;
+
+  if (get_boolean_property (device->dev, "ID_INPUT_TABLET_PAD", FALSE))
+    device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_TABLET_PAD;
+
+  if (get_boolean_property (device->dev, "ID_INPUT_POINTING_STICK", FALSE))
+    device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_POINTING_STICK;
+
+  if (get_boolean_property (device->dev, "ID_INPUT_SWITCH", FALSE))
+    device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_SWITCH;
+
+  if (device->type_flags == SRT_INPUT_DEVICE_TYPE_FLAGS_NONE)
+    {
+      /* Older versions of udev put classes on input devices */
+      const char *value = symbols.udev_device_get_property_value (device->dev,
+                                                                  "ID_CLASS");
+
+      if (g_strcmp0 (value, "joystick") == 0)
+        device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK;
+      else if (g_strcmp0 (value, "mouse") == 0)
+        device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_MOUSE;
+      else if (g_strcmp0 (value, "kbd") == 0)
+        device->type_flags |= SRT_INPUT_DEVICE_TYPE_FLAGS_KEYBOARD;
+    }
+
+  if (device->type_flags == SRT_INPUT_DEVICE_TYPE_FLAGS_NONE
+      && !get_boolean_property (device->dev, "ID_INPUT", FALSE))
+    {
+      g_debug ("input_id builtin did not run, guessing device type...");
+      device->type_flags = _srt_evdev_capabilities_guess_type (&device->evdev_caps);
+    }
+
+  g_hash_table_replace (self->devices, (char *) syspath, device);
+  _srt_input_device_monitor_emit_added (SRT_INPUT_DEVICE_MONITOR (self),
+                                        SRT_INPUT_DEVICE (device));
+}
+
+static void
+remove_device (SrtUdevInputDeviceMonitor *self,
+               struct udev_device *dev)
+{
+  void *device = NULL;
+  const char *syspath;
+
+  syspath = symbols.udev_device_get_syspath (dev);
+
+  if (G_UNLIKELY (syspath == NULL))
+    {
+      g_warning ("trying to remove udev_device with NULL syspath");
+      symbols.udev_device_unref (dev);
+      return;
+    }
+
+  g_debug ("Removing device %s", syspath);
+
+  if (g_hash_table_lookup_extended (self->devices, syspath, NULL, &device))
+    {
+      g_hash_table_steal (self->devices, syspath);
+      _srt_input_device_monitor_emit_removed (SRT_INPUT_DEVICE_MONITOR (self),
+                                              device);
+      g_object_unref (device);
+    }
+}
+
+static gboolean
+srt_udev_input_device_monitor_cb (int fd,
+                                  GIOCondition condition,
+                                  void *user_data)
+{
+  SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (user_data);
+  struct udev_device *dev;
+  const char *action;
+
+  g_return_val_if_fail (SRT_IS_UDEV_INPUT_DEVICE_MONITOR (self), G_SOURCE_REMOVE);
+
+  dev = symbols.udev_monitor_receive_device (self->monitor);
+
+  if (dev == NULL)
+    return G_SOURCE_CONTINUE;
+
+  action = symbols.udev_device_get_action (dev);
+
+  if (g_strcmp0 (action, "remove") == 0)
+    remove_device (self, dev);
+  else
+    add_device (self, dev);
+
+  g_clear_pointer (&dev, symbols.udev_device_unref);
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+srt_udev_input_device_monitor_request_raw_hid (SrtInputDeviceMonitor *monitor)
+{
+  SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (monitor);
+
+  g_return_if_fail (SRT_IS_UDEV_INPUT_DEVICE_MONITOR (monitor));
+  g_return_if_fail (self->state == NOT_STARTED);
+
+  self->want_hidraw = TRUE;
+}
+
+static void
+srt_udev_input_device_monitor_request_evdev (SrtInputDeviceMonitor *monitor)
+{
+  SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (monitor);
+
+  g_return_if_fail (SRT_IS_UDEV_INPUT_DEVICE_MONITOR (monitor));
+  g_return_if_fail (self->state == NOT_STARTED);
+
+  self->want_evdev = TRUE;
+}
+
+static gboolean
+enumerate_cb (gpointer user_data)
+{
+  SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (user_data);
+
+  if (self->want_hidraw || self->want_evdev)
+    {
+      struct udev_enumerate *enumerator = NULL;
+      struct udev_list_entry *devs;
+      struct udev_list_entry *item;
+
+      enumerator = symbols.udev_enumerate_new (self->context);
+
+      if (enumerator == NULL)
+        {
+          g_warning ("udev_enumerate_new: %s", g_strerror (errno));
+          goto out;
+        }
+
+      if (self->want_evdev)
+        symbols.udev_enumerate_add_match_subsystem (enumerator, "input");
+
+      if (self->want_hidraw)
+        symbols.udev_enumerate_add_match_subsystem (enumerator, "hidraw");
+
+      symbols.udev_enumerate_scan_devices (enumerator);
+      devs = symbols.udev_enumerate_get_list_entry (enumerator);
+
+      for (item = devs; item != NULL; item = symbols.udev_list_entry_get_next (item))
+        {
+          const char *syspath = symbols.udev_list_entry_get_name (item);
+          struct udev_device *dev;
+
+          dev = symbols.udev_device_new_from_syspath (self->context, syspath);
+
+          if (dev != NULL)
+            add_device (self, dev);
+          else
+            g_warning ("udev_device_new_from_syspath \"%s\": %s",
+                       syspath, g_strerror (errno));
+
+          symbols.udev_device_unref (dev);
+        }
+
+      symbols.udev_enumerate_unref (enumerator);
+    }
+
+out:
+  _srt_input_device_monitor_emit_all_for_now (SRT_INPUT_DEVICE_MONITOR (self));
+
+  return G_SOURCE_REMOVE;
+}
+
+static gboolean
+srt_udev_input_device_monitor_start (SrtInputDeviceMonitor *monitor,
+                                     GError **error)
+{
+  SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (monitor);
+
+  g_return_val_if_fail (SRT_IS_UDEV_INPUT_DEVICE_MONITOR (monitor), FALSE);
+  g_return_val_if_fail (self->state == NOT_STARTED, FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+  /* If this assertion fails, g_initable_init() failed or wasn't called */
+  g_return_val_if_fail (symbols.handle != NULL, FALSE);
+
+  self->state = STARTED;
+
+  self->context = symbols.udev_new ();
+
+  if (self->context == NULL)
+    return glnx_throw_errno_prefix (error, "udev_new");
+
+  if ((self->flags & SRT_INPUT_DEVICE_MONITOR_FLAGS_ONCE) == 0
+      && (self->want_hidraw || self->want_evdev))
+    {
+      int fd;
+
+      self->monitor = symbols.udev_monitor_new_from_netlink (self->context, "udev");
+
+      if (self->monitor == NULL)
+        return glnx_throw_errno_prefix (error, "udev_monitor_new_from_netlink");
+
+      if (self->want_evdev)
+        symbols.udev_monitor_filter_add_match_subsystem_devtype (self->monitor, "input", NULL);
+
+      if (self->want_hidraw)
+        symbols.udev_monitor_filter_add_match_subsystem_devtype (self->monitor, "hidraw", NULL);
+
+      symbols.udev_monitor_enable_receiving (self->monitor);
+
+      fd = symbols.udev_monitor_get_fd (self->monitor);
+
+      if (fd < 0)
+        {
+          errno = -fd;
+          return glnx_throw_errno_prefix (error, "udev_monitor_get_fd");
+        }
+
+      self->monitor_source = g_unix_fd_source_new (fd, G_IO_IN);
+      g_source_set_callback (self->monitor_source,
+                             (GSourceFunc) G_CALLBACK (srt_udev_input_device_monitor_cb),
+                             self, NULL);
+      g_source_set_priority (self->monitor_source, G_PRIORITY_DEFAULT);
+      g_source_attach (self->monitor_source, self->monitor_context);
+    }
+
+  /* Make sure the signals for the initial batch of devices are emitted in
+   * the correct main-context */
+  g_main_context_invoke_full (self->monitor_context, G_PRIORITY_DEFAULT,
+                              enumerate_cb, g_object_ref (self),
+                              g_object_unref);
+
+  return TRUE;
+}
+
+static void
+srt_udev_input_device_monitor_stop (SrtInputDeviceMonitor *monitor)
+{
+  SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (monitor);
+
+  self->state = STOPPED;
+
+  if (self->monitor_source != NULL)
+    g_source_destroy (self->monitor_source);
+
+  g_clear_pointer (&self->monitor_source, g_source_unref);
+  g_clear_pointer (&self->monitor_context, g_main_context_unref);
+  g_clear_pointer (&self->monitor, symbols.udev_monitor_unref);
+  g_clear_pointer (&self->context, symbols.udev_unref);
+  g_clear_pointer (&self->devices, g_hash_table_unref);
+}
+
+static void
+srt_udev_input_device_monitor_iface_init (SrtInputDeviceMonitorInterface *iface)
+{
+#define IMPLEMENT(x) iface->x = srt_udev_input_device_monitor_ ## x
+  IMPLEMENT (request_raw_hid);
+  IMPLEMENT (request_evdev);
+  IMPLEMENT (start);
+  IMPLEMENT (stop);
+#undef IMPLEMENT
+}
+
+static void
+srt_udev_input_device_monitor_initable_iface_init (GInitableIface *iface)
+{
+  iface->init = srt_udev_input_device_monitor_initable_init;
+}
diff --git a/steam-runtime-tools/utils-internal.h b/steam-runtime-tools/utils-internal.h
index 3a7f0fba5871441d952de90dd25b7b23d188cc4d..7e06650ce58a47fbef63880cb3268263afc1df48 100644
--- a/steam-runtime-tools/utils-internal.h
+++ b/steam-runtime-tools/utils-internal.h
@@ -101,3 +101,5 @@ gboolean _srt_file_test_in_sysroot (const char *sysroot,
 G_GNUC_INTERNAL const char * const *_srt_peek_environ_nonnull (void);
 
 G_GNUC_INTERNAL void _srt_setenv_disable_gio_modules (void);
+
+G_GNUC_INTERNAL gboolean _srt_str_is_integer (const char *str);
diff --git a/steam-runtime-tools/utils.c b/steam-runtime-tools/utils.c
index 074a8ad3b19daec307893830f3e7938f097554f2..5601a1778b291ea0d469f3629d41ba793e031c47 100644
--- a/steam-runtime-tools/utils.c
+++ b/steam-runtime-tools/utils.c
@@ -961,3 +961,28 @@ _srt_setenv_disable_gio_modules (void)
   g_setenv ("GIO_USE_VFS", "local", TRUE);
   g_setenv ("GIO_MODULE_DIR", "/nonexistent", TRUE);
 }
+
+/*
+ * Return TRUE if @str points to one or more decimal digits, followed
+ * by nul termination. This is the same as Python bytes.isdigit().
+ */
+gboolean
+_srt_str_is_integer (const char *str)
+{
+  const char *p;
+
+  g_return_val_if_fail (str != NULL, FALSE);
+
+  if (*str == '\0')
+    return FALSE;
+
+  for (p = str;
+       *p != '\0';
+       p++)
+    {
+      if (!g_ascii_isdigit (*p))
+        break;
+    }
+
+  return (*p == '\0');
+}
diff --git a/sysroot/run-in-sysroot.py b/sysroot/run-in-sysroot.py
index b22b927c63e6f3b4f94a1eebaf56e5edaea4aaa2..73d07ceebda0e76b45a750b95ebb565447089ffe 100755
--- a/sysroot/run-in-sysroot.py
+++ b/sysroot/run-in-sysroot.py
@@ -110,6 +110,8 @@ def main():
         '--tmpfs', '/tmp',
         '--tmpfs', '/var/tmp',
         '--tmpfs', '/home',
+        '--tmpfs', '/run',
+        '--tmpfs', '/run/host',
         '--bind', abs_srcdir, abs_srcdir,
         '--bind', abs_builddir, abs_builddir,
     ]
diff --git a/tests/input-device.c b/tests/input-device.c
new file mode 100644
index 0000000000000000000000000000000000000000..659faeecb5f40cb5f1d0e227ec6bb0faf4acc3ee
--- /dev/null
+++ b/tests/input-device.c
@@ -0,0 +1,2541 @@
+/*
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include <libglnx.h>
+
+#include <steam-runtime-tools/steam-runtime-tools.h>
+
+#include <linux/input.h>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <glib-object.h>
+
+#include "steam-runtime-tools/json-glib-backports-internal.h"
+#include "steam-runtime-tools/utils-internal.h"
+#include "mock-input-device.h"
+#include "test-utils.h"
+
+static const char *argv0;
+
+typedef struct
+{
+  enum { MOCK, DIRECT, UDEV } type;
+} Config;
+
+static const Config defconfig =
+{
+  .type = MOCK,
+};
+
+static const Config direct_config =
+{
+  .type = DIRECT,
+};
+
+static const Config udev_config =
+{
+  .type = UDEV,
+};
+
+typedef struct
+{
+  const Config *config;
+  gchar *srcdir;
+  gchar *builddir;
+  GMainContext *monitor_context;
+  GPtrArray *log;
+  gboolean skipped;
+} Fixture;
+
+static void
+setup (Fixture *f,
+       gconstpointer context)
+{
+  const Config *config = context;
+  GStatBuf sb;
+
+  f->srcdir = g_strdup (g_getenv ("G_TEST_SRCDIR"));
+  f->builddir = g_strdup (g_getenv ("G_TEST_BUILDDIR"));
+
+  if (f->srcdir == NULL)
+    f->srcdir = g_path_get_dirname (argv0);
+
+  if (f->builddir == NULL)
+    f->builddir = g_path_get_dirname (argv0);
+
+  if (config == NULL)
+    f->config = &defconfig;
+  else
+    f->config = config;
+
+  if (f->config->type == DIRECT && g_stat ("/dev/input", &sb) != 0)
+    {
+      g_test_skip ("/dev/input not available");
+      f->skipped = TRUE;
+    }
+
+  f->log = g_ptr_array_new_with_free_func (g_free);
+}
+
+static void
+teardown (Fixture *f,
+          gconstpointer context)
+{
+  G_GNUC_UNUSED const Config *config = context;
+
+  g_free (f->srcdir);
+  g_free (f->builddir);
+
+  g_clear_pointer (&f->log, g_ptr_array_unref);
+  g_clear_pointer (&f->monitor_context, g_main_context_unref);
+}
+
+#define VENDOR_VALVE 0x28de
+#define PRODUCT_VALVE_STEAM_CONTROLLER 0x1142
+
+static SrtSimpleInputDevice *
+load_json (Fixture *f,
+           const char *filename)
+{
+  g_autoptr(SrtSimpleInputDevice) simple = NULL;
+  g_autoptr(JsonParser) parser = NULL;
+  g_autoptr(GError) error = NULL;
+  JsonNode *node;
+  JsonObject *object;
+  JsonObject *added;
+  g_autofree gchar *path = NULL;
+
+  parser = json_parser_new ();
+
+  path = g_build_filename (f->srcdir, "input-monitor-outputs",
+                           filename, NULL);
+  json_parser_load_from_file (parser, path, &error);
+  g_assert_no_error (error);
+  node = json_parser_get_root (parser);
+  g_assert_nonnull (node);
+  object = json_node_get_object (node);
+  g_assert_nonnull (object);
+  added = json_object_get_object_member (object, "added");
+  g_assert_nonnull (added);
+  simple = _srt_simple_input_device_new_from_json (added);
+  g_assert_nonnull (simple);
+
+  return g_steal_pointer (&simple);
+}
+
+static void
+test_input_device_from_json_no_details (Fixture *f)
+{
+  g_autoptr(SrtSimpleInputDevice) simple = load_json (f, "no-details.json");
+  SrtInputDevice *dev = SRT_INPUT_DEVICE (simple);
+  struct
+  {
+    unsigned int bus_type;
+    unsigned int vendor_id;
+    unsigned int product_id;
+    unsigned int version;
+  } identity;
+  unsigned long bits[LONGS_FOR_BITS (HIGHEST_EVENT_CODE)];
+  g_autofree gchar *uevent = NULL;
+
+  g_assert_cmphex (srt_input_device_get_interface_flags (dev),
+                   ==, SRT_INPUT_DEVICE_INTERFACE_FLAGS_NONE);
+  g_assert_cmphex (srt_input_device_get_type_flags (dev),
+                   ==, SRT_INPUT_DEVICE_TYPE_FLAGS_NONE);
+  g_assert_cmpstr (srt_input_device_get_dev_node (dev), ==, NULL);
+  g_assert_cmpstr (srt_input_device_get_subsystem (dev), ==, NULL);
+  g_assert_cmpstr (srt_input_device_get_sys_path (dev), ==, NULL);
+
+  g_assert_false (srt_input_device_get_identity (dev,
+                                                 NULL, NULL, NULL, NULL));
+  g_assert_false (srt_input_device_get_identity (dev,
+                                                 &identity.bus_type,
+                                                 &identity.vendor_id,
+                                                 &identity.product_id,
+                                                 &identity.version));
+
+  g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, 0,
+                                                             bits,
+                                                             G_N_ELEMENTS (bits)),
+                    ==, 1);
+  g_assert_cmphex (bits[0], ==, 0);
+  g_assert_cmphex (bits[1], ==, 0);
+
+  g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, EV_ABS,
+                                                             bits,
+                                                             G_N_ELEMENTS (bits)),
+                    >=, 1);
+  g_assert_cmphex (bits[0], ==, 0);
+
+  g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, EV_REL,
+                                                             bits,
+                                                             G_N_ELEMENTS (bits)),
+                    >=, 1);
+  g_assert_cmphex (bits[0], ==, 0);
+
+  g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, EV_KEY,
+                                                             bits,
+                                                             G_N_ELEMENTS (bits)),
+                    >=, 1);
+  g_assert_cmphex (bits[0], ==, 0);
+  g_assert_cmpuint (srt_input_device_get_input_properties (dev, bits,
+                                                           G_N_ELEMENTS (bits)),
+                    ==, 1);
+  g_assert_cmphex (bits[0], ==, 0);
+
+  uevent = srt_input_device_dup_uevent (dev);
+  g_assert_cmpstr (uevent, ==, NULL);
+
+  g_assert_cmpstr (srt_input_device_get_hid_sys_path (dev), ==, NULL);
+  g_assert_false (srt_input_device_get_hid_identity (dev,
+                                                     NULL, NULL, NULL,
+                                                     NULL, NULL, NULL));
+  uevent = srt_input_device_dup_hid_uevent (dev);
+  g_assert_cmpstr (uevent, ==, NULL);
+
+  g_assert_cmpstr (srt_input_device_get_input_sys_path (dev), ==, NULL);
+  g_assert_false (srt_input_device_get_input_identity (dev,
+                                                       NULL, NULL, NULL, NULL,
+                                                       NULL, NULL, NULL));
+  uevent = srt_input_device_dup_input_uevent (dev);
+  g_assert_cmpstr (uevent, ==, NULL);
+
+  g_assert_cmpstr (srt_input_device_get_usb_device_sys_path (dev), ==, NULL);
+  g_assert_false (srt_input_device_get_usb_device_identity (dev,
+                                                            NULL, NULL, NULL,
+                                                            NULL, NULL, NULL));
+  uevent = srt_input_device_dup_usb_device_uevent (dev);
+  g_assert_cmpstr (uevent, ==, NULL);
+}
+
+static void
+test_input_device_from_json_odd (Fixture *f)
+{
+  g_autoptr(SrtSimpleInputDevice) simple = load_json (f, "odd.json");
+  SrtInputDevice *dev = SRT_INPUT_DEVICE (simple);
+  struct
+  {
+    unsigned int bus_type;
+    unsigned int vendor_id;
+    unsigned int product_id;
+    const char *name;
+    const char *phys;
+    const char *uniq;
+  } hid_identity;
+  struct
+  {
+    unsigned int bus_type;
+    unsigned int vendor_id;
+    unsigned int product_id;
+    unsigned int version;
+    const char *name;
+    const char *phys;
+    const char *uniq;
+  } input_identity;
+  struct
+  {
+    unsigned int vendor_id;
+    unsigned int product_id;
+    unsigned int version;
+    const char *manufacturer;
+    const char *product;
+    const char *serial;
+  } usb_identity;
+  unsigned long bits[LONGS_FOR_BITS (HIGHEST_EVENT_CODE)];
+
+  g_assert_cmphex (srt_input_device_get_interface_flags (dev),
+                   ==, SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID);
+  g_assert_cmphex (srt_input_device_get_type_flags (dev),
+                   ==, SRT_INPUT_DEVICE_TYPE_FLAGS_NONE);
+  g_assert_cmpstr (srt_input_device_get_dev_node (dev), ==, NULL);
+  g_assert_cmpstr (srt_input_device_get_subsystem (dev), ==, NULL);
+  g_assert_cmpstr (srt_input_device_get_sys_path (dev), ==, NULL);
+
+  g_assert_false (srt_input_device_get_identity (dev,
+                                                 NULL, NULL, NULL, NULL));
+
+  g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, 0,
+                                                             bits,
+                                                             G_N_ELEMENTS (bits)),
+                    ==, 1);
+  g_assert_cmphex (bits[0], ==, 0);
+
+  g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, EV_ABS,
+                                                             bits,
+                                                             G_N_ELEMENTS (bits)),
+                    >=, 1);
+#if defined(__LP64__)
+  g_assert_cmphex (bits[0], ==, 0x0807060504030201UL);
+#elif defined(__i386__)
+  g_assert_cmphex (bits[0], ==, 0x04030201UL);
+  g_assert_cmphex (bits[1], ==, 0x08070605UL);
+#endif
+
+  g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, EV_REL,
+                                                             bits,
+                                                             G_N_ELEMENTS (bits)),
+                    >=, 1);
+  g_assert_cmphex (bits[0], ==, 0);
+
+  g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, EV_KEY,
+                                                             bits,
+                                                             G_N_ELEMENTS (bits)),
+                    >=, 1);
+  g_assert_cmphex (bits[0], ==, 0);
+  g_assert_cmpuint (srt_input_device_get_input_properties (dev, bits,
+                                                           G_N_ELEMENTS (bits)),
+                    ==, 1);
+#if defined(__LP64__)
+  g_assert_cmphex (bits[0], ==, 0x2143658778563412UL);
+#elif defined(__i386__)
+  g_assert_cmphex (bits[0], ==, 0x78563412UL);
+#endif
+  g_assert_cmphex (bits[1], ==, 0);
+
+  g_assert_true (srt_input_device_get_hid_identity (dev,
+                                                    NULL, NULL, NULL,
+                                                    NULL, NULL, NULL));
+  g_assert_true (srt_input_device_get_hid_identity (dev,
+                                                    &hid_identity.bus_type,
+                                                    &hid_identity.vendor_id,
+                                                    &hid_identity.product_id,
+                                                    &hid_identity.name,
+                                                    &hid_identity.phys,
+                                                    &hid_identity.uniq));
+  g_assert_cmphex (hid_identity.bus_type, ==, 0xfff1);
+  g_assert_cmphex (hid_identity.vendor_id, ==, 0xfff1);
+  g_assert_cmphex (hid_identity.product_id, ==, 0xfff1);
+  g_assert_cmpstr (hid_identity.name, ==, "Acme Weird Device");
+  g_assert_cmpstr (hid_identity.phys, ==, NULL);
+  g_assert_cmpstr (hid_identity.uniq, ==, "12345678");
+
+  g_assert_true (srt_input_device_get_input_identity (dev,
+                                                      NULL, NULL, NULL, NULL,
+                                                      NULL, NULL, NULL));
+  g_assert_true (srt_input_device_get_input_identity (dev,
+                                                      &input_identity.bus_type,
+                                                      &input_identity.vendor_id,
+                                                      &input_identity.product_id,
+                                                      &input_identity.version,
+                                                      &input_identity.name,
+                                                      &input_identity.phys,
+                                                      &input_identity.uniq));
+  g_assert_cmphex (input_identity.bus_type, ==, 0xfff2);
+  g_assert_cmphex (input_identity.vendor_id, ==, 0xfff2);
+  g_assert_cmphex (input_identity.product_id, ==, 0xfff2);
+  g_assert_cmphex (input_identity.version, ==, 0);
+  g_assert_cmpstr (input_identity.name, ==, NULL);
+  g_assert_cmpstr (input_identity.phys, ==, NULL);
+  g_assert_cmpstr (input_identity.uniq, ==, "1234-5678");
+
+  g_assert_cmpstr (srt_input_device_get_usb_device_sys_path (dev), ==, "/...");
+  g_assert_true (srt_input_device_get_usb_device_identity (dev,
+                                                           NULL, NULL, NULL,
+                                                           NULL, NULL, NULL));
+  g_assert_true (srt_input_device_get_usb_device_identity (dev,
+                                                           &usb_identity.vendor_id,
+                                                           &usb_identity.product_id,
+                                                           &usb_identity.version,
+                                                           &usb_identity.manufacturer,
+                                                           &usb_identity.product,
+                                                           &usb_identity.serial));
+  g_assert_cmphex (usb_identity.vendor_id, ==, 0xfff3);
+  g_assert_cmphex (usb_identity.product_id, ==, 0xfff3);
+  g_assert_cmphex (usb_identity.version, ==, 0);
+  g_assert_cmpstr (usb_identity.manufacturer, ==, NULL);
+  g_assert_cmpstr (usb_identity.product, ==, NULL);
+  g_assert_cmpstr (usb_identity.serial, ==, "12:34:56:78");
+}
+
+static void
+test_input_device_from_json_steam_controller (Fixture *f)
+{
+  g_autoptr(SrtSimpleInputDevice) simple = load_json (f, "steam-controller.json");
+  SrtInputDevice *dev = SRT_INPUT_DEVICE (simple);
+  struct
+  {
+    unsigned int bus_type;
+    unsigned int vendor_id;
+    unsigned int product_id;
+    unsigned int version;
+  } identity;
+  struct
+  {
+    unsigned int bus_type;
+    unsigned int vendor_id;
+    unsigned int product_id;
+    const char *name;
+    const char *phys;
+    const char *uniq;
+  } hid_identity;
+  struct
+  {
+    unsigned int bus_type;
+    unsigned int vendor_id;
+    unsigned int product_id;
+    unsigned int version;
+    const char *name;
+    const char *phys;
+    const char *uniq;
+  } input_identity;
+  struct
+  {
+    unsigned int vendor_id;
+    unsigned int product_id;
+    unsigned int version;
+    const char *manufacturer;
+    const char *product;
+    const char *serial;
+  } usb_identity;
+  unsigned long bits[LONGS_FOR_BITS (HIGHEST_EVENT_CODE)];
+  g_autofree gchar *uevent = NULL;
+
+  g_assert_cmphex (srt_input_device_get_interface_flags (dev),
+                   ==, SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT);
+  g_assert_cmphex (srt_input_device_get_type_flags (dev),
+                   ==, (SRT_INPUT_DEVICE_TYPE_FLAGS_KEYBOARD
+                        | SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS
+                        | SRT_INPUT_DEVICE_TYPE_FLAGS_MOUSE));
+  g_assert_cmpstr (srt_input_device_get_dev_node (dev),
+                   ==, "/dev/input/event20");
+  g_assert_cmpstr (srt_input_device_get_subsystem (dev),
+                   ==, "input");
+  g_assert_cmpstr (srt_input_device_get_sys_path (dev),
+                   ==, "/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.1/1-1.1:1.0/0003:28DE:1142.00DD/input/input308/event20");
+
+  g_assert_true (srt_input_device_get_identity (dev,
+                                                NULL, NULL, NULL, NULL));
+  g_assert_true (srt_input_device_get_identity (dev,
+                                                &identity.bus_type,
+                                                &identity.vendor_id,
+                                                &identity.product_id,
+                                                &identity.version));
+  /* Using magic numbers rather than a #define here so that it's easier
+   * to validate against the JSON */
+  g_assert_cmphex (identity.bus_type, ==, 0x0003);
+  g_assert_cmphex (identity.vendor_id, ==, 0x28de);
+  g_assert_cmphex (identity.product_id, ==, 0x1142);
+  g_assert_cmphex (identity.version, ==, 0x0111);
+
+  g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, 0,
+                                                             bits,
+                                                             G_N_ELEMENTS (bits)),
+                    ==, 1);
+  g_assert_cmphex (bits[0], ==, 0x120017);
+  g_assert_cmphex (bits[1], ==, 0);
+
+  g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, EV_ABS,
+                                                             bits,
+                                                             G_N_ELEMENTS (bits)),
+                    >=, 1);
+  g_assert_cmphex (bits[0], ==, 0);
+
+  g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, EV_REL,
+                                                             bits,
+                                                             G_N_ELEMENTS (bits)),
+                    >=, 1);
+  g_assert_cmphex (bits[0], ==, 0x0903);
+
+  g_assert_cmpuint (srt_input_device_get_event_capabilities (dev, EV_KEY,
+                                                             bits,
+                                                             G_N_ELEMENTS (bits)),
+                    >=, 1);
+#if defined(__LP64__)
+  g_assert_cmphex (bits[0], ==, 0xfffffffffffffffeUL);
+  g_assert_cmphex (bits[1], ==, 0xe080ffdf01cfffffUL);
+  g_assert_cmphex (bits[2], ==, 0);
+  g_assert_cmphex (bits[3], ==, 0);
+  g_assert_cmphex (bits[4], ==, 0x1f0000);
+  g_assert_cmphex (bits[5], ==, 0);
+#elif defined(__i386__)
+  g_assert_cmphex (bits[0], ==, 0xfffffffeUL);
+  g_assert_cmphex (bits[1], ==, 0xffffffffUL);
+  g_assert_cmphex (bits[2], ==, 0x01cfffffUL);
+  g_assert_cmphex (bits[3], ==, 0xe080ffdfUL);
+  g_assert_cmphex (bits[4], ==, 0);
+  g_assert_cmphex (bits[5], ==, 0);
+  g_assert_cmphex (bits[6], ==, 0);
+  g_assert_cmphex (bits[7], ==, 0);
+  g_assert_cmphex (bits[8], ==, 0x1f0000);
+  g_assert_cmphex (bits[9], ==, 0);
+  g_assert_cmphex (bits[10], ==, 0);
+  g_assert_cmphex (bits[11], ==, 0);
+#endif
+  g_assert_cmpuint (srt_input_device_get_input_properties (dev, bits,
+                                                           G_N_ELEMENTS (bits)),
+                    ==, 1);
+  g_assert_cmphex (bits[0], ==, 0);
+
+  uevent = srt_input_device_dup_uevent (dev);
+  g_assert_cmpstr (uevent, ==,
+                   "MAJOR=13\n"
+                   "MINOR=84\n"
+                   "DEVNAME=input/event20\n");
+  g_clear_pointer (&uevent, g_free);
+
+  g_assert_cmpstr (srt_input_device_get_hid_sys_path (dev),
+                   ==, "/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.1/1-1.1:1.0/0003:28DE:1142.00DD");
+  g_assert_true (srt_input_device_get_hid_identity (dev,
+                                                    NULL, NULL, NULL,
+                                                    NULL, NULL, NULL));
+  g_assert_true (srt_input_device_get_hid_identity (dev,
+                                                    &hid_identity.bus_type,
+                                                    &hid_identity.vendor_id,
+                                                    &hid_identity.product_id,
+                                                    &hid_identity.name,
+                                                    &hid_identity.phys,
+                                                    &hid_identity.uniq));
+  g_assert_cmphex (hid_identity.bus_type, ==, 0x0003);
+  g_assert_cmphex (hid_identity.vendor_id, ==, 0x28de);
+  g_assert_cmphex (hid_identity.product_id, ==, 0x1142);
+  g_assert_cmpstr (hid_identity.name, ==, "Valve Software Steam Controller");
+  g_assert_cmpstr (hid_identity.phys, ==, "usb-0000:00:14.0-1.1/input0");
+  g_assert_cmpstr (hid_identity.uniq, ==, "");
+  uevent = srt_input_device_dup_hid_uevent (dev);
+  g_assert_cmpstr (uevent, ==,
+                   "DRIVER=hid-steam\n"
+                   "HID_ID=0003:000028DE:00001142\n"
+                   "HID_NAME=Valve Software Steam Controller\n"
+                   "HID_PHYS=usb-0000:00:14.0-1.1/input0\n"
+                   "HID_UNIQ=\n"
+                   "MODALIAS=hid:b0003g0001v000028DEp00001142\n");
+  g_clear_pointer (&uevent, g_free);
+
+  g_assert_cmpstr (srt_input_device_get_input_sys_path (dev),
+                   ==, "/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.1/1-1.1:1.0/0003:28DE:1142.00DD/input/input308");
+  g_assert_true (srt_input_device_get_input_identity (dev,
+                                                      NULL, NULL, NULL, NULL,
+                                                      NULL, NULL, NULL));
+  g_assert_true (srt_input_device_get_input_identity (dev,
+                                                      &input_identity.bus_type,
+                                                      &input_identity.vendor_id,
+                                                      &input_identity.product_id,
+                                                      &input_identity.version,
+                                                      &input_identity.name,
+                                                      &input_identity.phys,
+                                                      &input_identity.uniq));
+  g_assert_cmphex (input_identity.bus_type, ==, 0x0003);
+  g_assert_cmphex (input_identity.vendor_id, ==, 0x28de);
+  g_assert_cmphex (input_identity.product_id, ==, 0x1142);
+  g_assert_cmphex (input_identity.version, ==, 0x0111);
+  g_assert_cmpstr (input_identity.name, ==, "Valve Software Steam Controller");
+  g_assert_cmpstr (input_identity.phys, ==, "usb-0000:00:14.0-1.1/input0");
+  g_assert_cmpstr (input_identity.uniq, ==, NULL);
+  uevent = srt_input_device_dup_input_uevent (dev);
+  g_assert_cmpstr (uevent, ==,
+                   "PRODUCT=3/28de/1142/111\n"
+                   "NAME=\"Valve Software Steam Controller\"\n"
+                   "PHYS=\"usb-0000:00:14.0-1.1/input0\"\n"
+                   "UNIQ=\"\"\n"
+                   "PROP=0\n"
+                   "EV=120017\n"
+                   "KEY=1f0000 0 0 e080ffdf01cfffff fffffffffffffffe\n"
+                   "REL=903\n"
+                   "MSC=10\n"
+                   "LED=1f\n"
+                   "MODALIAS=input:b0003v28DEp1142e0111-e0,1,2,4,11,14,k77,7D,7E,7F,110,111,112,113,114,r0,1,8,B,am4,l0,1,2,3,4,sfw\n");
+  g_clear_pointer (&uevent, g_free);
+
+  g_assert_cmpstr (srt_input_device_get_usb_device_sys_path (dev),
+                   ==, "/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.1");
+  g_assert_true (srt_input_device_get_usb_device_identity (dev,
+                                                           NULL, NULL, NULL,
+                                                           NULL, NULL, NULL));
+  g_assert_true (srt_input_device_get_usb_device_identity (dev,
+                                                           &usb_identity.vendor_id,
+                                                           &usb_identity.product_id,
+                                                           &usb_identity.version,
+                                                           &usb_identity.manufacturer,
+                                                           &usb_identity.product,
+                                                           &usb_identity.serial));
+  g_assert_cmphex (usb_identity.vendor_id, ==, 0x28de);
+  g_assert_cmphex (usb_identity.product_id, ==, 0x1142);
+  g_assert_cmphex (usb_identity.version, ==, 0x0001);
+  g_assert_cmpstr (usb_identity.manufacturer, ==, "Valve Software");
+  g_assert_cmpstr (usb_identity.product, ==, "Steam Controller");
+  g_assert_cmpstr (usb_identity.serial, ==, NULL);
+  uevent = srt_input_device_dup_usb_device_uevent (dev);
+  g_assert_cmpstr (uevent, ==,
+                   "MAJOR=189\n"
+                   "MINOR=66\n"
+                   "DEVNAME=bus/usb/001/067\n"
+                   "DEVTYPE=usb_device\n"
+                   "DRIVER=usb\n"
+                   "PRODUCT=28de/1142/1\n"
+                   "TYPE=0/0/0\n"
+                   "BUSNUM=001\n"
+                   "DEVNUM=067\n");
+  g_clear_pointer (&uevent, g_free);
+}
+
+static void
+test_input_device_from_json (Fixture *f,
+                             gconstpointer context)
+{
+  test_input_device_from_json_no_details (f);
+  test_input_device_from_json_odd (f);
+  test_input_device_from_json_steam_controller (f);
+}
+
+typedef struct
+{
+  const char *name;
+  guint16 bus_type;
+  guint16 vendor_id;
+  guint16 product_id;
+  guint16 version;
+  guint8 ev[(EV_MAX + 1) / 8];
+  guint8 keys[(KEY_MAX + 1) / 8];
+  guint8 abs[(ABS_MAX + 1) / 8];
+  guint8 rel[(REL_MAX + 1) / 8];
+  guint8 ff[(FF_MAX + 1) / 8];
+  guint8 props[INPUT_PROP_MAX / 8];
+  SrtInputDeviceTypeFlags expected;
+} GuessTest;
+
+/*
+ * Test-cases for guessing a device type from its capabilities.
+ *
+ * The bytes in ev, etc. are in little-endian byte order, the same as
+ * the JSON output from input-monitor. Trailing zeroes can be omitted.
+ */
+#define ZEROx4 0, 0, 0, 0
+#define ZEROx8 ZEROx4, ZEROx4
+#define FFx4 0xff, 0xff, 0xff, 0xff
+#define FFx8 FFx4, FFx4
+
+static const GuessTest guess_tests[] =
+{
+    {
+      .name = "Xbox 360 wired USB controller",
+      .bus_type = 0x0003,
+      .vendor_id = 0x045e,
+      .product_id = 0x028e,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      /* SYN, KEY, ABS, FF */
+      .ev = { 0x0b, 0x00, 0x20 },
+      /* X, Y, Z, RX, RY, RZ, HAT0X, HAT0Y */
+      .abs = { 0x3f, 0x00, 0x03 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* A, B, X, Y, TL, TR, SELECT, START, MODE, THUMBL, THUMBR */
+          /* 0x100 */ ZEROx4, 0x00, 0x00, 0xdb, 0x7c,
+      },
+    },
+    {
+      .name = "X-Box One Elite",
+      .bus_type = 0x0003,
+      .vendor_id = 0x045e,
+      .product_id = 0x02e3,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      /* SYN, KEY, ABS */
+      .ev = { 0x0b },
+      /* X, Y, Z, RX, RY, RZ, HAT0X, HAT0Y */
+      .abs = { 0x3f, 0x00, 0x03 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* A, B, X, Y, TL, TR, SELECT, START, MODE, THUMBL, THUMBR */
+          /* 0x100 */ ZEROx4, 0x00, 0x00, 0xdb, 0x7c,
+      },
+    },
+    {
+      .name = "X-Box One S via Bluetooth",
+      .bus_type = 0x0005,
+      .vendor_id = 0x045e,
+      .product_id = 0x02e0,
+      .version = 0x1130,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      /* SYN, KEY, ABS */
+      .ev = { 0x0b },
+      /* X, Y, Z, RX, RY, RZ, HAT0X, HAT0Y */
+      .abs = { 0x3f, 0x00, 0x03 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* A, B, X, Y, TL, TR, SELECT, START, MODE, THUMBL, THUMBR */
+          /* 0x100 */ ZEROx4, 0x00, 0x00, 0xdb, 0x7c,
+      },
+    },
+    {
+      .name = "X-Box One S wired",
+      .bus_type = 0x0003,
+      .vendor_id = 0x045e,
+      .product_id = 0x02ea,
+      .version = 0x0301,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      /* SYN, KEY, ABS */
+      .ev = { 0x0b },
+      /* X, Y, Z, RX, RY, RZ, HAT0X, HAT0Y */
+      .abs = { 0x3f, 0x00, 0x03 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* A, B, X, Y, TL, TR, SELECT, START, MODE, THUMBL, THUMBR */
+          /* 0x100 */ ZEROx4, 0x00, 0x00, 0xdb, 0x7c,
+      },
+    },
+    {
+      .name = "DualShock 4 - gamepad",
+      .bus_type = 0x0003,
+      .vendor_id = 0x054c,
+      .product_id = 0x09cc,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      /* SYN, KEY, ABS, MSC, FF */
+      /* Some versions only have 0x0b, SYN, KEY, ABS, like the
+       * Bluetooth example below */
+      .ev = { 0x1b, 0x00, 0x20 },
+      /* X, Y, Z, RX, RY, RZ, HAT0X, HAT0Y */
+      .abs = { 0x3f, 0x00, 0x03 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* A, B, X, Y, TL, TR, TL2, TR2, SELECT, START, MODE,
+           * THUMBL, THUMBR */
+          /* 0x100 */ ZEROx4, 0x00, 0x00, 0xdb, 0x7f,
+      },
+    },
+    {
+      .name = "DualShock 4 - gamepad via Bluetooth",
+      .bus_type = 0x0005,
+      .vendor_id = 0x054c,
+      .product_id = 0x09cc,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      /* SYN, KEY, ABS */
+      .ev = { 0x0b },
+      /* X, Y, Z, RX, RY, RZ, HAT0X, HAT0Y */
+      .abs = { 0x3f, 0x00, 0x03 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* A, B, X, Y, TL, TR, TL2, TR2, SELECT, START, MODE,
+           * THUMBL, THUMBR */
+          /* 0x100 */ ZEROx4, 0x00, 0x00, 0xdb, 0x7f,
+      },
+    },
+    {
+      .name = "DualShock 4 - touchpad",
+      .bus_type = 0x0003,
+      .vendor_id = 0x054c,
+      .product_id = 0x09cc,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_TOUCHPAD,
+      /* SYN, KEY, ABS */
+      .ev = { 0x0b },
+      /* X, Y, multitouch */
+      .abs = { 0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0x60, 0x02 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* Left mouse button */
+          /* 0x100 */ 0x00, 0x00, 0x01, 0x00, ZEROx4,
+          /* BTN_TOOL_FINGER and some multitouch stuff */
+          /* 0x140 */ 0x20, 0x24, 0x00, 0x00
+      },
+      /* POINTER, BUTTONPAD */
+      .props = { 0x05 },
+    },
+    {
+      .name = "DualShock 4 - accelerometer",
+      .bus_type = 0x0003,
+      .vendor_id = 0x054c,
+      .product_id = 0x09cc,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_ACCELEROMETER,
+      /* SYN, ABS, MSC */
+      .ev = { 0x19 },
+      /* X, Y, Z, RX, RY, RZ */
+      .abs = { 0x3f },
+      /* ACCELEROMETER */
+      .props = { 0x40 },
+    },
+    {
+      .name = "DualShock 4 via USB dongle",
+      .bus_type = 0x0003,
+      .vendor_id = 0x054c,
+      .product_id = 0x0ba0,
+      .version = 0x8111,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      /* SYN, ABS, KEY */
+      .ev = { 0x0b },
+      /* X, Y, Z, RX, RY, RZ, HAT0X, HAT0Y */
+      .abs = { 0x3f, 0x00, 0x03 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* A, B, X, Y, TL, TR, TL2, TR2, SELECT, START, MODE,
+           * THUMBL, THUMBR */
+          /* 0x100 */ ZEROx4, 0x00, 0x00, 0xdb, 0x7f,
+      },
+    },
+    {
+      .name = "DualShock 3 - gamepad",
+      .bus_type = 0x0003,
+      .vendor_id = 0x054c,
+      .product_id = 0x0268,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      /* SYN, KEY, ABS, MSC, FF */
+      .ev = { 0x1b, 0x00, 0x20 },
+      /* X, Y, Z, RX, RY, RZ */
+      .abs = { 0x3f },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* A, B, X, Y, TL, TR, TL2, TR2, SELECT, START, MODE,
+           * THUMBL, THUMBR */
+          /* 0x100 */ ZEROx4, 0x00, 0x00, 0xdb, 0x7f,
+          /* 0x140 */ ZEROx8,
+          /* 0x180 */ ZEROx8,
+          /* 0x1c0 */ ZEROx8,
+          /* Digital dpad */
+          /* 0x200 */ ZEROx4, 0x0f, 0x00, 0x00, 0x00,
+      },
+    },
+    {
+      .name = "DualShock 3 - accelerometer",
+      .bus_type = 0x0003,
+      .vendor_id = 0x054c,
+      .product_id = 0x0268,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_ACCELEROMETER,
+      /* SYN, ABS */
+      .ev = { 0x09 },
+      /* X, Y, Z */
+      .abs = { 0x07 },
+      /* ACCELEROMETER */
+      .props = { 0x40 },
+    },
+    {
+      .name = "Steam Controller - gamepad",
+      .bus_type = 0x0003,
+      .vendor_id = 0x28de,
+      .product_id = 0x1142,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      /* SYN, KEY, ABS */
+      .ev = { 0x0b },
+      /* X, Y, RX, RY, HAT0X, HAT0Y, HAT2X, HAT2Y */
+      .abs = { 0x1b, 0x00, 0x33 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* A, B, X, Y, TL, TR, TL2, TR2, SELECT, START, MODE,
+           * THUMBL, THUMBR, joystick THUMB, joystick THUMB2  */
+          /* 0x100 */ ZEROx4, 0x06, 0x00, 0xdb, 0x7f,
+          /* GEAR_DOWN, GEAR_UP */
+          /* 0x140 */ 0x00, 0x00, 0x03, 0x00, ZEROx4,
+          /* 0x180 */ ZEROx8,
+          /* 0x1c0 */ ZEROx8,
+          /* Digital dpad */
+          /* 0x200 */ ZEROx4, 0x0f, 0x00, 0x00, 0x00,
+      },
+    },
+    {
+      /* Present to support lizard mode, even if no Steam Controller
+       * is connected */
+      .name = "Steam Controller - dongle",
+      .bus_type = 0x0003,
+      .vendor_id = 0x28de,
+      .product_id = 0x1142,
+      .expected = (SRT_INPUT_DEVICE_TYPE_FLAGS_KEYBOARD
+                   | SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS
+                   | SRT_INPUT_DEVICE_TYPE_FLAGS_MOUSE),
+      /* SYN, KEY, REL, MSC, LED, REP */
+      .ev = { 0x17, 0x00, 0x12 },
+      /* X, Y, mouse wheel, high-res mouse wheel */
+      .rel = { 0x03, 0x09 },
+      .keys = {
+          /* 0x00 */ 0xfe, 0xff, 0xff, 0xff, FFx4,
+          /* 0x40 */ 0xff, 0xff, 0xcf, 0x01, 0xdf, 0xff, 0x80, 0xe0,
+          /* 0x80 */ ZEROx8,
+          /* 0xc0 */ ZEROx8,
+          /* 0x100 */ 0x00, 0x00, 0x1f, 0x00, ZEROx4,
+      },
+    },
+    {
+      .name = "Guitar Hero for PS3",
+      .bus_type = 0x0003,
+      .vendor_id = 0x12ba,
+      .product_id = 0x0100,
+      .version = 0x0110,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      /* SYN, KEY, ABS */
+      .ev = { 0x0b },
+      /* X, Y, Z, RZ, HAT0X, HAT0Y */
+      .abs = { 0x27, 0x00, 0x03 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* ABC, XYZ, TL, TR, TL2, TR2, SELECT, START, MODE */
+          /* 0x100 */ ZEROx4, 0x00, 0x00, 0xff, 0x1f,
+      },
+    },
+    {
+      .name = "G27 Racing Wheel, 0003:046d:c29b v0111",
+      .bus_type = 0x0003,
+      .vendor_id = 0x046d,
+      .product_id = 0xc29b,
+      .version = 0x0111,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      /* SYN, KEY, ABS */
+      .ev = { 0x0b },
+      /* X, Y, Z, RZ, HAT0X, HAT0Y */
+      .abs = { 0x27, 0x00, 0x03 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* 16 buttons: TRIGGER, THUMB, THUMB2, TOP, TOP2, PINKIE, BASE,
+           * BASE2..BASE6, unregistered event codes 0x12c-0x12e, DEAD */
+          /* 0x100 */ ZEROx4, 0xff, 0xff, 0x00, 0x00,
+          /* 0x140 */ ZEROx8,
+          /* 0x180 */ ZEROx8,
+          /* 0x1c0 */ ZEROx8,
+          /* 0x200 */ ZEROx8,
+          /* 0x240 */ ZEROx8,
+          /* 0x280 */ ZEROx8,
+          /* TRIGGER_HAPPY1..TRIGGER_HAPPY7 */
+          /* 0x2c0 */ 0x7f,
+      },
+    },
+    {
+      .name = "Logitech Driving Force, 0003:046d:c294 v0100",
+      .bus_type = 0x0003,
+      .vendor_id = 0x046d,
+      .product_id = 0xc294,
+      .version = 0x0100,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      /* SYN, KEY, ABS */
+      .ev = { 0x0b },
+      /* X, Y, RZ, HAT0X, HAT0Y */
+      .abs = { 0x23, 0x00, 0x03 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* 12 buttons: TRIGGER, THUMB, THUMB2, TOP, TOP2, PINKIE, BASE,
+           * BASE2..BASE6 */
+          /* 0x100 */ ZEROx4, 0xff, 0x0f, 0x00, 0x00,
+      },
+    },
+    {
+      .name = "Logitech Dual Action",
+      .bus_type = 0x0003,
+      .vendor_id = 0x046d,
+      .product_id = 0xc216,
+      .version = 0x0110,
+      /* Logitech RumblePad 2 USB, 0003:046d:c218 v0110, is the same
+       * except for having force feedback, which we don't use in our
+       * heuristic */
+      /* Jess Tech GGE909 PC Recoil Pad, 0003:0f30:010b v0110, is the same */
+      /* 8BitDo SNES30, 0003:2dc8:ab20 v0110, is the same */
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      /* SYN, KEY, ABS */
+      .ev = { 0x0b },
+      /* X, Y, Z, RZ, HAT0X, HAT0Y */
+      .abs = { 0x27, 0x00, 0x03 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* 12 buttons: TRIGGER, THUMB, THUMB2, TOP, TOP2, PINKIE, BASE,
+           * BASE2..BASE6 */
+          /* 0x100 */ ZEROx4, 0xff, 0x0f, 0x00, 0x00,
+      },
+    },
+    {
+      .name = "Saitek ST290 Pro flight stick",
+      .bus_type = 0x0003,
+      .vendor_id = 0x06a3,
+      .product_id = 0x0160,   /* 0x0460 seems to be the same */
+      .version = 0x0100,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      /* SYN, KEY, ABS, MSC */
+      .ev = { 0x1b },
+      /* X, Y, Z, RZ, HAT0X, HAT0Y */
+      .abs = { 0x27, 0x00, 0x03 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* TRIGGER, THUMB, THUMB2, TOP, TOP2, PINKIE */
+          /* 0x100 */ ZEROx4, 0x3f, 0x00, 0x00, 0x00,
+      },
+    },
+    {
+      .name = "Saitek X52 Pro Flight Control System",
+      .bus_type = 0x0003,
+      .vendor_id = 0x06a3,
+      .product_id = 0x0762,
+      .version = 0x0111,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      .ev = { 0x0b },
+      /* XYZ, RXYZ, throttle, hat0, MISC, unregistered event code 0x29 */
+      .abs = { 0x7f, 0x00, 0x03, 0x00, 0x00, 0x03 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* 16 buttons: TRIGGER, THUMB, THUMB2, TOP, TOP2, PINKIE, BASE,
+           * BASE2..BASE6, unregistered event codes 0x12c-0x12e, DEAD */
+          /* 0x100 */ ZEROx4, 0xff, 0xff, 0x00, 0x00,
+          /* 0x140 */ ZEROx8,
+          /* 0x180 */ ZEROx8,
+          /* 0x1c0 */ ZEROx8,
+          /* 0x200 */ ZEROx8,
+          /* 0x240 */ ZEROx8,
+          /* 0x280 */ ZEROx8,
+          /* TRIGGER_HAPPY1..TRIGGER_HAPPY23 */
+          /* 0x2c0 */ 0xff, 0xff, 0x7f,
+      },
+    },
+    {
+      .name = "Logitech Extreme 3D",
+      .bus_type = 0x0003,
+      .vendor_id = 0x046d,
+      .product_id = 0xc215,
+      .version = 0x0110,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      /* SYN, KEY, ABS, MSC */
+      .ev = { 0x0b },
+      /* X, Y, RZ, throttle, hat 0 */
+      .abs = { 0x63, 0x00, 0x03 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* 12 buttons: TRIGGER, THUMB, THUMB2, TOP, TOP2, PINKIE, BASE,
+           * BASE2..BASE6 */
+          /* 0x100 */ ZEROx4, 0xff, 0x0f, 0x00, 0x00,
+      },
+    },
+    {
+      .name = "Hori Real Arcade Pro VX-SA",
+      .bus_type = 0x0003,
+      .vendor_id = 0x24c6,
+      .product_id = 0x5501,
+      .version = 0x0533,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      /* SYN, KEY, ABS */
+      .ev = { 0x0b },
+      /* X, Y, Z, RX, RY, RZ, hat 0 */
+      .abs = { 0x3f, 0x00, 0x03 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* A, B, X, Y, TL, TR, SELECT, START, MODE, THUMBL, THUMBR */
+          /* 0x100 */ ZEROx4, 0x00, 0x00, 0xdb, 0x7c,
+      },
+    },
+    {
+      .name = "Switch Pro Controller via Bluetooth",
+      .bus_type = 0x0005,
+      .vendor_id = 0x057e,
+      .product_id = 0x2009,
+      .version = 0x0001,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      /* SYN, KEY, ABS */
+      .ev = { 0x0b },
+      /* X, Y, RX, RY, hat 0 */
+      .abs = { 0x1b, 0x00, 0x03 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* 16 buttons: TRIGGER, THUMB, THUMB2, TOP, TOP2, PINKIE, BASE,
+           * BASE2..BASE6, unregistered event codes 0x12c-0x12e, DEAD */
+          /* 0x100 */ ZEROx4, 0xff, 0xff, 0x00, 0x00,
+          /* 0x140 */ ZEROx8,
+          /* 0x180 */ ZEROx8,
+          /* 0x1c0 */ ZEROx8,
+          /* 0x200 */ ZEROx8,
+          /* 0x240 */ ZEROx8,
+          /* 0x280 */ ZEROx8,
+          /* TRIGGER_HAPPY1..TRIGGER_HAPPY2 */
+          /* 0x2c0 */ 0x03,
+      },
+    },
+    {
+      .name = "Switch Pro Controller via USB",
+      .bus_type = 0x0003,
+      .vendor_id = 0x057e,
+      .product_id = 0x2009,
+      .version = 0x0111,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      /* SYN, KEY, ABS */
+      .ev = { 0x0b },
+      /* X, Y, Z, RZ, HAT0X, HAT0Y */
+      .abs = { 0x27, 0x00, 0x03 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+      },
+    },
+    {
+      .name = "Thrustmaster Racing Wheel FFB",
+      /* Mad Catz FightStick TE S+ PS4, 0003:0738:8384:0111 v0111
+       * (aka Street Fighter V Arcade FightStick TES+)
+       * is functionally the same */
+      .bus_type = 0x0003,
+      .vendor_id = 0x044f,
+      .product_id = 0xb66d,
+      .version = 0x0110,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      .ev = { 0x0b },
+      /* XYZ, RXYZ, hat 0 */
+      .abs = { 0x3f, 0x00, 0x03 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* ABC, XYZ, TL, TR, TL2, TR2, SELECT, START, MODE,
+           * THUMBL */
+          /* 0x100 */ ZEROx4, 0x00, 0x00, 0xff, 0x3f,
+      },
+    },
+    {
+      .name = "Thrustmaster T.Flight Hotas X",
+      .bus_type = 0x0003,
+      .vendor_id = 0x044f,
+      .product_id = 0xb108,
+      .version = 0x0100,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      .ev = { 0x0b },
+      /* XYZ, RZ, throttle, hat 0 */
+      .abs = { 0x67, 0x00, 0x03 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* trigger, thumb, thumb2, top, top2, pinkie, base,
+           * base2..base6 */
+          /* 0x100 */ ZEROx4, 0xff, 0x0f
+      },
+    },
+    {
+      .name = "8BitDo N30 Pro 2",
+      .bus_type = 0x0003,
+      .vendor_id = 0x2dc8,
+      .product_id = 0x9015,
+      .version = 0x0111,
+      /* 8BitDo NES30 Pro, 0003:2dc8:9001 v0111, is the same */
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      .ev = { 0x0b },
+      /* XYZ, RZ, gas, brake, hat0 */
+      .abs = { 0x27, 0x06, 0x03 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* ABC, XYZ, TL, TR, TL2, TR2, select, start, mode, thumbl,
+           * thumbr */
+          /* 0x100 */ ZEROx4, 0x00, 0x00, 0xff, 0x7f,
+      },
+    },
+    {
+      .name = "Retro Power SNES-style controller, 0003:0079:0011 v0110",
+      .bus_type = 0x0003,
+      .vendor_id = 0x0079,
+      .product_id = 0x0011,
+      .version = 0x0110,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      .ev = { 0x0b },
+      /* X, Y */
+      .abs = { 0x03 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* trigger, thumb, thumb2, top, top2, pinkie, base,
+           * base2..base4 */
+          /* 0x100 */ ZEROx4, 0xff, 0x03, 0x00, 0x00,
+      },
+    },
+    {
+      .name = "Wiimote - buttons",
+      .bus_type = 0x0005,
+      .vendor_id = 0x057e,
+      .product_id = 0x0306,
+      .version = 0x8600,
+      /* This one is a bit weird because some of the buttons are mapped
+       * to the arrow, page up and page down keys, so it's a joystick
+       * with a subset of a keyboard attached */
+      .expected = (SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK
+                   | SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS),
+      /* SYN, KEY */
+      .ev = { 0x03 },
+      .keys = {
+          /* 0x00 */ ZEROx8,
+          /* left, right, up down */
+          /* 0x40 */ ZEROx4, 0x80, 0x16, 0x00, 0x00,
+          /* 0x80 */ ZEROx8,
+          /* 0xc0 */ ZEROx8,
+          /* BTN_1, BTN_2, BTN_A, BTN_B, BTN_MODE */
+          /* 0x100 */ 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x10,
+          /* 0x140 */ ZEROx8,
+          /* next (page down), previous (page up) */
+          /* 0x180 */ 0x00, 0x00, 0x80, 0x10, ZEROx4,
+      },
+    },
+    {
+      .name = "Wiimote - Motion Plus or accelerometer",
+      .bus_type = 0x0005,
+      .vendor_id = 0x057e,
+      .product_id = 0x0306,
+      .version = 0x8600,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_ACCELEROMETER,
+      /* SYN, ABS */
+      .ev = { 0x09 },
+      /* RX, RY, RZ */
+      .abs = { 0x38 },
+    },
+    {
+      .name = "Wiimote - IR positioning",
+      .bus_type = 0x0005,
+      .vendor_id = 0x057e,
+      .product_id = 0x0306,
+      .version = 0x8600,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      /* SYN, ABS */
+      .ev = { 0x09 },
+      /* HAT0 to HAT3 */
+      .abs = { 0x00, 0x1f },
+    },
+    {
+      .name = "Wiimote - Nunchuck",
+      .bus_type = 0x0005,
+      .vendor_id = 0x057e,
+      .product_id = 0x0306,
+      .version = 0x8600,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      /* SYN, KEY, ABS */
+      .ev = { 0x0b },
+      /* RX, RY, RZ, hat 0 */
+      .abs = { 0x38, 0x00, 0x03 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+         /* C and Z buttons */
+          /* 0x100 */ ZEROx4, 0x00, 0x00, 0x24, 0x00,
+      },
+    },
+    {
+      /* Flags guessed from kernel source code */
+      .name = "Wiimote - Classic Controller",
+      .expected = (SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK
+                   | SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS),
+      /* SYN, KEY, ABS */
+      .ev = { 0x0b },
+      /* Hat 1-3 */
+      .abs = { 0x00, 0x1c },
+      .keys = {
+          /* 0x00 */ ZEROx8,
+          /* left, right, up down */
+          /* 0x40 */ ZEROx4, 0x80, 0x16, 0x00, 0x00,
+          /* 0x80 */ ZEROx8,
+          /* 0xc0 */ ZEROx8,
+          /* A, B, X, Y, MODE, TL, TL2, TR, TR2 */
+          /* 0x100 */ ZEROx4, 0x00, 0x13, 0xdb, 0x10,
+          /* 0x140 */ ZEROx8,
+          /* next, previous */
+          /* 0x180 */ 0x00, 0x00, 0x80, 0x10, ZEROx4,
+      },
+    },
+    {
+      /* Flags guessed from kernel source code */
+      .name = "Wiimote - Balance Board",
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      /* SYN, KEY, ABS */
+      .ev = { 0x0b },
+      /* Hat 0-1 */
+      .abs = { 0x00, 0x0f },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* A */
+          /* 0x100 */ ZEROx4, 0x00, 0x00, 0x01, 0x00,
+      },
+    },
+    {
+      /* Flags guessed from kernel source code */
+      .name = "Wiimote - Wii U Pro Controller",
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK,
+      /* SYN, KEY, ABS */
+      .ev = { 0x0b },
+      /* X, Y, RX, RY */
+      .abs = { 0x1b },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* A, B, X, Y, TL, TR, TL2, TR2, SELECT, START, MODE,
+           * THUMBL, THUMBR */
+          /* 0x100 */ ZEROx4, 0x00, 0x00, 0xdb, 0x7f,
+          /* 0x140 */ ZEROx8,
+          /* 0x180 */ ZEROx8,
+          /* 0x1c0 */ ZEROx8,
+          /* Digital dpad */
+          /* 0x200 */ ZEROx4, 0x0f, 0x00, 0x00, 0x00,
+      },
+    },
+    {
+      .name = "Synaptics TM3381-002 (Thinkpad X280 trackpad)",
+      .bus_type = 0x001d,   /* BUS_RMI */
+      .vendor_id = 0x06cb,
+      .product_id = 0x0000,
+      .version = 0x0000,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_TOUCHPAD,
+      /* SYN, KEY, ABS */
+      .ev = { 0x0b },
+      /* X, Y, pressure, multitouch */
+      .abs = { 0x03, 0x00, 0x00, 0x01, 0x00, 0x80, 0xf3, 0x06 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* Left mouse button */
+          /* 0x100 */ 0x00, 0x00, 0x01, 0x00, ZEROx4,
+          /* BTN_TOOL_FINGER and some multitouch gestures */
+          /* 0x140 */ 0x20, 0xe5
+      },
+      /* POINTER, BUTTONPAD */
+      .props = { 0x05 },
+    },
+    {
+      .name = "TPPS/2 Elan TrackPoint (Thinkpad X280)",
+      .bus_type = 0x0011,   /* BUS_I8042 */
+      .vendor_id = 0x0002,
+      .product_id = 0x000a,
+      .version = 0x0000,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_POINTING_STICK,
+      /* SYN, KEY, REL */
+      .ev = { 0x07 },
+      /* X, Y */
+      .rel = { 0x03 },
+      .keys = {
+          /* 0x00-0xff */ ZEROx8, ZEROx8, ZEROx8, ZEROx8,
+          /* Left, middle, right mouse buttons */
+          /* 0x100 */ 0x00, 0x00, 0x07,
+      },
+      /* POINTER, POINTING_STICK */
+      .props = { 0x21 },
+    },
+    {
+      .name = "Thinkpad ACPI buttons",
+      .expected = (SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS |
+                   SRT_INPUT_DEVICE_TYPE_FLAGS_SWITCH),
+      /* SYN, KEY, MSC, SW */
+      .ev = { 0x33 },
+      .keys = {
+          /* 0x00 */ ZEROx8,
+          /* 0x40 */ ZEROx4, 0x00, 0x00, 0x0e, 0x01,
+          /* 0x80 */ 0x00, 0x50, 0x11, 0x51, 0x00, 0x28, 0x00, 0xc0,
+          /* 0xc0 */ 0x04, 0x20, 0x10, 0x02, 0x1b, 0x70, 0x01, 0x00,
+          /* 0x100 */ ZEROx8,
+          /* 0x140 */ ZEROx4, 0x00, 0x00, 0x50, 0x00,
+          /* 0x180 */ ZEROx8,
+          /* 0x1c0 */ 0x00, 0x00, 0x04, 0x18, ZEROx4,
+          /* 0x200 */ ZEROx8,
+          /* 0x240 */ 0x40, 0x00, 0x01, 0x00, ZEROx4,
+      },
+    },
+    {
+      .name = "PC speaker",
+      .bus_type = 0x0010,   /* BUS_ISA */
+      .vendor_id = 0x001f,
+      .product_id = 0x0001,
+      .version = 0x0100,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_NONE,
+      /* SYN, SND */
+      .ev = { 0x01, 0x00, 0x04 },
+    },
+    {
+      .name = "ALSA headphone detection, etc.",
+      .bus_type = 0x0000,
+      .vendor_id = 0x0000,
+      .product_id = 0x0000,
+      .version = 0x0000,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_SWITCH,
+      /* SYN, SW */
+      .ev = { 0x21 },
+    },
+    {
+      /* Assumed to be a reasonably typical i8042 (PC AT) keyboard */
+      .name = "Thinkpad T520 and X280 keyboards",
+      .bus_type = 0x0011,   /* BUS_I8042 */
+      .vendor_id = 0x0001,
+      .product_id = 0x0001,
+      .version = 0xab54,
+      .expected = (SRT_INPUT_DEVICE_TYPE_FLAGS_KEYBOARD
+                   | SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS),
+      /* SYN, KEY, MSC, LED, REP */
+      .ev = { 0x13, 0x00, 0x12 },
+      .keys = {
+          /* 0x00 */ 0xfe, 0xff, 0xff, 0xff, FFx4,
+          /* 0x40 */ 0xff, 0xff, 0xef, 0xff, 0xdf, 0xff, 0xff, 0xfe,
+          /* 0x80 */ 0x01, 0xd0, 0x00, 0xf8, 0x78, 0x30, 0x80, 0x03,
+          /* 0xc0 */ 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0x00,
+      },
+    },
+    {
+      .name = "Thinkpad X280 sleep button",
+      .bus_type = 0x0019,   /* BUS_HOST */
+      .vendor_id = 0x0000,
+      .product_id = 0x0003,
+      .version = 0x0000,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS,
+      /* SYN, KEY */
+      .ev = { 0x03 },
+      .keys = {
+          /* 0x00 */ ZEROx8,
+          /* 0x40 */ ZEROx8,
+          /* KEY_SLEEP */
+          /* 0x80 */ 0x00, 0x40,
+      },
+    },
+    {
+      .name = "Thinkpad X280 lid switch",
+      .bus_type = 0x0019,   /* BUS_HOST */
+      .vendor_id = 0x0000,
+      .product_id = 0x0005,
+      .version = 0x0000,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_SWITCH,
+      /* SYN, SW */
+      .ev = { 0x21 },
+    },
+    {
+      .name = "Thinkpad X280 power button",
+      .bus_type = 0x0019,   /* BUS_HOST */
+      .vendor_id = 0x0000,
+      .product_id = 0x0001,
+      .version = 0x0000,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS,
+      /* SYN, KEY */
+      .ev = { 0x03 },
+      .keys = {
+          /* 0x00 */ ZEROx8,
+          /* KEY_POWER */
+          /* 0x40 */ ZEROx4, 0x00, 0x00, 0x10, 0x00,
+      },
+    },
+    {
+      .name = "Thinkpad X280 video bus",
+      .bus_type = 0x0019,   /* BUS_HOST */
+      .vendor_id = 0x0000,
+      .product_id = 0x0006,
+      .version = 0x0000,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS,
+      /* SYN, KEY */
+      .ev = { 0x03 },
+      .keys = {
+          /* 0x00 */ ZEROx8,
+          /* 0x40 */ ZEROx8,
+          /* 0x80 */ ZEROx8,
+          /* brightness control, video mode, display off */
+          /* 0xc0 */ ZEROx4, 0x0b, 0x00, 0x3e, 0x00,
+      },
+    },
+    {
+      .name = "Thinkpad X280 extra buttons",
+      .bus_type = 0x0019,   /* BUS_HOST */
+      .vendor_id = 0x17aa,
+      .product_id = 0x5054,
+      .version = 0x4101,
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS,
+      /* SYN, KEY */
+      .ev = { 0x03 },
+      .keys = {
+          /* 0x00 */ ZEROx8,
+          /* 0x40 */ ZEROx4, 0x00, 0x00, 0x0e, 0x01,
+          /* 0x80 */ 0x00, 0x50, 0x11, 0x51, 0x00, 0x28, 0x00, 0xc0,
+          /* 0xc0 */ 0x04, 0x20, 0x10, 0x02, 0x1b, 0x70, 0x01, 0x00,
+          /* 0x100 */ ZEROx8,
+          /* 0x140 */ ZEROx4, 0x00, 0x00, 0x50, 0x00,
+          /* 0x180 */ ZEROx8,
+          /* 0x1c0 */ 0x00, 0x00, 0x04, 0x18, ZEROx4,
+          /* 0x200 */ ZEROx8,
+          /* 0x240 */ 0x40, 0x00, 0x01, 0x00, ZEROx4,
+      },
+    },
+    {
+      .name = "Thinkpad USB keyboard with Trackpoint - keyboard",
+      .bus_type = 0x0003,
+      .vendor_id = 0x17ef,
+      .product_id = 0x6009,
+      .expected = (SRT_INPUT_DEVICE_TYPE_FLAGS_KEYBOARD
+                   | SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS),
+      /* SYN, KEY, MSC, LED, REP */
+      .ev = { 0x13, 0x00, 0x12 },
+      .keys = {
+          /* 0x00 */ 0xfe, 0xff, 0xff, 0xff, FFx4,
+          /* 0x40 */ 0xff, 0xff, 0xef, 0xff, 0xdf, 0xff, 0xbe, 0xfe,
+          /* 0x80 */ 0xff, 0x57, 0x40, 0xc1, 0x7a, 0x20, 0x9f, 0xff,
+          /* 0xc0 */ 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
+      },
+    },
+    {
+      .name = "Thinkpad USB keyboard with Trackpoint - Trackpoint",
+      .bus_type = 0x0003,
+      .vendor_id = 0x17ef,
+      .product_id = 0x6009,
+      /* For some reason the special keys like mute and wlan toggle
+       * show up here instead of, or in addition to, as part of
+       * the keyboard - so we report this as having keys too. */
+      .expected = (SRT_INPUT_DEVICE_TYPE_FLAGS_MOUSE
+                   | SRT_INPUT_DEVICE_TYPE_FLAGS_HAS_KEYS),
+      /* SYN, KEY, REL, MSC, LED */
+      .ev = { 0x17, 0x00, 0x02 },
+      /* X, Y */
+      .rel = { 0x03 },
+      .keys = {
+          /* 0x00 */ ZEROx8,
+          /* 0x40 */ ZEROx4, 0x00, 0x00, 0x1e, 0x00,
+          /* 0x80 */ 0x00, 0xcc, 0x11, 0x01, 0x78, 0x40, 0x00, 0xc0,
+          /* 0xc0 */ 0x00, 0x20, 0x10, 0x00, 0x0b, 0x50, 0x00, 0x00,
+          /* Mouse buttons: left, right, middle, "task" */
+          /* 0x100 */ 0x00, 0x00, 0x87, 0x68, ZEROx4,
+          /* 0x140 */ ZEROx4, 0x00, 0x00, 0x10, 0x00,
+          /* 0x180 */ ZEROx4, 0x00, 0x00, 0x40, 0x00,
+      },
+    },
+    {
+      .name = "No information",
+      .expected = SRT_INPUT_DEVICE_TYPE_FLAGS_NONE,
+    }
+};
+
+static void
+test_input_device_guess (Fixture *f,
+                         gconstpointer context)
+{
+  size_t i;
+
+  for (i = 0; i < G_N_ELEMENTS (guess_tests); i++)
+    {
+      const GuessTest *t = &guess_tests[i];
+      SrtEvdevCapabilities caps;
+      size_t j;
+      SrtInputDeviceTypeFlags actual;
+
+      g_test_message ("%s", t->name);
+
+      /* The real SrtEvdevCapabilities rounds the sizes up to the next
+       * 32- or 64-bit boundary, and GuessTest rounds them up to the next
+       * 8-bit boundary, so GuessTest is the same size or smaller. */
+      g_assert_cmpuint (sizeof (caps.ev), >=, sizeof (t->ev));
+      g_assert_cmpuint (sizeof (caps.keys), >=, sizeof (t->keys));
+      g_assert_cmpuint (sizeof (caps.abs), >=, sizeof (t->abs));
+      g_assert_cmpuint (sizeof (caps.rel), >=, sizeof (t->rel));
+      g_assert_cmpuint (sizeof (caps.ff), >=, sizeof (t->ff));
+      g_assert_cmpuint (sizeof (caps.props), >=, sizeof (t->props));
+
+      memset (&caps, '\0', sizeof (caps));
+      memcpy (caps.ev, t->ev, sizeof (t->ev));
+      memcpy (caps.keys, t->keys, sizeof (t->keys));
+      memcpy (caps.abs, t->abs, sizeof (t->abs));
+      memcpy (caps.rel, t->rel, sizeof (t->rel));
+      memcpy (caps.ff, t->ff, sizeof (t->ff));
+      memcpy (caps.props, t->props, sizeof (t->props));
+
+      for (j = 0; j < G_N_ELEMENTS (caps.ev); j++)
+        caps.ev[j] = GULONG_FROM_LE (caps.ev[j]);
+
+      for (j = 0; j < G_N_ELEMENTS (caps.keys); j++)
+        caps.keys[j] = GULONG_FROM_LE (caps.keys[j]);
+
+      for (j = 0; j < G_N_ELEMENTS (caps.abs); j++)
+        caps.abs[j] = GULONG_FROM_LE (caps.abs[j]);
+
+      for (j = 0; j < G_N_ELEMENTS (caps.rel); j++)
+        caps.rel[j] = GULONG_FROM_LE (caps.rel[j]);
+
+      for (j = 0; j < G_N_ELEMENTS (caps.ff); j++)
+        caps.ff[j] = GULONG_FROM_LE (caps.ff[j]);
+
+      for (j = 0; j < G_N_ELEMENTS (caps.props); j++)
+        caps.props[j] = GULONG_FROM_LE (caps.props[j]);
+
+      _srt_evdev_capabilities_dump (&caps);
+
+      /* Now we can check whether our guess works */
+      actual = _srt_evdev_capabilities_guess_type (&caps);
+      g_assert_cmphex (actual, ==, t->expected);
+    }
+}
+
+static void
+test_input_device_identity_from_hid_uevent (Fixture *f,
+                                            gconstpointer context)
+{
+  static const char text[] =
+    "DRIVER=hid-steam\n"
+    "HID_ID=0003:000028DE:00001142\n"
+    "HID_NAME=Valve Software Steam Controller\n"
+    "HID_PHYS=usb-0000:00:14.0-1.1/input0\n"
+    "HID_UNIQ=serialnumber\n"
+    "MODALIAS=hid:b0003g0001v000028DEp00001142\n";
+  guint32 bus_type, vendor_id, product_id;
+  g_autofree gchar *name = NULL;
+  g_autofree gchar *phys = NULL;
+  g_autofree gchar *uniq = NULL;
+
+  g_assert_true (_srt_get_identity_from_hid_uevent (text,
+                                                    &bus_type,
+                                                    &vendor_id,
+                                                    &product_id,
+                                                    &name,
+                                                    &phys,
+                                                    &uniq));
+  g_assert_cmphex (bus_type, ==, 0x0003);
+  g_assert_cmphex (vendor_id, ==, 0x28de);
+  g_assert_cmphex (product_id, ==, 0x1142);
+  g_assert_cmpstr (name, ==, "Valve Software Steam Controller");
+  g_assert_cmpstr (phys, ==, "usb-0000:00:14.0-1.1/input0");
+  /* Real Steam Controllers don't expose a serial number here, but it's
+   * a better test if we include one */
+  g_assert_cmpstr (uniq, ==, "serialnumber");
+}
+
+#define VENDOR_SONY 0x0268
+#define PRODUCT_SONY_PS3 0x054c
+
+/* These aren't in the real vendor/product IDs, but we add them here
+ * to make the test able to distinguish. They look a bit like HID,
+ * EVDE(v) and USB, if you squint. */
+#define HID_MARKER 0x41D00000
+#define EVDEV_MARKER 0xE7DE0000
+#define USB_MARKER 0x05B00000
+
+/* The test below assumes EV_MAX doesn't increase its value */
+G_STATIC_ASSERT (EV_MAX <= 31);
+/* Same for INPUT_PROP_MAX */
+G_STATIC_ASSERT (INPUT_PROP_MAX <= 31);
+
+static void
+test_input_device_usb (Fixture *f,
+                       gconstpointer context)
+{
+  g_autoptr(MockInputDevice) mock_device = mock_input_device_new ();
+  SrtInputDevice *device = SRT_INPUT_DEVICE (mock_device);
+  SrtSimpleInputDevice *simple = SRT_SIMPLE_INPUT_DEVICE (mock_device);
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+  g_auto(GStrv) udev_properties = NULL;
+  g_autofree gchar *uevent = NULL;
+  g_autofree gchar *hid_uevent = NULL;
+  g_autofree gchar *input_uevent = NULL;
+  g_autofree gchar *usb_uevent = NULL;
+  struct
+  {
+    unsigned int bus_type;
+    unsigned int vendor_id;
+    unsigned int product_id;
+    unsigned int version;
+  } identity = { 1, 1, 1, 1 };
+  struct
+  {
+    unsigned int bus_type;
+    unsigned int vendor_id;
+    unsigned int product_id;
+    const char *name;
+    const char *phys;
+    const char *uniq;
+  } hid_identity = { 1, 1, 1, "x", "x", "x" };
+  struct
+  {
+    unsigned int bus_type;
+    unsigned int vendor_id;
+    unsigned int product_id;
+    unsigned int version;
+    const char *name;
+    const char *phys;
+    const char *uniq;
+  } input_identity = { 1, 1, 1, 1, "x", "x", "x" };
+  struct
+  {
+    unsigned int vendor_id;
+    unsigned int product_id;
+    unsigned int version;
+    const char *manufacturer;
+    const char *product;
+    const char *serial;
+  } usb_identity = { 1, 1, 1, "x", "x", "x" };
+  unsigned long evbits;
+  /* Initialize the first two to nonzero to check that they get zeroed */
+  unsigned long bits[LONGS_FOR_BITS (HIGHEST_EVENT_CODE)] = { 0xa, 0xb };
+  gsize i;
+
+  simple->iface_flags = (SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT
+                              | SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE);
+  simple->dev_node = g_strdup ("/dev/input/event0");
+  simple->sys_path = g_strdup ("/sys/devices/mock/usb/hid/input/input0/event0");
+  simple->subsystem = g_strdup ("input");
+  simple->udev_properties = g_new0 (gchar *, 2);
+  simple->udev_properties[0] = g_strdup ("ID_INPUT_JOYSTICK=1");
+  simple->udev_properties[1] = NULL;
+  simple->uevent = g_strdup ("A=a\nB=b\n");
+  /* This is a semi-realistic PS3 controller. */
+  simple->type_flags = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK;
+  simple->bus_type = BUS_USB;
+  simple->vendor_id = VENDOR_SONY;
+  simple->product_id = PRODUCT_SONY_PS3;
+  simple->version = 0x8111;
+
+  /* We don't set all the bits, just enough to be vaguely realistic */
+  set_bit (EV_KEY, simple->evdev_caps.ev);
+  set_bit (EV_ABS, simple->evdev_caps.ev);
+  set_bit (BTN_A, simple->evdev_caps.keys);
+  set_bit (BTN_B, simple->evdev_caps.keys);
+  set_bit (BTN_TL, simple->evdev_caps.keys);
+  set_bit (BTN_TR, simple->evdev_caps.keys);
+  set_bit (ABS_X, simple->evdev_caps.abs);
+  set_bit (ABS_Y, simple->evdev_caps.abs);
+  set_bit (ABS_RX, simple->evdev_caps.abs);
+  set_bit (ABS_RY, simple->evdev_caps.abs);
+
+  g_debug ("Mock device capabilities:");
+  _srt_evdev_capabilities_dump (&simple->evdev_caps);
+
+  simple->hid_ancestor.sys_path = g_strdup ("/sys/devices/mock/usb/hid");
+  simple->hid_ancestor.uevent = g_strdup ("HID=yes\n");
+  /* The part in square brackets isn't present on the real device, but
+   * makes this test more thorough by letting us distinguish. */
+  simple->hid_ancestor.name = g_strdup ("Sony PLAYSTATION(R)3 Controller [hid]");
+  simple->hid_ancestor.phys = g_strdup ("usb-0000:00:14.0-1/input0");
+  simple->hid_ancestor.uniq = g_strdup ("12:34:56:78:9a:bc");
+  simple->hid_ancestor.bus_type = HID_MARKER | BUS_USB;
+  simple->hid_ancestor.vendor_id = HID_MARKER | VENDOR_SONY;
+  simple->hid_ancestor.product_id = HID_MARKER | PRODUCT_SONY_PS3;
+
+  simple->input_ancestor.sys_path = g_strdup ("/sys/devices/mock/usb/hid/input");
+  simple->input_ancestor.uevent = g_strdup ("INPUT=yes\n");
+  simple->input_ancestor.name = g_strdup ("Sony PLAYSTATION(R)3 Controller [input]");
+  simple->input_ancestor.phys = NULL;
+  simple->input_ancestor.uniq = NULL;
+  simple->input_ancestor.bus_type = EVDEV_MARKER | BUS_USB;
+  simple->input_ancestor.vendor_id = EVDEV_MARKER | VENDOR_SONY;
+  simple->input_ancestor.product_id = EVDEV_MARKER | PRODUCT_SONY_PS3;
+  simple->input_ancestor.version = EVDEV_MARKER | 0x8111;
+
+  simple->usb_device_ancestor.sys_path = g_strdup ("/sys/devices/mock/usb");
+  simple->usb_device_ancestor.uevent = g_strdup ("USB=usb_device\n");
+  simple->usb_device_ancestor.vendor_id = USB_MARKER | VENDOR_SONY;
+  simple->usb_device_ancestor.product_id = USB_MARKER | PRODUCT_SONY_PS3;
+  simple->usb_device_ancestor.device_version = USB_MARKER | 0x0100;
+  simple->usb_device_ancestor.manufacturer = g_strdup ("Sony");
+  simple->usb_device_ancestor.product = g_strdup ("PLAYSTATION(R)3 Controller");
+  simple->usb_device_ancestor.serial = NULL;
+
+  g_assert_cmphex (srt_input_device_get_type_flags (device), ==,
+                   SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK);
+  g_assert_cmpuint (srt_input_device_get_interface_flags (device), ==,
+                    SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT
+                    | SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE);
+  g_assert_cmpstr (srt_input_device_get_dev_node (device), ==,
+                   "/dev/input/event0");
+  g_assert_cmpstr (srt_input_device_get_sys_path (device), ==,
+                   "/sys/devices/mock/usb/hid/input/input0/event0");
+  g_assert_cmpstr (srt_input_device_get_subsystem (device), ==, "input");
+
+  uevent = srt_input_device_dup_uevent (device);
+  g_assert_cmpstr (uevent, ==, "A=a\nB=b\n");
+
+  g_assert_cmpstr (srt_input_device_get_hid_sys_path (device), ==,
+                   "/sys/devices/mock/usb/hid");
+  hid_uevent = srt_input_device_dup_hid_uevent (device);
+  g_assert_cmpstr (hid_uevent, ==, "HID=yes\n");
+
+  g_assert_cmpstr (srt_input_device_get_input_sys_path (device), ==,
+                   "/sys/devices/mock/usb/hid/input");
+  input_uevent = srt_input_device_dup_input_uevent (device);
+  g_assert_cmpstr (input_uevent, ==, "INPUT=yes\n");
+
+  g_assert_cmpstr (srt_input_device_get_usb_device_sys_path (device), ==,
+                   "/sys/devices/mock/usb");
+  usb_uevent = srt_input_device_dup_usb_device_uevent (device);
+  g_assert_cmpstr (usb_uevent, ==, "USB=usb_device\n");
+
+  udev_properties = srt_input_device_dup_udev_properties (device);
+  g_assert_nonnull (udev_properties);
+  g_assert_cmpstr (udev_properties[0], ==, "ID_INPUT_JOYSTICK=1");
+  g_assert_cmpstr (udev_properties[1], ==, NULL);
+
+  g_assert_true (srt_input_device_get_identity (device,
+                                                NULL, NULL, NULL, NULL));
+  g_assert_true (srt_input_device_get_identity (device,
+                                                &identity.bus_type,
+                                                &identity.vendor_id,
+                                                &identity.product_id,
+                                                &identity.version));
+  g_assert_cmphex (identity.bus_type, ==, BUS_USB);
+  g_assert_cmphex (identity.vendor_id, ==, VENDOR_SONY);
+  g_assert_cmphex (identity.product_id, ==, PRODUCT_SONY_PS3);
+  g_assert_cmphex (identity.version, ==, 0x8111);
+
+  g_assert_true (srt_input_device_get_hid_identity (device,
+                                                    NULL, NULL, NULL,
+                                                    NULL, NULL, NULL));
+  g_assert_true (srt_input_device_get_hid_identity (device,
+                                                    &hid_identity.bus_type,
+                                                    &hid_identity.vendor_id,
+                                                    &hid_identity.product_id,
+                                                    &hid_identity.name,
+                                                    &hid_identity.phys,
+                                                    &hid_identity.uniq));
+  g_assert_cmphex (hid_identity.bus_type, ==, HID_MARKER | BUS_USB);
+  g_assert_cmphex (hid_identity.vendor_id, ==, HID_MARKER | VENDOR_SONY);
+  g_assert_cmphex (hid_identity.product_id, ==, HID_MARKER | PRODUCT_SONY_PS3);
+  g_assert_cmpstr (hid_identity.name, ==, "Sony PLAYSTATION(R)3 Controller [hid]");
+  g_assert_cmpstr (hid_identity.phys, ==, "usb-0000:00:14.0-1/input0");
+  g_assert_cmpstr (hid_identity.uniq, ==, "12:34:56:78:9a:bc");
+
+  g_assert_true (srt_input_device_get_input_identity (device,
+                                                      NULL, NULL, NULL, NULL,
+                                                      NULL, NULL, NULL));
+  g_assert_true (srt_input_device_get_input_identity (device,
+                                                      &input_identity.bus_type,
+                                                      &input_identity.vendor_id,
+                                                      &input_identity.product_id,
+                                                      &input_identity.version,
+                                                      &input_identity.name,
+                                                      &input_identity.phys,
+                                                      &input_identity.uniq));
+  g_assert_cmphex (input_identity.bus_type, ==, EVDEV_MARKER | BUS_USB);
+  g_assert_cmphex (input_identity.vendor_id, ==, EVDEV_MARKER | VENDOR_SONY);
+  g_assert_cmphex (input_identity.product_id, ==, EVDEV_MARKER | PRODUCT_SONY_PS3);
+  g_assert_cmphex (input_identity.version, ==, EVDEV_MARKER | 0x8111);
+  g_assert_cmpstr (input_identity.name, ==, "Sony PLAYSTATION(R)3 Controller [input]");
+  g_assert_cmpstr (input_identity.phys, ==, NULL);
+  g_assert_cmpstr (input_identity.uniq, ==, NULL);
+
+  g_assert_true (srt_input_device_get_usb_device_identity (device,
+                                                           NULL, NULL, NULL,
+                                                           NULL, NULL, NULL));
+  g_assert_true (srt_input_device_get_usb_device_identity (device,
+                                                           &usb_identity.vendor_id,
+                                                           &usb_identity.product_id,
+                                                           &usb_identity.version,
+                                                           &usb_identity.manufacturer,
+                                                           &usb_identity.product,
+                                                           &usb_identity.serial));
+  g_assert_cmphex (usb_identity.vendor_id, ==, USB_MARKER | VENDOR_SONY);
+  g_assert_cmphex (usb_identity.product_id, ==, USB_MARKER | PRODUCT_SONY_PS3);
+  g_assert_cmpstr (usb_identity.manufacturer, ==, "Sony");
+  g_assert_cmpstr (usb_identity.product, ==, "PLAYSTATION(R)3 Controller");
+  g_assert_cmpstr (usb_identity.serial, ==, NULL);
+
+  g_debug ("Capabilities internally:");
+  _srt_evdev_capabilities_dump (iface->peek_event_capabilities (device));
+
+  /* This assumes EV_MAX doesn't increase its value */
+  g_assert_cmpuint (srt_input_device_get_event_types (device, NULL, 0),
+                    ==, 1);
+  g_assert_cmpuint (srt_input_device_get_event_types (device, &evbits, 1),
+                    ==, 1);
+  g_assert_cmphex (evbits, ==, simple->evdev_caps.ev[0]);
+  g_assert_cmphex (evbits & (1 << EV_KEY), ==, 1 << EV_KEY);
+  g_assert_cmphex (evbits & (1 << EV_ABS), ==, 1 << EV_ABS);
+  g_assert_cmphex (evbits & (1 << EV_SW), ==, 0);
+  g_assert_cmphex (evbits & (1 << EV_MSC), ==, 0);
+  g_assert_cmpint (srt_input_device_has_event_type (device, EV_KEY), ==, TRUE);
+  g_assert_cmpint (srt_input_device_has_event_type (device, EV_SW), ==, FALSE);
+  g_assert_cmpint (srt_input_device_has_event_capability (device, 0, EV_KEY),
+                   ==, TRUE);
+  g_assert_cmpint (srt_input_device_has_event_capability (device, 0, EV_SW),
+                   ==, FALSE);
+
+  g_assert_cmpuint (srt_input_device_get_event_capabilities (device, 0,
+                                                             bits,
+                                                             G_N_ELEMENTS (bits)),
+                    ==, 1);
+  g_assert_cmphex (bits[0], ==, evbits);
+
+  for (i = 1; i < G_N_ELEMENTS (bits); i++)
+    g_assert_cmphex (bits[i], ==, 0);
+
+  g_assert_cmpuint (srt_input_device_get_event_capabilities (device, EV_KEY,
+                                                             bits,
+                                                             G_N_ELEMENTS (bits)),
+                    >, 1);
+  /* Low KEY_ codes are keyboard keys, which we don't have */
+  g_assert_cmphex (bits[0], ==, 0);
+  g_assert_cmpint (test_bit (BTN_A, bits), ==, 1);
+  g_assert_cmpint (test_bit (BTN_STYLUS, bits), ==, 0);
+  g_assert_cmpint (test_bit (KEY_SEMICOLON, bits), ==, 0);
+  g_assert_cmpmem (bits,
+                   MIN (sizeof (bits), sizeof (simple->evdev_caps.keys)),
+                   simple->evdev_caps.keys,
+                   MIN (sizeof (bits), sizeof (simple->evdev_caps.keys)));
+
+  /* ABS axes also match */
+  g_assert_cmpuint (srt_input_device_get_event_capabilities (device, EV_ABS,
+                                                             bits,
+                                                             G_N_ELEMENTS (bits)),
+                    >=, 1);
+  g_assert_cmpint (test_bit (ABS_X, bits), ==, 1);
+  g_assert_cmpint (test_bit (ABS_Z, bits), ==, 0);
+  g_assert_cmpmem (bits,
+                   MIN (sizeof (bits), sizeof (simple->evdev_caps.abs)),
+                   simple->evdev_caps.abs,
+                   MIN (sizeof (bits), sizeof (simple->evdev_caps.abs)));
+
+  /* REL axes also match (in fact we don't have any, but we still store
+   * the bitfield) */
+  g_assert_cmpuint (srt_input_device_get_event_capabilities (device, EV_REL,
+                                                             bits,
+                                                             G_N_ELEMENTS (bits)),
+                    >=, 1);
+  g_assert_cmpmem (bits,
+                   MIN (sizeof (bits), sizeof (simple->evdev_caps.rel)),
+                   simple->evdev_caps.rel,
+                   MIN (sizeof (bits), sizeof (simple->evdev_caps.rel)));
+
+  /* We don't support EV_SW */
+  g_assert_cmpuint (srt_input_device_get_event_capabilities (device, EV_SW,
+                                                             bits,
+                                                             G_N_ELEMENTS (bits)),
+                    ==, 0);
+
+  for (i = 1; i < G_N_ELEMENTS (bits); i++)
+    g_assert_cmphex (bits[i], ==, 0);
+
+  g_assert_cmpuint (srt_input_device_get_input_properties (device,
+                                                           bits,
+                                                           G_N_ELEMENTS (bits)),
+                    ==, 1);
+  g_assert_cmphex (bits[0], ==, 0);
+  g_assert_cmpint (srt_input_device_has_input_property (device, INPUT_PROP_SEMI_MT),
+                   ==, FALSE);
+
+  for (i = 1; i < G_N_ELEMENTS (bits); i++)
+    g_assert_cmphex (bits[i], ==, 0);
+}
+
+static gboolean
+in_monitor_main_context (Fixture *f)
+{
+  if (f->monitor_context == NULL)
+    return g_main_context_is_owner (g_main_context_default ());
+
+  return g_main_context_is_owner (f->monitor_context);
+}
+
+static void
+device_added_cb (SrtInputDeviceMonitor *monitor,
+                 SrtInputDevice *device,
+                 gpointer user_data)
+{
+  Fixture *f = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autofree gchar *message = NULL;
+  struct
+  {
+    unsigned int bus_type;
+    unsigned int vendor_id;
+    unsigned int product_id;
+    unsigned int version;
+  } identity = { 1, 1, 1, 1 };
+  struct
+  {
+    unsigned int bus_type;
+    unsigned int vendor_id;
+    unsigned int product_id;
+    const char *name;
+    const char *phys;
+    const char *uniq;
+  } hid_identity = { 1, 1, 1, "x", "x", "x" };
+  struct
+  {
+    unsigned int bus_type;
+    unsigned int vendor_id;
+    unsigned int product_id;
+    unsigned int version;
+    const char *name;
+    const char *phys;
+    const char *uniq;
+  } input_identity = { 1, 1, 1, 1, "x", "x", "x" };
+  struct
+  {
+    unsigned int vendor_id;
+    unsigned int product_id;
+    unsigned int version;
+    const char *manufacturer;
+    const char *product;
+    const char *serial;
+  } usb_identity = { 1, 1, 1, "x", "x", "x" };
+  SrtInputDeviceInterfaceFlags iface_flags;
+  int fd;
+
+  g_assert_true (SRT_IS_INPUT_DEVICE_MONITOR (monitor));
+  g_assert_true (SRT_IS_INPUT_DEVICE (device));
+
+  message = g_strdup_printf ("added device: %s",
+                             srt_input_device_get_dev_node (device));
+  g_debug ("%s: %s", G_OBJECT_TYPE_NAME (monitor), message);
+
+  iface_flags = srt_input_device_get_interface_flags (device);
+
+  if (srt_input_device_get_identity (device,
+                                     &identity.bus_type,
+                                     &identity.vendor_id,
+                                     &identity.product_id,
+                                     &identity.version))
+    {
+      g_assert_true (srt_input_device_get_identity (device,
+                                                    NULL, NULL, NULL, NULL));
+    }
+  else
+    {
+      g_assert_false (srt_input_device_get_identity (device,
+                                                     NULL, NULL, NULL, NULL));
+      /* previous contents are untouched */
+      g_assert_cmphex (identity.bus_type, ==, 1);
+      g_assert_cmphex (identity.vendor_id, ==, 1);
+      g_assert_cmphex (identity.product_id, ==, 1);
+      g_assert_cmphex (identity.version, ==, 1);
+    }
+
+  if (srt_input_device_get_hid_identity (device,
+                                         &hid_identity.bus_type,
+                                         &hid_identity.vendor_id,
+                                         &hid_identity.product_id,
+                                         &hid_identity.name,
+                                         &hid_identity.phys,
+                                         &hid_identity.uniq))
+    {
+      g_assert_true (srt_input_device_get_hid_identity (device,
+                                                        NULL, NULL, NULL,
+                                                        NULL, NULL, NULL));
+    }
+  else
+    {
+      g_assert_false (srt_input_device_get_hid_identity (device,
+                                                         NULL, NULL, NULL,
+                                                         NULL, NULL, NULL));
+      /* previous contents are untouched */
+      g_assert_cmphex (hid_identity.bus_type, ==, 1);
+      g_assert_cmphex (hid_identity.vendor_id, ==, 1);
+      g_assert_cmphex (hid_identity.product_id, ==, 1);
+      g_assert_cmpstr (hid_identity.name, ==, "x");
+      g_assert_cmpstr (hid_identity.phys, ==, "x");
+      g_assert_cmpstr (hid_identity.uniq, ==, "x");
+    }
+
+  if (srt_input_device_get_input_identity (device,
+                                           &input_identity.bus_type,
+                                           &input_identity.vendor_id,
+                                           &input_identity.product_id,
+                                           &input_identity.version,
+                                           &input_identity.name,
+                                           &input_identity.phys,
+                                           &input_identity.uniq))
+    {
+      g_assert_true (srt_input_device_get_input_identity (device,
+                                                          NULL, NULL, NULL, NULL,
+                                                          NULL, NULL, NULL));
+    }
+  else
+    {
+      g_assert_false (srt_input_device_get_input_identity (device,
+                                                           NULL, NULL, NULL, NULL,
+                                                           NULL, NULL, NULL));
+      /* previous contents are untouched */
+      g_assert_cmphex (input_identity.bus_type, ==, 1);
+      g_assert_cmphex (input_identity.vendor_id, ==, 1);
+      g_assert_cmphex (input_identity.product_id, ==, 1);
+      g_assert_cmphex (input_identity.version, ==, 1);
+      g_assert_cmpstr (input_identity.name, ==, "x");
+      g_assert_cmpstr (input_identity.phys, ==, "x");
+      g_assert_cmpstr (input_identity.uniq, ==, "x");
+    }
+
+  if (srt_input_device_get_usb_device_identity (device,
+                                                &usb_identity.vendor_id,
+                                                &usb_identity.product_id,
+                                                &usb_identity.version,
+                                                &usb_identity.manufacturer,
+                                                &usb_identity.product,
+                                                &usb_identity.serial))
+    {
+      g_assert_true (srt_input_device_get_usb_device_identity (device,
+                                                               NULL, NULL, NULL,
+                                                               NULL, NULL, NULL));
+    }
+  else
+    {
+      g_assert_false (srt_input_device_get_usb_device_identity (device,
+                                                                NULL, NULL, NULL,
+                                                                NULL, NULL, NULL));
+      /* previous contents are untouched */
+      g_assert_cmphex (usb_identity.vendor_id, ==, 1);
+      g_assert_cmphex (usb_identity.product_id, ==, 1);
+      g_assert_cmphex (usb_identity.version, ==, 1);
+      g_assert_cmpstr (usb_identity.manufacturer, ==, "x");
+      g_assert_cmpstr (usb_identity.product, ==, "x");
+      g_assert_cmpstr (usb_identity.serial, ==, "x");
+    }
+
+  fd = srt_input_device_open (device, O_RDONLY | O_NONBLOCK, &error);
+
+  if (iface_flags & SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE)
+    {
+      g_assert_no_error (error);
+      g_assert_cmpint (fd, >=, 0);
+    }
+  else
+    {
+      g_assert_nonnull (error);
+      g_assert_cmpint (fd, <, 0);
+    }
+
+  glnx_close_fd (&fd);
+  g_clear_error (&error);
+
+  fd = srt_input_device_open (device, O_RDWR | O_NONBLOCK, &error);
+
+  if (iface_flags & SRT_INPUT_DEVICE_INTERFACE_FLAGS_READ_WRITE)
+    {
+      g_assert_no_error (error);
+      g_assert_cmpint (fd, >=, 0);
+    }
+  else
+    {
+      g_assert_nonnull (error);
+      g_assert_cmpint (fd, <, 0);
+    }
+
+  glnx_close_fd (&fd);
+  g_clear_error (&error);
+
+  /* Unsupported flags (currently everything except O_NONBLOCK) are
+   * not allowed */
+  fd = srt_input_device_open (device, O_RDONLY | O_SYNC, &error);
+  g_assert_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT);
+  glnx_close_fd (&fd);
+  g_clear_error (&error);
+
+  /* For the mock device monitor, we know exactly what to expect, so
+   * we can compare the expected log with what actually happened. For
+   * real device monitors, we don't know what's physically present,
+   * so we have to just emit debug messages. */
+  if (f->config->type == MOCK)
+    {
+      g_autofree gchar *uevent = NULL;
+      g_autofree gchar *hid_uevent = NULL;
+      g_autofree gchar *input_uevent = NULL;
+      g_autofree gchar *usb_uevent = NULL;
+      g_auto(GStrv) udev_properties = NULL;
+      unsigned long evbits;
+      unsigned long bits[LONGS_FOR_BITS (HIGHEST_EVENT_CODE)];
+      gsize i;
+
+      g_assert_cmphex (srt_input_device_get_type_flags (device), ==,
+                       SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK);
+      g_assert_cmpuint (srt_input_device_get_interface_flags (device), ==,
+                        SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT
+                        | SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE
+                        | SRT_INPUT_DEVICE_INTERFACE_FLAGS_READ_WRITE);
+
+      g_assert_cmphex (identity.bus_type, ==, BUS_USB);
+      g_assert_cmphex (identity.vendor_id, ==, VENDOR_VALVE);
+      g_assert_cmphex (identity.product_id, ==, PRODUCT_VALVE_STEAM_CONTROLLER);
+      g_assert_cmphex (identity.version, ==, 0x0111);
+
+      g_assert_cmphex (hid_identity.bus_type, ==, HID_MARKER | BUS_USB);
+      g_assert_cmphex (hid_identity.vendor_id, ==, HID_MARKER | VENDOR_VALVE);
+      g_assert_cmphex (hid_identity.product_id, ==, HID_MARKER | PRODUCT_VALVE_STEAM_CONTROLLER);
+      g_assert_cmpstr (hid_identity.name, ==, "Valve Software Steam Controller");
+      g_assert_cmpstr (hid_identity.phys, ==, "[hid]usb-0000:00:14.0-1.2/input1");
+      g_assert_cmpstr (hid_identity.uniq, ==, "");
+
+      g_assert_cmphex (input_identity.bus_type, ==, EVDEV_MARKER | BUS_USB);
+      g_assert_cmphex (input_identity.vendor_id, ==, EVDEV_MARKER | VENDOR_VALVE);
+      g_assert_cmphex (input_identity.product_id, ==, EVDEV_MARKER | PRODUCT_VALVE_STEAM_CONTROLLER);
+      g_assert_cmphex (input_identity.version, ==, EVDEV_MARKER | 0x0111);
+      g_assert_cmpstr (input_identity.name, ==, "Wireless Steam Controller");
+      g_assert_cmpstr (input_identity.phys, ==, "[input]usb-0000:00:14.0-1.2/input1");
+      g_assert_cmpstr (input_identity.uniq, ==, "12345678");
+
+      g_assert_cmphex (usb_identity.vendor_id, ==, USB_MARKER | VENDOR_VALVE);
+      g_assert_cmphex (usb_identity.product_id, ==, USB_MARKER | PRODUCT_VALVE_STEAM_CONTROLLER);
+      g_assert_cmphex (usb_identity.version, ==, USB_MARKER | 0x0001);
+      g_assert_cmpstr (usb_identity.manufacturer, ==, "Valve Software");
+      g_assert_cmpstr (usb_identity.product, ==, "Steam Controller");
+      g_assert_cmpstr (usb_identity.serial, ==, NULL);
+
+      uevent = srt_input_device_dup_uevent (device);
+      g_assert_cmpstr (uevent, ==, "ONE=1\nTWO=2\n");
+
+      udev_properties = srt_input_device_dup_udev_properties (device);
+      g_assert_nonnull (udev_properties);
+      g_assert_cmpstr (udev_properties[0], ==, "ID_INPUT_JOYSTICK=1");
+      g_assert_cmpstr (udev_properties[1], ==, NULL);
+
+      g_assert_cmpstr (srt_input_device_get_hid_sys_path (device), ==,
+                       "/sys/devices/mock/usb/hid");
+      hid_uevent = srt_input_device_dup_hid_uevent (device);
+      g_assert_cmpstr (hid_uevent, ==, "HID=yes\n");
+
+      g_assert_cmpstr (srt_input_device_get_input_sys_path (device), ==,
+                       "/sys/devices/mock/usb/hid/input");
+      input_uevent = srt_input_device_dup_input_uevent (device);
+      g_assert_cmpstr (input_uevent, ==, "INPUT=yes\n");
+
+      g_assert_cmpstr (srt_input_device_get_usb_device_sys_path (device), ==,
+                       "/sys/devices/mock/usb");
+      usb_uevent = srt_input_device_dup_usb_device_uevent (device);
+      g_assert_cmpstr (usb_uevent, ==, "USB=usb_device\n");
+
+      /* This assumes EV_MAX doesn't increase its value */
+      g_assert_cmpuint (srt_input_device_get_event_types (device, NULL, 0),
+                        ==, 1);
+      g_assert_cmpuint (srt_input_device_get_event_types (device, &evbits, 1),
+                        ==, 1);
+      g_assert_cmphex (evbits & (1 << EV_KEY), ==, 1 << EV_KEY);
+      g_assert_cmphex (evbits & (1 << EV_ABS), ==, 1 << EV_ABS);
+      g_assert_cmphex (evbits & (1 << EV_SW), ==, 0);
+      g_assert_cmphex (evbits & (1 << EV_MSC), ==, 0);
+      g_assert_cmpint (srt_input_device_has_event_type (device, EV_KEY), ==, TRUE);
+      g_assert_cmpint (srt_input_device_has_event_type (device, EV_SW), ==, FALSE);
+      g_assert_cmpint (srt_input_device_has_event_capability (device, 0, EV_KEY),
+                       ==, TRUE);
+      g_assert_cmpint (srt_input_device_has_event_capability (device, 0, EV_SW),
+                       ==, FALSE);
+
+      g_assert_cmpuint (srt_input_device_get_event_capabilities (device, 0,
+                                                                 bits,
+                                                                 G_N_ELEMENTS (bits)),
+                        ==, 1);
+      g_assert_cmphex (bits[0], ==, evbits);
+
+      for (i = 1; i < G_N_ELEMENTS (bits); i++)
+        g_assert_cmphex (bits[i], ==, 0);
+
+      g_assert_cmpuint (srt_input_device_get_event_capabilities (device, EV_KEY,
+                                                                 bits,
+                                                                 G_N_ELEMENTS (bits)),
+                        >, 1);
+      /* Low KEY_ codes are keyboard keys, which we don't have */
+      g_assert_cmphex (bits[0], ==, 0);
+      g_assert_cmpint (test_bit (BTN_A, bits), ==, 1);
+      g_assert_cmpint (test_bit (BTN_STYLUS, bits), ==, 0);
+      g_assert_cmpint (test_bit (KEY_SEMICOLON, bits), ==, 0);
+
+      /* ABS axes also match */
+      g_assert_cmpuint (srt_input_device_get_event_capabilities (device, EV_ABS,
+                                                                 bits,
+                                                                 G_N_ELEMENTS (bits)),
+                        >=, 1);
+      g_assert_cmpint (test_bit (ABS_X, bits), ==, 1);
+      g_assert_cmpint (test_bit (ABS_Z, bits), ==, 0);
+
+      /* REL axes also match (in fact we don't have any, but we still store
+       * the bitfield) */
+      g_assert_cmpuint (srt_input_device_get_event_capabilities (device, EV_REL,
+                                                                 bits,
+                                                                 G_N_ELEMENTS (bits)),
+                        >=, 1);
+
+      for (i = 1; i < G_N_ELEMENTS (bits); i++)
+        g_assert_cmphex (bits[i], ==, 0);
+
+      /* We don't support EV_SW */
+      g_assert_cmpuint (srt_input_device_get_event_capabilities (device, EV_SW,
+                                                                 bits,
+                                                                 G_N_ELEMENTS (bits)),
+                        ==, 0);
+
+      for (i = 1; i < G_N_ELEMENTS (bits); i++)
+        g_assert_cmphex (bits[i], ==, 0);
+
+      g_assert_cmpuint (srt_input_device_get_input_properties (device,
+                                                               bits,
+                                                               G_N_ELEMENTS (bits)),
+                        ==, 1);
+      /* The mock implementation unrealistically sets INPUT_PROP_POINTER,
+       * just so we have something nonzero to test against */
+      g_assert_cmphex (bits[0], ==, (1 << INPUT_PROP_POINTER));
+      g_assert_cmpint (srt_input_device_has_input_property (device, INPUT_PROP_POINTER),
+                       ==, TRUE);
+      g_assert_cmpint (srt_input_device_has_input_property (device, INPUT_PROP_SEMI_MT),
+                       ==, FALSE);
+
+      for (i = 1; i < G_N_ELEMENTS (bits); i++)
+        g_assert_cmphex (bits[i], ==, 0);
+
+      g_ptr_array_add (f->log, g_steal_pointer (&message));
+    }
+
+  g_assert_true (in_monitor_main_context (f));
+}
+
+static void
+device_removed_cb (SrtInputDeviceMonitor *monitor,
+                   SrtInputDevice *device,
+                   gpointer user_data)
+{
+  Fixture *f = user_data;
+  g_autofree gchar *message = NULL;
+
+  g_assert_true (SRT_IS_INPUT_DEVICE_MONITOR (monitor));
+  g_assert_true (SRT_IS_INPUT_DEVICE (device));
+
+  message = g_strdup_printf ("removed device: %s",
+                             srt_input_device_get_dev_node (device));
+  g_debug ("%s: %s", G_OBJECT_TYPE_NAME (monitor), message);
+
+  if (f->config->type == MOCK)
+    g_ptr_array_add (f->log, g_steal_pointer (&message));
+
+  g_assert_true (in_monitor_main_context (f));
+}
+
+static void
+all_for_now_cb (SrtInputDeviceMonitor *monitor,
+                gpointer user_data)
+{
+  Fixture *f = user_data;
+
+  g_assert_true (SRT_IS_INPUT_DEVICE_MONITOR (monitor));
+
+  g_ptr_array_add (f->log, g_strdup ("all for now"));
+  g_debug ("%s: %s",
+           G_OBJECT_TYPE_NAME (monitor),
+           (const char *) g_ptr_array_index (f->log, f->log->len - 1));
+
+  g_assert_true (in_monitor_main_context (f));
+}
+
+static gboolean
+done_cb (gpointer user_data)
+{
+  gboolean *done = user_data;
+
+  *done = TRUE;
+  return G_SOURCE_REMOVE;
+}
+
+/* This is the equivalent of g_idle_add() for a non-default main context */
+static guint
+idle_add_in_context (GSourceFunc function,
+                     gpointer data,
+                     GMainContext *context)
+{
+  g_autoptr(GSource) idler = g_idle_source_new ();
+
+  g_source_set_callback (idler, function, data, NULL);
+  return g_source_attach (idler, context);
+}
+
+static SrtInputDeviceMonitor *
+input_device_monitor_new (Fixture *f,
+                          SrtInputDeviceMonitorFlags flags)
+{
+  switch (f->config->type)
+    {
+      case DIRECT:
+        flags |= SRT_INPUT_DEVICE_MONITOR_FLAGS_DIRECT;
+        return srt_input_device_monitor_new (flags);
+
+      case UDEV:
+        flags |= SRT_INPUT_DEVICE_MONITOR_FLAGS_UDEV;
+        return srt_input_device_monitor_new (flags);
+
+      case MOCK:
+      default:
+        return SRT_INPUT_DEVICE_MONITOR (mock_input_device_monitor_new (flags));
+    }
+}
+
+/*
+ * Test the basic behaviour of an input device monitor:
+ * - start
+ * - do initial enumeration
+ * - watch for new devices
+ * - emit signals in the correct main context
+ * - stop
+ */
+static void
+test_input_device_monitor (Fixture *f,
+                           gconstpointer context)
+{
+  g_autoptr(SrtInputDeviceMonitor) monitor = NULL;
+  g_autoptr(GError) error = NULL;
+  gboolean ok;
+  gboolean did_default_idle = FALSE;
+  gboolean did_context_idle = FALSE;
+  guint i;
+
+  if (f->skipped)
+    return;
+
+  f->monitor_context = g_main_context_new ();
+
+  /* To check that the signals get emitted in the correct main-context,
+   * temporarily set a new thread-default main-context while we create
+   * the monitor. */
+  g_main_context_push_thread_default (f->monitor_context);
+    {
+      monitor = input_device_monitor_new (f, SRT_INPUT_DEVICE_MONITOR_FLAGS_NONE);
+    }
+  g_main_context_pop_thread_default (f->monitor_context);
+
+  g_assert_nonnull (monitor);
+
+  srt_input_device_monitor_request_evdev (monitor);
+  srt_input_device_monitor_request_raw_hid (monitor);
+
+  g_signal_connect (monitor, "added", G_CALLBACK (device_added_cb), f);
+  g_signal_connect (monitor, "removed", G_CALLBACK (device_removed_cb), f);
+  g_signal_connect (monitor, "all-for-now", G_CALLBACK (all_for_now_cb), f);
+
+  /* Note that the signals are emitted in the main-context that was
+   * thread-default at the time we created the object, not the
+   * main-context that called start(). */
+  ok = srt_input_device_monitor_start (monitor, &error);
+  g_debug ("start() returned");
+  g_assert_no_error (error);
+  g_assert_true (ok);
+  g_ptr_array_add (f->log, g_strdup ("start() returned"));
+
+  g_idle_add (done_cb, &did_default_idle);
+  idle_add_in_context (done_cb, &did_context_idle, f->monitor_context);
+
+  i = 0;
+
+  g_assert_cmpuint (f->log->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (f->log, i++), ==,
+                   "start() returned");
+  /* There's nothing else in the log yet */
+  g_assert_cmpuint (f->log->len, ==, i);
+
+  /* Iterating the default main context does not deliver signals */
+  while (!did_default_idle)
+    g_main_context_iteration (NULL, TRUE);
+
+  g_assert_cmpuint (f->log->len, ==, i);
+
+  /* Iterating the main context that was thread-default at the time we
+   * constructed the monitor *does* deliver signals */
+  while (!did_context_idle)
+    g_main_context_iteration (f->monitor_context, TRUE);
+
+  /* For the mock device monitor, we can predict which devices will be added,
+   * so we log them and assert about them. For real device monitors we
+   * can't reliably do this. */
+  if (f->config->type == MOCK)
+    {
+      g_assert_cmpuint (f->log->len, >, i);
+      g_assert_cmpstr (g_ptr_array_index (f->log, i++), ==,
+                       "added device: /dev/input/event0");
+    }
+
+  g_assert_cmpuint (f->log->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (f->log, i++), ==,
+                   "all for now");
+
+  if (f->config->type == MOCK)
+    {
+      g_assert_cmpuint (f->log->len, >, i);
+      g_assert_cmpstr (g_ptr_array_index (f->log, i++), ==,
+                       "added device: /dev/input/event-connected-briefly");
+      g_assert_cmpuint (f->log->len, >, i);
+      g_assert_cmpstr (g_ptr_array_index (f->log, i++), ==,
+                       "removed device: /dev/input/event-connected-briefly");
+    }
+
+  g_assert_cmpuint (f->log->len, ==, i);
+
+  /* Explicitly stop it here. We test not explicitly stopping in the
+   * other test-case */
+  srt_input_device_monitor_stop (monitor);
+
+  /* It's possible that not all the memory used is freed until we have
+   * iterated the main-context one last time */
+  did_context_idle = FALSE;
+  idle_add_in_context (done_cb, &did_context_idle, f->monitor_context);
+
+  while (!did_context_idle)
+    g_main_context_iteration (f->monitor_context, TRUE);
+}
+
+/*
+ * Test things we couldn't test in the previous test-case:
+ * - the ONCE flag, which disables monitoring
+ * - using our thread-default main-context throughout
+ */
+static void
+test_input_device_monitor_once (Fixture *f,
+                                gconstpointer context)
+{
+  g_autoptr(SrtInputDeviceMonitor) monitor = NULL;
+  g_autoptr(GError) error = NULL;
+  gboolean ok;
+  gboolean done = FALSE;
+  guint i;
+
+  if (f->skipped)
+    return;
+
+  monitor = input_device_monitor_new (f, SRT_INPUT_DEVICE_MONITOR_FLAGS_ONCE);
+  g_assert_nonnull (monitor);
+
+  srt_input_device_monitor_request_evdev (monitor);
+  srt_input_device_monitor_request_raw_hid (monitor);
+
+  g_signal_connect (monitor, "added", G_CALLBACK (device_added_cb), f);
+  g_signal_connect (monitor, "removed", G_CALLBACK (device_removed_cb), f);
+  g_signal_connect (monitor, "all-for-now", G_CALLBACK (all_for_now_cb), f);
+
+  ok = srt_input_device_monitor_start (monitor, &error);
+  g_debug ("start() returned");
+  g_assert_no_error (error);
+  g_assert_true (ok);
+  g_ptr_array_add (f->log, g_strdup ("start() returned"));
+
+  g_idle_add (done_cb, &done);
+
+  while (!done)
+    g_main_context_iteration (NULL, TRUE);
+
+  i = 0;
+
+  /* Because the same main context was the thread-default at the
+   * time we created the object and at the time we called start(),
+   * the first batch of signals arrive even before start() has returned. */
+  if (f->config->type == MOCK)
+    {
+      g_assert_cmpuint (f->log->len, >, i);
+      g_assert_cmpstr (g_ptr_array_index (f->log, i++), ==,
+                       "added device: /dev/input/event0");
+    }
+
+  g_assert_cmpuint (f->log->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (f->log, i++), ==,
+                   "all for now");
+  g_assert_cmpuint (f->log->len, >, i);
+  g_assert_cmpstr (g_ptr_array_index (f->log, i++), ==,
+                   "start() returned");
+  g_assert_cmpuint (f->log->len, ==, i);
+
+  /* Don't explicitly stop it here. We test explicitly stopping in the
+   * other test-case */
+  g_clear_object (&monitor);
+
+  /* It's possible that not all the memory used is freed until we have
+   * iterate the main-context one last time */
+  done = FALSE;
+  g_idle_add (done_cb, &done);
+
+  while (!done)
+    g_main_context_iteration (NULL, TRUE);
+}
+
+int
+main (int argc,
+      char **argv)
+{
+  argv0 = argv[0];
+  g_test_init (&argc, &argv, NULL);
+
+  g_test_add ("/input-device/from-json", Fixture, NULL,
+              setup, test_input_device_from_json, teardown);
+  g_test_add ("/input-device/guess", Fixture, NULL,
+              setup, test_input_device_guess, teardown);
+  g_test_add ("/input-device/identity-from-hid-uevent", Fixture, NULL,
+              setup, test_input_device_identity_from_hid_uevent, teardown);
+  g_test_add ("/input-device/usb", Fixture, NULL,
+              setup, test_input_device_usb, teardown);
+  g_test_add ("/input-device/monitor/mock", Fixture, NULL,
+              setup, test_input_device_monitor, teardown);
+  g_test_add ("/input-device/monitor-once/mock", Fixture, NULL,
+              setup, test_input_device_monitor_once, teardown);
+  g_test_add ("/input-device/monitor/direct", Fixture, &direct_config,
+              setup, test_input_device_monitor, teardown);
+  g_test_add ("/input-device/monitor-once/direct", Fixture, &direct_config,
+              setup, test_input_device_monitor_once, teardown);
+  g_test_add ("/input-device/monitor/udev", Fixture, &udev_config,
+              setup, test_input_device_monitor, teardown);
+  g_test_add ("/input-device/monitor-once/udev", Fixture, &udev_config,
+              setup, test_input_device_monitor_once, teardown);
+
+  return g_test_run ();
+}
diff --git a/tests/input-monitor-outputs/no-details.json b/tests/input-monitor-outputs/no-details.json
new file mode 100644
index 0000000000000000000000000000000000000000..dcff69967f9ec1fce40023be4e6b45b92c7ec37d
--- /dev/null
+++ b/tests/input-monitor-outputs/no-details.json
@@ -0,0 +1 @@
+{"added" : {}}
diff --git a/tests/input-monitor-outputs/odd.json b/tests/input-monitor-outputs/odd.json
new file mode 100644
index 0000000000000000000000000000000000000000..ab91a8b321a08e2325ec172a2a822d234f572652
--- /dev/null
+++ b/tests/input-monitor-outputs/odd.json
@@ -0,0 +1,36 @@
+{
+  "added" : {
+    "interface_flags" : [
+      "raw-hid",
+      "__future__"
+    ],
+    "type_flags" : [
+      "__future__"
+    ],
+    "evdev" : {
+      "raw_abs" : "01 02 03 04 05 06 07 08  ff ff ff ff",
+      "raw_input_properties" : "12 34 56 78 87 65 43 21  ff ff ff ff"
+    },
+    "hid_ancestor" : {
+      "sys_path" : "/...",
+      "name" : "Acme Weird Device",
+      "bus_type" : "0xfff1",
+      "vendor_id" : "0xfff1",
+      "product_id" : "0xfff1",
+      "uniq" : "12345678"
+    },
+    "input_ancestor" : {
+      "sys_path" : "/...",
+      "bus_type" : "0xfff2",
+      "vendor_id" : "0xfff2",
+      "product_id" : "0xfff2",
+      "uniq" : "1234-5678"
+    },
+    "usb_device_ancestor" : {
+      "sys_path" : "/...",
+      "vendor_id" : "0xfff3",
+      "product_id" : "0xfff3",
+      "serial" : "12:34:56:78"
+    }
+  }
+}
diff --git a/tests/input-monitor-outputs/steam-controller.json b/tests/input-monitor-outputs/steam-controller.json
new file mode 100644
index 0000000000000000000000000000000000000000..6065ff8eef79781a3165ae38feef91ab77409a1c
--- /dev/null
+++ b/tests/input-monitor-outputs/steam-controller.json
@@ -0,0 +1,155 @@
+{
+  "added" : {
+    "interface_flags" : [
+      "event"
+    ],
+    "type_flags" : [
+      "keyboard",
+      "has-keys",
+      "mouse"
+    ],
+    "dev_node" : "/dev/input/event20",
+    "subsystem" : "input",
+    "sys_path" : "/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.1/1-1.1:1.0/0003:28DE:1142.00DD/input/input308/event20",
+    "bus_type" : "0x0003",
+    "vendor_id" : "0x28de",
+    "product_id" : "0x1142",
+    "version" : "0x0111",
+    "evdev" : {
+      "types" : [
+        "SYN",
+        "KEY",
+        "REL",
+        "MSC",
+        "LED",
+        "REP"
+      ],
+      "raw_types" : "17 00 12 00 00 00 00 00  ",
+      "absolute_axes" : [],
+      "raw_abs" : "00 00 00 00 00 00 00 00  ",
+      "relative_axes" : [
+        "X",
+        "Y",
+        "WHEEL",
+        "WHEEL_HI_RES"
+      ],
+      "raw_rel" : "03 09 00 00 00 00 00 00  ",
+      "keys" : [
+        "KEY_ESC",
+        "KEY_0",
+        "KEY_A",
+        "KEY_KP0",
+        "BTN_MOUSE",
+        "BTN_RIGHT",
+        "BTN_MIDDLE",
+        "BTN_SIDE",
+        "BTN_EXTRA"
+      ],
+      "raw_keys" : "fe ff ff ff ff ff ff ff  ff ff cf 01 df ff 80 e0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  00 00 1f 00 00 00 00 00  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ",
+      "input_properties" : [],
+      "raw_input_properties" : "00 00 00 00 00 00 00 00  ",
+      "guessed_type_flags" : [
+        "keyboard",
+        "has-keys",
+        "mouse"
+      ]
+    },
+    "udev_properties" : [
+      "BACKSPACE=guess",
+      "DEVLINKS=/dev/input/by-id/usb-Valve_Software_Steam_Controller-event-mouse /dev/input/by-path/pci-0000:00:14.0-usb-0:1.1:1.0-event-mouse",
+      "DEVNAME=/dev/input/event20",
+      "DEVPATH=/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.1/1-1.1:1.0/0003:28DE:1142.00DD/input/input308/event20",
+      "ID_BUS=usb",
+      "ID_INPUT=1",
+      "ID_INPUT_KEY=1",
+      "ID_INPUT_KEYBOARD=1",
+      "ID_INPUT_MOUSE=1",
+      "ID_MODEL=Steam_Controller",
+      "ID_MODEL_ENC=Steam\\x20Controller",
+      "ID_MODEL_ID=1142",
+      "ID_PATH=pci-0000:00:14.0-usb-0:1.1:1.0",
+      "ID_PATH_TAG=pci-0000_00_14_0-usb-0_1_1_1_0",
+      "ID_REVISION=0001",
+      "ID_SERIAL=Valve_Software_Steam_Controller",
+      "ID_TYPE=hid",
+      "ID_USB_DRIVER=usbhid",
+      "ID_USB_INTERFACES=:030101:030000:",
+      "ID_USB_INTERFACE_NUM=00",
+      "ID_VENDOR=Valve_Software",
+      "ID_VENDOR_ENC=Valve\\x20Software",
+      "ID_VENDOR_ID=28de",
+      "LIBINPUT_DEVICE_GROUP=3/28de/1142:usb-0000:00:14.0-1",
+      "MAJOR=13",
+      "MINOR=84",
+      "SUBSYSTEM=input",
+      "TAGS=:power-switch:",
+      "USEC_INITIALIZED=1162877255953",
+      "XKBLAYOUT=gb",
+      "XKBMODEL=pc105"
+    ],
+    "uevent" : [
+      "MAJOR=13",
+      "MINOR=84",
+      "DEVNAME=input/event20"
+    ],
+    "hid_ancestor" : {
+      "sys_path" : "/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.1/1-1.1:1.0/0003:28DE:1142.00DD",
+      "name" : "Valve Software Steam Controller",
+      "bus_type" : "0x0003",
+      "vendor_id" : "0x28de",
+      "product_id" : "0x1142",
+      "uniq" : "",
+      "phys" : "usb-0000:00:14.0-1.1/input0",
+      "uevent" : [
+        "DRIVER=hid-steam",
+        "HID_ID=0003:000028DE:00001142",
+        "HID_NAME=Valve Software Steam Controller",
+        "HID_PHYS=usb-0000:00:14.0-1.1/input0",
+        "HID_UNIQ=",
+        "MODALIAS=hid:b0003g0001v000028DEp00001142"
+      ]
+    },
+    "input_ancestor" : {
+      "sys_path" : "/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.1/1-1.1:1.0/0003:28DE:1142.00DD/input/input308",
+      "name" : "Valve Software Steam Controller",
+      "bus_type" : "0x0003",
+      "vendor_id" : "0x28de",
+      "product_id" : "0x1142",
+      "version" : "0x0111",
+      "phys" : "usb-0000:00:14.0-1.1/input0",
+      "uevent" : [
+        "PRODUCT=3/28de/1142/111",
+        "NAME=\"Valve Software Steam Controller\"",
+        "PHYS=\"usb-0000:00:14.0-1.1/input0\"",
+        "UNIQ=\"\"",
+        "PROP=0",
+        "EV=120017",
+        "KEY=1f0000 0 0 e080ffdf01cfffff fffffffffffffffe",
+        "REL=903",
+        "MSC=10",
+        "LED=1f",
+        "MODALIAS=input:b0003v28DEp1142e0111-e0,1,2,4,11,14,k77,7D,7E,7F,110,111,112,113,114,r0,1,8,B,am4,l0,1,2,3,4,sfw"
+      ]
+    },
+    "usb_device_ancestor" : {
+      "sys_path" : "/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.1",
+      "vendor_id" : "0x28de",
+      "product_id" : "0x1142",
+      "version" : "0x0001",
+      "manufacturer" : "Valve Software",
+      "product" : "Steam Controller",
+      "serial" : null,
+      "uevent" : [
+        "MAJOR=189",
+        "MINOR=66",
+        "DEVNAME=bus/usb/001/067",
+        "DEVTYPE=usb_device",
+        "DRIVER=usb",
+        "PRODUCT=28de/1142/1",
+        "TYPE=0/0/0",
+        "BUSNUM=001",
+        "DEVNUM=067"
+      ]
+    }
+  }
+}
diff --git a/tests/meson.build b/tests/meson.build
index b2d5b1db772a355ebc60b5adc754d730136059b9..f23d73fbb8c48f68608f072e66c3e19f79bad610 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -31,6 +31,15 @@ tests = [
   {'name': 'architecture'},
   {'name': 'desktop-entry'},
   {'name': 'graphics'},
+  {
+    'name': 'input-device',
+    'static': true,
+    'sources': [
+      'input-device.c',
+      'mock-input-device.c',
+      'mock-input-device.h',
+    ],
+  },
   {'name': 'json-utils', 'static': true},
   {'name': 'library'},
   {'name': 'locale'},
@@ -104,6 +113,7 @@ install_subdir('expectations_with_missings', install_dir : tests_dir)
 # Vulkan *.json files. (The order of EGL *.json files is well-defined.)
 install_subdir('fake-icds', install_dir : tests_dir)
 install_subdir('fake-steam-runtime', install_dir : tests_dir)
+install_subdir('input-monitor-output', install_dir : tests_dir)
 install_subdir('json-report', install_dir : tests_dir)
 
 meson.add_postconf_script(
@@ -153,9 +163,11 @@ foreach test_info : tests
     deps = [libsteamrt_dep, test_utils_dep]
   endif
 
+  sources = test_info.get('sources', files(test_name + '.c'))
+
   exe = executable(
     'test-' + test_name,
-    files(test_name + '.c'),
+    sources,
     c_args : [
       '-D_SRT_MULTIARCH="' + multiarch + '"',
     ],
diff --git a/tests/mock-input-device.c b/tests/mock-input-device.c
new file mode 100644
index 0000000000000000000000000000000000000000..123e411f73e3f17036f531a334f4859e1386e498
--- /dev/null
+++ b/tests/mock-input-device.c
@@ -0,0 +1,450 @@
+/*
+ * Mock input device monitor, loosely based on SDL code.
+ *
+ * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org>
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: Zlib
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ */
+
+#include "mock-input-device.h"
+
+#include <linux/input.h>
+
+#include <glib-unix.h>
+#include <libglnx.h>
+
+#include "steam-runtime-tools/glib-backports-internal.h"
+#include "steam-runtime-tools/input-device.h"
+#include "steam-runtime-tools/input-device-internal.h"
+#include "steam-runtime-tools/utils-internal.h"
+
+#define VENDOR_VALVE 0x28de
+#define PRODUCT_VALVE_STEAM_CONTROLLER 0x1142
+
+/* These aren't in the real vendor/product IDs, but we add them here
+ * to make the test able to distinguish. They look a bit like HID,
+ * EVDE(v) and USB, if you squint. */
+#define HID_MARKER 0x41D00000
+#define EVDEV_MARKER 0xE7DE0000
+#define USB_MARKER 0x05B00000
+
+static void mock_input_device_iface_init (SrtInputDeviceInterface *iface);
+static void mock_input_device_monitor_iface_init (SrtInputDeviceMonitorInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (MockInputDevice,
+                         mock_input_device,
+                         SRT_TYPE_SIMPLE_INPUT_DEVICE,
+                         G_IMPLEMENT_INTERFACE (SRT_TYPE_INPUT_DEVICE,
+                                                mock_input_device_iface_init))
+
+G_DEFINE_TYPE_WITH_CODE (MockInputDeviceMonitor,
+                         mock_input_device_monitor,
+                         G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (SRT_TYPE_INPUT_DEVICE_MONITOR,
+                                                mock_input_device_monitor_iface_init))
+
+static void
+mock_input_device_init (MockInputDevice *self)
+{
+}
+
+static void
+mock_input_device_class_init (MockInputDeviceClass *cls)
+{
+}
+
+static int
+mock_input_device_open_device (SrtInputDevice *device,
+                               int flags,
+                               GError **error)
+{
+  SrtInputDeviceInterfaceFlags iface_flags;
+  const char *devnode = NULL;
+  int fd = -1;
+
+  if (!_srt_input_device_check_open_flags (flags, error))
+    return -1;
+
+  iface_flags = srt_input_device_get_interface_flags (device);
+  devnode = srt_input_device_get_dev_node (device);
+
+  if (devnode == NULL)
+    {
+      glnx_throw (error, "Device has no device node");
+      return -1;
+    }
+
+  /* We aren't really going to open the device node, so provide a somewhat
+   * realistic permissions check. */
+  switch (flags & (O_RDONLY | O_RDWR | O_WRONLY))
+    {
+      case O_RDONLY:
+        if (!(iface_flags & SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE))
+          {
+            g_set_error (error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED,
+                         "Device node cannot be read");
+            return -1;
+          }
+
+        break;
+
+      case O_RDWR:
+      case O_WRONLY:
+        if (!(iface_flags & SRT_INPUT_DEVICE_INTERFACE_FLAGS_READ_WRITE))
+          {
+            g_set_error (error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED,
+                         "Device node cannot be written");
+            return -1;
+          }
+
+        break;
+
+      default:
+        /* _srt_input_device_check_open_flags should have caught this */
+        g_return_val_if_reached (-1);
+    }
+
+  /* This is a mock device, so open /dev/null instead of the real
+   * device node. */
+  fd = open ("/dev/null", flags | _SRT_INPUT_DEVICE_ALWAYS_OPEN_FLAGS);
+
+  if (fd < 0)
+    {
+      glnx_throw_errno_prefix (error,
+                               "Unable to open device node \"%s\"",
+                               devnode);
+      return -1;
+    }
+
+  return fd;
+}
+
+static void
+mock_input_device_iface_init (SrtInputDeviceInterface *iface)
+{
+#define IMPLEMENT(x) iface->x = mock_input_device_ ## x
+
+  IMPLEMENT (open_device);
+
+#undef IMPLEMENT
+}
+
+MockInputDevice *
+mock_input_device_new (void)
+{
+  return g_object_new (MOCK_TYPE_INPUT_DEVICE,
+                       NULL);
+}
+
+typedef enum
+{
+  PROP_0,
+  PROP_FLAGS,
+  PROP_IS_ACTIVE,
+  N_PROPERTIES
+} Property;
+
+static void
+mock_input_device_monitor_init (MockInputDeviceMonitor *self)
+{
+  self->monitor_context = g_main_context_ref_thread_default ();
+  self->state = NOT_STARTED;
+  self->devices = g_hash_table_new_full (NULL,
+                                         NULL,
+                                         g_object_unref,
+                                         NULL);
+}
+
+static void
+mock_input_device_monitor_get_property (GObject *object,
+                                        guint prop_id,
+                                        GValue *value,
+                                        GParamSpec *pspec)
+{
+  MockInputDeviceMonitor *self = MOCK_INPUT_DEVICE_MONITOR (object);
+
+  switch ((Property) prop_id)
+    {
+      case PROP_IS_ACTIVE:
+        g_value_set_boolean (value, (self->state == STARTED));
+        break;
+
+      case PROP_FLAGS:
+        g_value_set_flags (value, self->flags);
+        break;
+
+      case PROP_0:
+      case N_PROPERTIES:
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+mock_input_device_monitor_set_property (GObject *object,
+                                        guint prop_id,
+                                        const GValue *value,
+                                        GParamSpec *pspec)
+{
+  MockInputDeviceMonitor *self = MOCK_INPUT_DEVICE_MONITOR (object);
+
+  switch ((Property) prop_id)
+    {
+      case PROP_FLAGS:
+        /* Construct-only */
+        g_return_if_fail (self->flags == 0);
+        self->flags = g_value_get_flags (value);
+        break;
+
+      case PROP_IS_ACTIVE:
+      case PROP_0:
+      case N_PROPERTIES:
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+mock_input_device_monitor_dispose (GObject *object)
+{
+  srt_input_device_monitor_stop (SRT_INPUT_DEVICE_MONITOR (object));
+  G_OBJECT_CLASS (mock_input_device_monitor_parent_class)->dispose (object);
+}
+
+static void
+mock_input_device_monitor_finalize (GObject *object)
+{
+  srt_input_device_monitor_stop (SRT_INPUT_DEVICE_MONITOR (object));
+  G_OBJECT_CLASS (mock_input_device_monitor_parent_class)->finalize (object);
+}
+
+static void
+mock_input_device_monitor_class_init (MockInputDeviceMonitorClass *cls)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (cls);
+
+  object_class->get_property = mock_input_device_monitor_get_property;
+  object_class->set_property = mock_input_device_monitor_set_property;
+  object_class->dispose = mock_input_device_monitor_dispose;
+  object_class->finalize = mock_input_device_monitor_finalize;
+
+  g_object_class_override_property (object_class, PROP_IS_ACTIVE, "is-active");
+  g_object_class_override_property (object_class, PROP_FLAGS, "flags");
+}
+
+MockInputDeviceMonitor *
+mock_input_device_monitor_new (SrtInputDeviceMonitorFlags flags)
+{
+  return g_object_new (MOCK_TYPE_INPUT_DEVICE_MONITOR,
+                       "flags", flags,
+                       NULL);
+}
+
+void
+mock_input_device_monitor_add (MockInputDeviceMonitor *self,
+                               MockInputDevice *device)
+{
+  /* Ref it first in case the last ref to it is in the hash table */
+  g_autoptr(MockInputDevice) ref = g_object_ref (device);
+
+  mock_input_device_monitor_remove (self, device);
+
+  g_hash_table_add (self->devices, g_steal_pointer (&ref));
+
+  _srt_input_device_monitor_emit_added (SRT_INPUT_DEVICE_MONITOR (self),
+                                        SRT_INPUT_DEVICE (device));
+}
+
+void
+mock_input_device_monitor_remove (MockInputDeviceMonitor *self,
+                                  MockInputDevice *device)
+{
+  if (g_hash_table_steal (self->devices, device))
+    {
+      _srt_input_device_monitor_emit_removed (SRT_INPUT_DEVICE_MONITOR (self),
+                                              SRT_INPUT_DEVICE (device));
+      g_object_unref (device);
+    }
+}
+
+static void
+add_steam_controller (MockInputDeviceMonitor *self,
+                      const char *tail,
+                      MockInputDevice **dev_out)
+{
+  g_autoptr(MockInputDevice) mock = mock_input_device_new ();
+  SrtSimpleInputDevice *device = SRT_SIMPLE_INPUT_DEVICE (mock);
+  g_autoptr(GPtrArray) arr = g_ptr_array_new_full (1, g_free);
+
+  device->iface_flags = (SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT
+                         | SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE
+                         | SRT_INPUT_DEVICE_INTERFACE_FLAGS_READ_WRITE);
+  device->dev_node = g_strdup_printf ("/dev/input/event%s", tail);
+  device->sys_path = g_strdup_printf ("/sys/devices/mock/usb/hid/input/input0/event%s",
+                                      tail);
+  device->subsystem = g_strdup ("input");
+  device->uevent = g_strdup ("ONE=1\nTWO=2\n");
+
+  device->hid_ancestor.sys_path = g_strdup ("/sys/devices/mock/usb/hid");
+  device->hid_ancestor.uevent = g_strdup ("HID=yes\n");
+
+  device->input_ancestor.sys_path = g_strdup ("/sys/devices/mock/usb/hid/input");
+  device->input_ancestor.uevent = g_strdup ("INPUT=yes\n");
+
+  device->usb_device_ancestor.sys_path = g_strdup ("/sys/devices/mock/usb");
+  device->usb_device_ancestor.uevent = g_strdup ("USB=usb_device\n");
+
+  g_ptr_array_add (arr, g_strdup ("ID_INPUT_JOYSTICK=1"));
+  g_ptr_array_add (arr, NULL);
+  device->udev_properties = (gchar **) g_ptr_array_free (g_steal_pointer (&arr), FALSE);
+
+  /* This is a semi-realistic Steam Controller. */
+  device->type_flags = SRT_INPUT_DEVICE_TYPE_FLAGS_JOYSTICK;
+  device->bus_type = BUS_USB;
+  device->vendor_id = VENDOR_VALVE;
+  device->product_id = PRODUCT_VALVE_STEAM_CONTROLLER;
+  device->version = 0x0111;
+  /* We don't set all the bits, just enough to be vaguely realistic */
+  set_bit (EV_KEY, device->evdev_caps.ev);
+  set_bit (EV_ABS, device->evdev_caps.ev);
+  set_bit (BTN_A, device->evdev_caps.keys);
+  set_bit (BTN_B, device->evdev_caps.keys);
+  set_bit (BTN_X, device->evdev_caps.keys);
+  set_bit (BTN_Y, device->evdev_caps.keys);
+  set_bit (ABS_X, device->evdev_caps.abs);
+  set_bit (ABS_Y, device->evdev_caps.abs);
+  set_bit (ABS_RX, device->evdev_caps.abs);
+  set_bit (ABS_RY, device->evdev_caps.abs);
+  /* This is unrealistic, but it's hard to test the properties if their
+   * value is zero */
+  set_bit (INPUT_PROP_POINTER, device->evdev_caps.props);
+
+  /* The part in square brackets isn't present on the real device, but
+   * makes this test more thorough by letting us distinguish. */
+  device->hid_ancestor.name = g_strdup ("Valve Software Steam Controller");
+  device->hid_ancestor.phys = g_strdup ("[hid]usb-0000:00:14.0-1.2/input1");
+  device->hid_ancestor.uniq = g_strdup ("");
+  device->hid_ancestor.bus_type = HID_MARKER | BUS_USB;
+  device->hid_ancestor.vendor_id = HID_MARKER | VENDOR_VALVE;
+  device->hid_ancestor.product_id = HID_MARKER | PRODUCT_VALVE_STEAM_CONTROLLER;
+
+  device->input_ancestor.name = g_strdup ("Wireless Steam Controller");
+  device->input_ancestor.phys = g_strdup ("[input]usb-0000:00:14.0-1.2/input1");
+  device->input_ancestor.uniq = g_strdup ("12345678");
+  device->input_ancestor.bus_type = EVDEV_MARKER | BUS_USB;
+  device->input_ancestor.vendor_id = EVDEV_MARKER | VENDOR_VALVE;
+  device->input_ancestor.product_id = EVDEV_MARKER | PRODUCT_VALVE_STEAM_CONTROLLER;
+  device->input_ancestor.version = EVDEV_MARKER | 0x0111;
+
+  device->usb_device_ancestor.vendor_id = USB_MARKER | VENDOR_VALVE;
+  device->usb_device_ancestor.product_id = USB_MARKER | PRODUCT_VALVE_STEAM_CONTROLLER;
+  device->usb_device_ancestor.device_version = USB_MARKER | 0x0001;
+  device->usb_device_ancestor.manufacturer = g_strdup ("Valve Software");
+  device->usb_device_ancestor.product = g_strdup ("Steam Controller");
+  device->usb_device_ancestor.serial = NULL;
+
+  mock_input_device_monitor_add (self, mock);
+
+  if (dev_out != NULL)
+    *dev_out = g_steal_pointer (&mock);
+}
+
+static gboolean
+briefly_add_steam_controller_in_idle_cb (gpointer user_data)
+{
+  MockInputDeviceMonitor *self = MOCK_INPUT_DEVICE_MONITOR (user_data);
+  g_autoptr(MockInputDevice) device = NULL;
+
+  add_steam_controller (self, "-connected-briefly", &device);
+  mock_input_device_monitor_remove (self, device);
+  return G_SOURCE_REMOVE;
+}
+
+static gboolean
+enumerate_cb (gpointer user_data)
+{
+  MockInputDeviceMonitor *self = MOCK_INPUT_DEVICE_MONITOR (user_data);
+
+  add_steam_controller (self, "0", NULL);
+  _srt_input_device_monitor_emit_all_for_now (SRT_INPUT_DEVICE_MONITOR (self));
+  return G_SOURCE_REMOVE;
+}
+
+static gboolean
+mock_input_device_monitor_start (SrtInputDeviceMonitor *monitor,
+                                 GError **error)
+{
+  MockInputDeviceMonitor *self = MOCK_INPUT_DEVICE_MONITOR (monitor);
+
+  g_return_val_if_fail (MOCK_IS_INPUT_DEVICE_MONITOR (monitor), FALSE);
+  g_return_val_if_fail (self->state == NOT_STARTED, FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+  self->state = STARTED;
+
+  /* Make sure the signals for the initial batch of devices are emitted in
+   * the correct main-context */
+  g_main_context_invoke_full (self->monitor_context, G_PRIORITY_DEFAULT,
+                              enumerate_cb, g_object_ref (self),
+                              g_object_unref);
+
+  if ((self->flags & SRT_INPUT_DEVICE_MONITOR_FLAGS_ONCE) == 0)
+    {
+      GSource *source = g_idle_source_new ();
+
+      g_source_set_callback (source,
+                             briefly_add_steam_controller_in_idle_cb,
+                             self, NULL);
+      g_source_set_priority (source, G_PRIORITY_DEFAULT);
+      g_source_attach (source, self->monitor_context);
+      self->sources = g_list_prepend (self->sources, g_steal_pointer (&source));
+    }
+
+  return TRUE;
+}
+
+static void
+mock_input_device_monitor_stop (SrtInputDeviceMonitor *monitor)
+{
+  MockInputDeviceMonitor *self = MOCK_INPUT_DEVICE_MONITOR (monitor);
+
+  self->state = STOPPED;
+
+  while (self->sources != NULL)
+    {
+      GSource *s = self->sources->data;
+
+      self->sources = g_list_delete_link (self->sources, self->sources);
+      g_source_destroy (s);
+      g_source_unref (s);
+    }
+
+  g_clear_pointer (&self->monitor_context, g_main_context_unref);
+  g_clear_pointer (&self->devices, g_hash_table_unref);
+}
+
+static void
+mock_input_device_monitor_iface_init (SrtInputDeviceMonitorInterface *iface)
+{
+#define IMPLEMENT(x) iface->x = mock_input_device_monitor_ ## x
+  IMPLEMENT (start);
+  IMPLEMENT (stop);
+#undef IMPLEMENT
+}
diff --git a/tests/mock-input-device.h b/tests/mock-input-device.h
new file mode 100644
index 0000000000000000000000000000000000000000..afca72098ec74a8c797ce5549799910bd762b4a1
--- /dev/null
+++ b/tests/mock-input-device.h
@@ -0,0 +1,102 @@
+/*
+ * Mock input device monitor, loosely based on SDL code.
+ *
+ * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org>
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: Zlib
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ */
+
+#pragma once
+
+#include <linux/input.h>
+
+#include <glib-unix.h>
+#include <libglnx.h>
+
+#include "steam-runtime-tools/glib-backports-internal.h"
+#include "steam-runtime-tools/input-device.h"
+#include "steam-runtime-tools/input-device-internal.h"
+#include "steam-runtime-tools/simple-input-device-internal.h"
+
+typedef struct _MockInputDevice MockInputDevice;
+typedef struct _MockInputDeviceClass MockInputDeviceClass;
+
+GType mock_input_device_get_type (void);
+#define MOCK_TYPE_INPUT_DEVICE (mock_input_device_get_type ())
+#define MOCK_INPUT_DEVICE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), MOCK_TYPE_INPUT_DEVICE, MockInputDevice))
+#define MOCK_IS_INPUT_DEVICE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), MOCK_TYPE_INPUT_DEVICE))
+#define MOCK_INPUT_DEVICE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), MOCK_TYPE_INPUT_DEVICE, MockInputDeviceClass))
+#define MOCK_INPUT_DEVICE_CLASS(c) (G_TYPE_CHECK_CLASS_CAST ((c), MOCK_TYPE_INPUT_DEVICE, MockInputDeviceClass))
+#define MOCK_IS_INPUT_DEVICE_CLASS(c) (G_TYPE_CHECK_CLASS_TYPE ((c), MOCK_TYPE_INPUT_DEVICE))
+
+struct _MockInputDevice
+{
+  SrtSimpleInputDevice parent;
+};
+
+struct _MockInputDeviceClass
+{
+  SrtSimpleInputDeviceClass parent;
+};
+
+MockInputDevice *mock_input_device_new (void);
+
+typedef struct _MockInputDeviceMonitor MockInputDeviceMonitor;
+typedef struct _MockInputDeviceMonitorClass MockInputDeviceMonitorClass;
+
+GType mock_input_device_monitor_get_type (void);
+#define MOCK_TYPE_INPUT_DEVICE_MONITOR (mock_input_device_monitor_get_type ())
+#define MOCK_INPUT_DEVICE_MONITOR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), MOCK_TYPE_INPUT_DEVICE_MONITOR, MockInputDeviceMonitor))
+#define MOCK_IS_INPUT_DEVICE_MONITOR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), MOCK_TYPE_INPUT_DEVICE_MONITOR))
+#define MOCK_INPUT_DEVICE_MONITOR_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), MOCK_TYPE_INPUT_DEVICE_MONITOR, MockInputDeviceMonitorClass))
+#define MOCK_INPUT_DEVICE_MONITOR_CLASS(c) (G_TYPE_CHECK_CLASS_CAST ((c), MOCK_TYPE_INPUT_DEVICE_MONITOR, MockInputDeviceMonitorClass))
+#define MOCK_IS_INPUT_DEVICE_MONITOR_CLASS(c) (G_TYPE_CHECK_CLASS_TYPE ((c), MOCK_TYPE_INPUT_DEVICE_MONITOR))
+
+struct _MockInputDeviceMonitor
+{
+  GObject parent;
+
+  GMainContext *monitor_context;
+  GHashTable *devices;
+  GList *sources;
+
+  SrtInputDeviceMonitorFlags flags;
+  enum
+  {
+    NOT_STARTED = 0,
+    STARTED,
+    STOPPED
+  } state;
+};
+
+struct _MockInputDeviceMonitorClass
+{
+  GObjectClass parent;
+};
+
+MockInputDeviceMonitor *mock_input_device_monitor_new (SrtInputDeviceMonitorFlags flags);
+
+void mock_input_device_monitor_add (MockInputDeviceMonitor *self,
+                                    MockInputDevice *device);
+void mock_input_device_monitor_remove (MockInputDeviceMonitor *self,
+                                       MockInputDevice *device);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (MockInputDevice, g_object_unref)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (MockInputDeviceMonitor, g_object_unref)
diff --git a/tests/utils.c b/tests/utils.c
index 25c70547c87fc504dd5ac07d1740d71f57b302e2..b7bb9102c94256d5de7bc36c382bbe9e8f7eee2f 100644
--- a/tests/utils.c
+++ b/tests/utils.c
@@ -29,6 +29,7 @@
 #include <glib/gstdio.h>
 #include <glib-object.h>
 
+#include "steam-runtime-tools/input-device-internal.h"
 #include "steam-runtime-tools/utils-internal.h"
 #include "test-utils.h"
 
@@ -79,6 +80,88 @@ test_avoid_gvfs (Fixture *f,
                     G_OBJECT_TYPE (local));
 }
 
+static void
+test_evdev_bits (Fixture *f,
+                 gconstpointer context)
+{
+  unsigned long words[] = { 0x00020001, 0x00080005 };
+
+#ifdef __i386__
+  g_assert_cmpuint (BITS_PER_LONG, ==, 32);
+  g_assert_cmpuint (LONGS_FOR_BITS (1), ==, 1);
+  g_assert_cmpuint (LONGS_FOR_BITS (32), ==, 1);
+  g_assert_cmpuint (LONGS_FOR_BITS (33), ==, 2);
+  g_assert_cmpuint (CHOOSE_BIT (0), ==, 0);
+  g_assert_cmpuint (CHOOSE_BIT (31), ==, 31);
+  g_assert_cmpuint (CHOOSE_BIT (32), ==, 0);
+  g_assert_cmpuint (CHOOSE_BIT (33), ==, 1);
+  g_assert_cmpuint (CHOOSE_BIT (63), ==, 31);
+  g_assert_cmpuint (CHOOSE_BIT (64), ==, 0);
+  g_assert_cmpuint (CHOOSE_LONG (0), ==, 0);
+  g_assert_cmpuint (CHOOSE_LONG (31), ==, 0);
+  g_assert_cmpuint (CHOOSE_LONG (32), ==, 1);
+  g_assert_cmpuint (CHOOSE_LONG (33), ==, 1);
+  g_assert_cmpuint (CHOOSE_LONG (63), ==, 1);
+  g_assert_cmpuint (CHOOSE_LONG (64), ==, 2);
+#elif defined(__LP64__)
+  g_assert_cmpuint (BITS_PER_LONG, ==, 64);
+  g_assert_cmpuint (LONGS_FOR_BITS (1), ==, 1);
+  g_assert_cmpuint (LONGS_FOR_BITS (64), ==, 1);
+  g_assert_cmpuint (LONGS_FOR_BITS (65), ==, 2);
+  g_assert_cmpuint (CHOOSE_BIT (0), ==, 0);
+  g_assert_cmpuint (CHOOSE_BIT (63), ==, 63);
+  g_assert_cmpuint (CHOOSE_BIT (64), ==, 0);
+  g_assert_cmpuint (CHOOSE_BIT (65), ==, 1);
+  g_assert_cmpuint (CHOOSE_BIT (127), ==, 63);
+  g_assert_cmpuint (CHOOSE_BIT (128), ==, 0);
+  g_assert_cmpuint (CHOOSE_LONG (0), ==, 0);
+  g_assert_cmpuint (CHOOSE_LONG (63), ==, 0);
+  g_assert_cmpuint (CHOOSE_LONG (64), ==, 1);
+  g_assert_cmpuint (CHOOSE_LONG (65), ==, 1);
+  g_assert_cmpuint (CHOOSE_LONG (127), ==, 1);
+  g_assert_cmpuint (CHOOSE_LONG (128), ==, 2);
+#endif
+
+  /* Among bits 0 to 15, only bit 0 (0x1) is set */
+  g_assert_cmpuint (test_bit_checked (0, words, G_N_ELEMENTS (words)), ==, 1);
+  g_assert_cmpuint (test_bit_checked (1, words, G_N_ELEMENTS (words)), ==, 0);
+  g_assert_cmpuint (test_bit_checked (15, words, G_N_ELEMENTS (words)), ==, 0);
+
+  /* Among bits 16 to 31, only bit 17 (0x2 << 16) is set */
+  g_assert_cmpuint (test_bit_checked (16, words, G_N_ELEMENTS (words)), ==, 0);
+  g_assert_cmpuint (test_bit_checked (17, words, G_N_ELEMENTS (words)), ==, 1);
+  g_assert_cmpuint (test_bit_checked (18, words, G_N_ELEMENTS (words)), ==, 0);
+  g_assert_cmpuint (test_bit_checked (31, words, G_N_ELEMENTS (words)), ==, 0);
+
+#ifdef __i386__
+  /* Among bits 32 to 63, only bits 32 (0x1 << 32), 34 (0x4 << 32)
+   * and 51 (0x8 << 48) are set, and they don't count as set unless we
+   * allow ourselves to look that far */
+  g_assert_cmpuint (test_bit_checked (32, words, 1), ==, 0);
+  g_assert_cmpuint (test_bit_checked (32, words, G_N_ELEMENTS (words)), ==, 1);
+  g_assert_cmpuint (test_bit_checked (33, words, G_N_ELEMENTS (words)), ==, 0);
+  g_assert_cmpuint (test_bit_checked (34, words, 1), ==, 0);
+  g_assert_cmpuint (test_bit_checked (34, words, G_N_ELEMENTS (words)), ==, 1);
+  g_assert_cmpuint (test_bit_checked (35, words, G_N_ELEMENTS (words)), ==, 0);
+  g_assert_cmpuint (test_bit_checked (50, words, G_N_ELEMENTS (words)), ==, 0);
+  g_assert_cmpuint (test_bit_checked (51, words, G_N_ELEMENTS (words)), ==, 1);
+  g_assert_cmpuint (test_bit_checked (52, words, G_N_ELEMENTS (words)), ==, 0);
+#elif defined(__LP64__)
+  /* Among bits 64 to 127, only bits 64 (0x1 << 64), 66 (0x4 << 64)
+   * and 83 (0x8 << 80) are set, and they don't count as set unless we
+   * allow ourselves to look that far */
+  g_assert_cmpuint (test_bit_checked (64, words, 1), ==, 0);
+  g_assert_cmpuint (test_bit_checked (64, words, G_N_ELEMENTS (words)), ==, 1);
+  g_assert_cmpuint (test_bit_checked (65, words, G_N_ELEMENTS (words)), ==, 0);
+  g_assert_cmpuint (test_bit_checked (66, words, 1), ==, 0);
+  g_assert_cmpuint (test_bit_checked (66, words, G_N_ELEMENTS (words)), ==, 1);
+  g_assert_cmpuint (test_bit_checked (67, words, G_N_ELEMENTS (words)), ==, 0);
+  g_assert_cmpuint (test_bit_checked (82, words, G_N_ELEMENTS (words)), ==, 0);
+  g_assert_cmpuint (test_bit_checked (83, words, G_N_ELEMENTS (words)), ==, 1);
+  g_assert_cmpuint (test_bit_checked (84, words, G_N_ELEMENTS (words)), ==, 0);
+#endif
+}
+
 typedef struct
 {
   const char *name;
@@ -216,6 +299,77 @@ filter_gameoverlayrenderer (Fixture *f,
   g_free (filtered_preload);
 }
 
+static void
+test_str_is_integer (Fixture *f,
+                     gconstpointer context)
+{
+  g_assert_false (_srt_str_is_integer (""));
+  g_assert_false (_srt_str_is_integer ("no"));
+  g_assert_true (_srt_str_is_integer ("1"));
+  g_assert_true (_srt_str_is_integer ("123456789012345678901234567890"));
+  g_assert_false (_srt_str_is_integer ("1.23"));
+  g_assert_false (_srt_str_is_integer ("x23"));
+  g_assert_false (_srt_str_is_integer ("23a"));
+}
+
+static const char uevent[] =
+"DRIVER=lenovo\n"
+"HID_ID=0003:000017EF:00006009\n"
+"HID_NAME=Lite-On Technology Corp. ThinkPad USB Keyboard with TrackPoint\n"
+"HID_PHYS=usb-0000:00:14.0-2/input0\n"
+"HID_UNIQ=\n"
+"MODALIAS=hid:b0003g0000v000017EFp00006009\n";
+
+static struct
+{
+  const char *key;
+  const char *value;
+} uevent_parsed[] =
+{
+  { "DRIVER", "lenovo" },
+  { "HID_ID", "0003:000017EF:00006009" },
+  { "HID_NAME", "Lite-On Technology Corp. ThinkPad USB Keyboard with TrackPoint" },
+  { "HID_PHYS", "usb-0000:00:14.0-2/input0" },
+  { "HID_UNIQ", "" },
+  { "MODALIAS", "hid:b0003g0000v000017EFp00006009" }
+};
+
+static const char no_newline[] = "DRIVER=lenovo";
+
+static void
+test_uevent_field (Fixture *f,
+                   gconstpointer context)
+{
+  gsize i;
+
+  g_assert_false (_srt_input_device_uevent_field_equals (no_newline, "DRIVER", ""));
+  g_assert_false (_srt_input_device_uevent_field_equals (no_newline, "DRIVER", "lenov"));
+  g_assert_true (_srt_input_device_uevent_field_equals (no_newline, "DRIVER", "lenovo"));
+  g_assert_false (_srt_input_device_uevent_field_equals (no_newline, "DRIVER", "lenovoo"));
+
+  g_assert_false (_srt_input_device_uevent_field_equals (uevent, "DRIVER", "lenov"));
+  g_assert_false (_srt_input_device_uevent_field_equals (uevent, "DRIVER", "lenovoo"));
+  g_assert_false (_srt_input_device_uevent_field_equals (uevent, "HID_ID", "0003:000017EF:0000600"));
+  g_assert_false (_srt_input_device_uevent_field_equals (uevent, "HID_ID", "0003:000017EF:000060099"));
+  g_assert_false (_srt_input_device_uevent_field_equals (uevent, "HID_UNIQ", "x"));
+  g_assert_false (_srt_input_device_uevent_field_equals (uevent, "MODALIAS", "nope"));
+  g_assert_false (_srt_input_device_uevent_field_equals (uevent, "NOPE", ""));
+  g_assert_false (_srt_input_device_uevent_field_equals (uevent, "NOPE", "nope"));
+
+  for (i = 0; i < G_N_ELEMENTS (uevent_parsed); i++)
+    {
+      g_autofree gchar *v = NULL;
+
+      v = _srt_input_device_uevent_field (uevent, uevent_parsed[i].key);
+      g_assert_cmpstr (v, ==, uevent_parsed[i].value);
+      g_assert_true (_srt_input_device_uevent_field_equals (uevent,
+                                                            uevent_parsed[i].key,
+                                                            uevent_parsed[i].value));
+    }
+
+  g_assert_null (_srt_input_device_uevent_field (uevent, "NOPE"));
+}
+
 int
 main (int argc,
       char **argv)
@@ -224,10 +378,16 @@ main (int argc,
 
   g_test_init (&argc, &argv, NULL);
   g_test_add ("/utils/avoid-gvfs", Fixture, NULL, setup, test_avoid_gvfs, teardown);
+  g_test_add ("/utils/evdev-bits", Fixture, NULL,
+              setup, test_evdev_bits, teardown);
   g_test_add ("/utils/test-file-in-sysroot", Fixture, NULL,
               setup, test_file_in_sysroot, teardown);
   g_test_add ("/utils/filter_gameoverlayrenderer", Fixture, NULL, setup,
               filter_gameoverlayrenderer, teardown);
+  g_test_add ("/utils/str_is_integer", Fixture, NULL,
+              setup, test_str_is_integer, teardown);
+  g_test_add ("/utils/uevent-field", Fixture, NULL,
+              setup, test_uevent_field, teardown);
 
   return g_test_run ();
 }