diff --git a/premake5.lua b/premake5.lua
index 80870afce..cd81dbf77 100644
--- a/premake5.lua
+++ b/premake5.lua
@@ -331,6 +331,10 @@ workspace("xenia")
     include("src/xenia/hid/sdl")
   end
 
+  if not os.istarget("windows") then
+    include("src/xenia/hid/keyboard")
+  end
+
   if os.istarget("windows") then
     include("src/xenia/apu/xaudio2")
     include("src/xenia/gpu/d3d12")
diff --git a/src/xenia/app/premake5.lua b/src/xenia/app/premake5.lua
index 11a7a0100..c7fae7a4f 100644
--- a/src/xenia/app/premake5.lua
+++ b/src/xenia/app/premake5.lua
@@ -97,6 +97,10 @@ project("xenia-app")
       "xenia-hid-sdl",
     })
 
+  filter("platforms:not Windows")
+    links({
+      "xenia-hid-keyboard"
+    })
   filter("platforms:Linux")
     links({
       "X11",
diff --git a/src/xenia/app/xenia_main.cc b/src/xenia/app/xenia_main.cc
index d90246936..8f459f0b7 100644
--- a/src/xenia/app/xenia_main.cc
+++ b/src/xenia/app/xenia_main.cc
@@ -56,6 +56,9 @@
 #if !XE_PLATFORM_ANDROID
 #include "xenia/hid/sdl/sdl_hid.h"
 #endif  // !XE_PLATFORM_ANDROID
+#if !XE_PLATFORM_WIN32
+#include "xenia/hid/keyboard/keyboard_hid.h"
+#endif
 #if XE_PLATFORM_WIN32
 #include "xenia/hid/winkey/winkey_hid.h"
 #include "xenia/hid/xinput/xinput_hid.h"
@@ -388,6 +391,9 @@ std::vector<std::unique_ptr<hid::InputDriver>> EmulatorApp::CreateInputDrivers(
 #if !XE_PLATFORM_ANDROID
     factory.Add("sdl", xe::hid::sdl::Create);
 #endif  // !XE_PLATFORM_ANDROID
+#if !XE_PLATFORM_WIN32
+    factory.Add("keyboard", xe::hid::keyboard::Create);
+#endif
 #if XE_PLATFORM_WIN32
     // WinKey input driver should always be the last input driver added!
     factory.Add("winkey", xe::hid::winkey::Create);
diff --git a/src/xenia/hid/hid_demo.cc b/src/xenia/hid/hid_demo.cc
index dd4198cb5..3e8df0bf4 100644
--- a/src/xenia/hid/hid_demo.cc
+++ b/src/xenia/hid/hid_demo.cc
@@ -40,6 +40,9 @@
 #if !XE_PLATFORM_ANDROID
 #include "xenia/hid/sdl/sdl_hid.h"
 #endif  // !XE_PLATFORM_ANDROID
+#if !XE_PLATFORM_WIN32
+#include "xenia/hid/keyboard/keyboard_hid.h"
+#endif
 #if XE_PLATFORM_WIN32
 #include "xenia/hid/winkey/winkey_hid.h"
 #include "xenia/hid/xinput/xinput_hid.h"
@@ -132,6 +135,13 @@ std::vector<std::unique_ptr<hid::InputDriver>> HidDemoApp::CreateInputDrivers(
       drivers.emplace_back(std::move(driver));
     }
 #endif  // !XE_PLATFORM_ANDROID
+#if !XE_PLATFORM_WIN32
+  } else if (cvars::hid.compare("keyboard") == 0) {
+    auto driver = xe::hid::keyboard::Create(window, kZOrderHidInput);
+    if (driver && XSUCCEEDED(driver->Setup())) {
+      drivers.emplace_back(std::move(driver));
+    }
+#endif
 #if XE_PLATFORM_WIN32
   } else if (cvars::hid.compare("winkey") == 0) {
     auto driver = xe::hid::winkey::Create(window, kZOrderHidInput);
@@ -151,6 +161,12 @@ std::vector<std::unique_ptr<hid::InputDriver>> HidDemoApp::CreateInputDrivers(
       drivers.emplace_back(std::move(sdl_driver));
     }
 #endif  // !XE_PLATFORM_ANDROID
+#if !XE_PLATFORM_WIN32
+    auto keyboard_driver = xe::hid::keyboard::Create(window, kZOrderHidInput);
+    if (keyboard_driver && XSUCCEEDED(keyboard_driver->Setup())) {
+      drivers.emplace_back(std::move(keyboard_driver));
+    }
+#endif
 #if XE_PLATFORM_WIN32
     auto xinput_driver = xe::hid::xinput::Create(window, kZOrderHidInput);
     if (xinput_driver && XSUCCEEDED(xinput_driver->Setup())) {
diff --git a/src/xenia/hid/keyboard/keyboard_binding_table.inc b/src/xenia/hid/keyboard/keyboard_binding_table.inc
new file mode 100644
index 000000000..4e4db3d1c
--- /dev/null
+++ b/src/xenia/hid/keyboard/keyboard_binding_table.inc
@@ -0,0 +1,38 @@
+/**
+ ******************************************************************************
+ * Xenia : Xbox 360 Emulator Research Project                                 *
+ ******************************************************************************
+ * Copyright 2022 Ben Vanik. All rights reserved.                             *
+ * Released under the BSD license - see LICENSE in the root for more details. *
+ ******************************************************************************
+ */
+
+// This is a partial file designed to be included by other files when
+// constructing various tables.
+
+// clang-format off
+XE_HID_KEYBOARD_BINDING(DpadLeft,    "DPAD_LEFT"          , keybind_dpad_left        , "^A"  )
+XE_HID_KEYBOARD_BINDING(DpadRight,   "DPAD_RIGHT"         , keybind_dpad_right       , "^D"  )
+XE_HID_KEYBOARD_BINDING(DpadDown,    "DPAD_DOWN"          , keybind_dpad_down        , "^S"  )
+XE_HID_KEYBOARD_BINDING(DpadUp,      "DPAD_UP"            , keybind_dpad_up          , "^W"  )
+XE_HID_KEYBOARD_BINDING(LThumbLeft,  "LEFT_THUMB_LEFT"    , keybind_left_thumb_left  , "_A"  )
+XE_HID_KEYBOARD_BINDING(LThumbRight, "LEFT_THUMB_RIGHT"   , keybind_left_thumb_right , "_D"  )
+XE_HID_KEYBOARD_BINDING(LThumbDown,  "LEFT_THUMB_DOWN"    , keybind_left_thumb_down  , "_S"  )
+XE_HID_KEYBOARD_BINDING(LThumbUp,    "LEFT_THUMB_UP"      , keybind_left_thumb_up    , "_W"  )
+XE_HID_KEYBOARD_BINDING(LThumbPress, "LEFT_THUMB_PRESSED" , keybind_left_thumb       , "F"   )
+XE_HID_KEYBOARD_BINDING(RThumbUp,    "RIGHT_THUMB_UP"     , keybind_right_thumb_up   , "0x26")
+XE_HID_KEYBOARD_BINDING(RThumbDown,  "RIGHT_THUMB_DOWN"   , keybind_right_thumb_down , "0x28")
+XE_HID_KEYBOARD_BINDING(RThumbRight, "RIGHT_THUMB_RIGHT"  , keybind_right_thumb_right, "0x27")
+XE_HID_KEYBOARD_BINDING(RThumbLeft,  "RIGHT_THUMB_LEFT"   , keybind_right_thumb_left , "0x25")
+XE_HID_KEYBOARD_BINDING(RThumbPress, "RIGHT_THUMB_PRESSED", keybind_right_thumb      , "K"   )
+XE_HID_KEYBOARD_BINDING(X,           "X"                  , keybind_x                , "L"   )
+XE_HID_KEYBOARD_BINDING(B,           "B"                  , keybind_b                , "0xDE")
+XE_HID_KEYBOARD_BINDING(A,           "A"                  , keybind_a                , "0xBA")
+XE_HID_KEYBOARD_BINDING(Y,           "Y"                  , keybind_y                , "P"   )
+XE_HID_KEYBOARD_BINDING(LTrigger,    "LEFT_TRIGGER"       , keybind_left_trigger     , "Q I" )
+XE_HID_KEYBOARD_BINDING(RTrigger,    "RIGHT_TRIGGER"      , keybind_right_trigger    , "E O" )
+XE_HID_KEYBOARD_BINDING(Back,        "BACK"               , keybind_back             , "Z"   )
+XE_HID_KEYBOARD_BINDING(Start,       "START"              , keybind_start            , "X"   )
+XE_HID_KEYBOARD_BINDING(LShoulder,   "LEFT_SHOULDER"      , keybind_left_shoulder    , "1"   )
+XE_HID_KEYBOARD_BINDING(RShoulder,   "RIGHT_SHOULDER"     , keybind_right_shoulder   , "3"   )
+// clang-format on
diff --git a/src/xenia/hid/keyboard/keyboard_hid.cc b/src/xenia/hid/keyboard/keyboard_hid.cc
new file mode 100644
index 000000000..983ff4ba0
--- /dev/null
+++ b/src/xenia/hid/keyboard/keyboard_hid.cc
@@ -0,0 +1,26 @@
+/**
+ ******************************************************************************
+ * Xenia : Xbox 360 Emulator Research Project                                 *
+ ******************************************************************************
+ * Copyright 2014 Ben Vanik. All rights reserved.                             *
+ * Released under the BSD license - see LICENSE in the root for more details. *
+ ******************************************************************************
+ */
+
+#include "xenia/hid/keyboard/keyboard_hid.h"
+
+#include "xenia/hid/keyboard/keyboard_input_driver.h"
+
+namespace xe {
+namespace hid {
+namespace keyboard {
+
+std::unique_ptr<InputDriver> Create(xe::ui::Window* window,
+                                    size_t window_z_order) {
+  return std::make_unique<xe::hid::keyboard::KeyboardInputDriver>(
+      window, window_z_order);
+}
+
+}  // namespace keyboard
+}  // namespace hid
+}  // namespace xe
diff --git a/src/xenia/hid/keyboard/keyboard_hid.h b/src/xenia/hid/keyboard/keyboard_hid.h
new file mode 100644
index 000000000..5d8915b42
--- /dev/null
+++ b/src/xenia/hid/keyboard/keyboard_hid.h
@@ -0,0 +1,28 @@
+/**
+ ******************************************************************************
+ * Xenia : Xbox 360 Emulator Research Project                                 *
+ ******************************************************************************
+ * Copyright 2014 Ben Vanik. All rights reserved.                             *
+ * Released under the BSD license - see LICENSE in the root for more details. *
+ ******************************************************************************
+ */
+
+#ifndef XENIA_HID_KEYBOARD_KEYBOARD_HID_H_
+#define XENIA_HID_KEYBOARD_KEYBOARD_HID_H_
+
+#include <memory>
+
+#include "xenia/hid/input_system.h"
+
+namespace xe {
+namespace hid {
+namespace keyboard {
+
+std::unique_ptr<InputDriver> Create(xe::ui::Window* window,
+                                    size_t window_z_order);
+
+}  // namespace keyboard
+}  // namespace hid
+}  // namespace xe
+
+#endif  // XENIA_HID_WINKEY_WINKEY_HID_H_
diff --git a/src/xenia/hid/keyboard/keyboard_input_driver.cc b/src/xenia/hid/keyboard/keyboard_input_driver.cc
new file mode 100644
index 000000000..184db383a
--- /dev/null
+++ b/src/xenia/hid/keyboard/keyboard_input_driver.cc
@@ -0,0 +1,340 @@
+/**
+ ******************************************************************************
+ * Xenia : Xbox 360 Emulator Research Project                                 *
+ ******************************************************************************
+ * Copyright 2022 Ben Vanik. All rights reserved.                             *
+ * Released under the BSD license - see LICENSE in the root for more details. *
+ ******************************************************************************
+ */
+
+#include "xenia/hid/keyboard/keyboard_input_driver.h"
+
+#include "xenia/base/logging.h"
+#include "xenia/hid/hid_flags.h"
+#include "xenia/hid/input_system.h"
+#include "xenia/ui/virtual_key.h"
+#include "xenia/ui/window.h"
+
+#define XE_HID_KEYBOARD_BINDING(button, description, cvar_name, \
+                                cvar_default_value)             \
+  DEFINE_string(cvar_name, cvar_default_value,                  \
+                "List of keys to bind to " description          \
+                ", separated by spaces",                        \
+                "HID.Key")
+#include "keyboard_binding_table.inc"
+#undef XE_HID_KEYBOARD_BINDING
+
+namespace xe {
+namespace hid {
+namespace keyboard {
+
+void KeyboardInputDriver::ParseKeyBinding(
+    ui::VirtualKey output_key, const std::string_view description,
+    const std::string_view source_tokens) {
+  for (const std::string_view source_token :
+       utf8::split(source_tokens, " ", true)) {
+    KeyBinding key_binding;
+    key_binding.output_key = output_key;
+
+    std::string_view token = source_token;
+
+    if (utf8::starts_with(token, "_")) {
+      key_binding.uppercase = false;
+      token = token.substr(1);
+    } else if (utf8::starts_with(token, "^")) {
+      key_binding.uppercase = true;
+      token = token.substr(1);
+    }
+
+    if (utf8::starts_with(token, "0x")) {
+      token = token.substr(2);
+      key_binding.input_key = static_cast<ui::VirtualKey>(
+          string_util::from_string<uint16_t>(token, true));
+    } else if (token.size() == 1 && (token[0] >= 'A' && token[0] <= 'Z') ||
+               (token[0] >= '0' && token[0] <= '9')) {
+      key_binding.input_key = static_cast<ui::VirtualKey>(token[0]);
+    }
+
+    if (key_binding.input_key == ui::VirtualKey::kNone) {
+      XELOGW(
+          "keyboard HID: failed to parse binding \"{}\" for controller input "
+          "{}.",
+          source_token, description);
+      continue;
+    }
+
+    key_bindings_.push_back(key_binding);
+    XELOGI("keyboard HID: \"{}\" binds key 0x{:X} to controller input {}.",
+           source_token, static_cast<uint16_t>(key_binding.input_key),
+           description);
+  }
+}
+
+KeyboardInputDriver::KeyboardInputDriver(xe::ui::Window* window,
+                                         size_t window_z_order)
+    : InputDriver(window, window_z_order), window_input_listener_(*this) {
+#define XE_HID_KEYBOARD_BINDING(button, description, cvar_name,        \
+                                cvar_default_value)                    \
+  ParseKeyBinding(xe::ui::VirtualKey::kXInputPad##button, description, \
+                  cvars::cvar_name);
+#include "keyboard_binding_table.inc"
+#undef XE_HID_KEYBOARD_BINDING
+
+  window->AddInputListener(&window_input_listener_, window_z_order);
+}
+
+KeyboardInputDriver::~KeyboardInputDriver() {
+  window()->RemoveInputListener(&window_input_listener_);
+}
+
+X_STATUS KeyboardInputDriver::Setup() { return X_STATUS_SUCCESS; }
+
+X_RESULT KeyboardInputDriver::GetCapabilities(uint32_t user_index,
+                                              uint32_t flags,
+                                              X_INPUT_CAPABILITIES* out_caps) {
+  if (user_index != 0) {
+    return X_ERROR_DEVICE_NOT_CONNECTED;
+  }
+
+  // TODO(benvanik): confirm with a real XInput controller.
+  out_caps->type = 0x01;      // XINPUT_DEVTYPE_GAMEPAD
+  out_caps->sub_type = 0x01;  // XINPUT_DEVSUBTYPE_GAMEPAD
+  out_caps->flags = 0;
+  out_caps->gamepad.buttons = 0xFFFF;
+  out_caps->gamepad.left_trigger = 0xFF;
+  out_caps->gamepad.right_trigger = 0xFF;
+  out_caps->gamepad.thumb_lx = (int16_t)0xFFFFu;
+  out_caps->gamepad.thumb_ly = (int16_t)0xFFFFu;
+  out_caps->gamepad.thumb_rx = (int16_t)0xFFFFu;
+  out_caps->gamepad.thumb_ry = (int16_t)0xFFFFu;
+  out_caps->vibration.left_motor_speed = 0;
+  out_caps->vibration.right_motor_speed = 0;
+  return X_ERROR_SUCCESS;
+}
+
+X_RESULT KeyboardInputDriver::GetState(uint32_t user_index,
+                                       X_INPUT_STATE* out_state) {
+  if (user_index != 0) {
+    return X_ERROR_DEVICE_NOT_CONNECTED;
+  }
+
+  packet_number_++;
+
+  uint16_t buttons = 0;
+  uint8_t left_trigger = 0;
+  uint8_t right_trigger = 0;
+  int16_t thumb_lx = 0;
+  int16_t thumb_ly = 0;
+  int16_t thumb_rx = 0;
+  int16_t thumb_ry = 0;
+
+  if (window()->HasFocus() && is_active()) {
+    for (const KeyBinding& b : key_bindings_) {
+      if (b.is_pressed) {
+        switch (b.output_key) {
+          case ui::VirtualKey::kXInputPadA:
+            buttons |= X_INPUT_GAMEPAD_A;
+            break;
+          case ui::VirtualKey::kXInputPadY:
+            buttons |= X_INPUT_GAMEPAD_Y;
+            break;
+          case ui::VirtualKey::kXInputPadB:
+            buttons |= X_INPUT_GAMEPAD_B;
+            break;
+          case ui::VirtualKey::kXInputPadX:
+            buttons |= X_INPUT_GAMEPAD_X;
+            break;
+          case ui::VirtualKey::kXInputPadGuide:
+            buttons |= X_INPUT_GAMEPAD_GUIDE;
+            break;
+          case ui::VirtualKey::kXInputPadDpadLeft:
+            buttons |= X_INPUT_GAMEPAD_DPAD_LEFT;
+            break;
+          case ui::VirtualKey::kXInputPadDpadRight:
+            buttons |= X_INPUT_GAMEPAD_DPAD_RIGHT;
+            break;
+          case ui::VirtualKey::kXInputPadDpadDown:
+            buttons |= X_INPUT_GAMEPAD_DPAD_DOWN;
+            break;
+          case ui::VirtualKey::kXInputPadDpadUp:
+            buttons |= X_INPUT_GAMEPAD_DPAD_UP;
+            break;
+          case ui::VirtualKey::kXInputPadRThumbPress:
+            buttons |= X_INPUT_GAMEPAD_RIGHT_THUMB;
+            break;
+          case ui::VirtualKey::kXInputPadLThumbPress:
+            buttons |= X_INPUT_GAMEPAD_LEFT_THUMB;
+            break;
+          case ui::VirtualKey::kXInputPadBack:
+            buttons |= X_INPUT_GAMEPAD_BACK;
+            break;
+          case ui::VirtualKey::kXInputPadStart:
+            buttons |= X_INPUT_GAMEPAD_START;
+            break;
+          case ui::VirtualKey::kXInputPadLShoulder:
+            buttons |= X_INPUT_GAMEPAD_LEFT_SHOULDER;
+            break;
+          case ui::VirtualKey::kXInputPadRShoulder:
+            buttons |= X_INPUT_GAMEPAD_RIGHT_SHOULDER;
+            break;
+          case ui::VirtualKey::kXInputPadLTrigger:
+            left_trigger = 0xFF;
+            break;
+          case ui::VirtualKey::kXInputPadRTrigger:
+            right_trigger = 0xFF;
+            break;
+          case ui::VirtualKey::kXInputPadLThumbLeft:
+            thumb_lx += SHRT_MIN;
+            break;
+          case ui::VirtualKey::kXInputPadLThumbRight:
+            thumb_lx += SHRT_MAX;
+            break;
+          case ui::VirtualKey::kXInputPadLThumbDown:
+            thumb_ly += SHRT_MIN;
+            break;
+          case ui::VirtualKey::kXInputPadLThumbUp:
+            thumb_ly += SHRT_MAX;
+            break;
+          case ui::VirtualKey::kXInputPadRThumbUp:
+            thumb_ry += SHRT_MAX;
+            break;
+          case ui::VirtualKey::kXInputPadRThumbDown:
+            thumb_ry += SHRT_MIN;
+            break;
+          case ui::VirtualKey::kXInputPadRThumbRight:
+            thumb_rx += SHRT_MAX;
+            break;
+          case ui::VirtualKey::kXInputPadRThumbLeft:
+            thumb_rx += SHRT_MIN;
+            break;
+          default:
+            assert_unhandled_case(b.output_key);
+        }
+      }
+    }
+  }
+
+  out_state->packet_number = packet_number_;
+  out_state->gamepad.buttons = buttons;
+  out_state->gamepad.left_trigger = left_trigger;
+  out_state->gamepad.right_trigger = right_trigger;
+  out_state->gamepad.thumb_lx = thumb_lx;
+  out_state->gamepad.thumb_ly = thumb_ly;
+  out_state->gamepad.thumb_rx = thumb_rx;
+  out_state->gamepad.thumb_ry = thumb_ry;
+
+  return X_ERROR_SUCCESS;
+}
+
+X_RESULT KeyboardInputDriver::SetState(uint32_t user_index,
+                                       X_INPUT_VIBRATION* vibration) {
+  if (user_index != 0) {
+    return X_ERROR_DEVICE_NOT_CONNECTED;
+  }
+
+  return X_ERROR_SUCCESS;
+}
+
+X_RESULT KeyboardInputDriver::GetKeystroke(uint32_t user_index, uint32_t flags,
+                                           X_INPUT_KEYSTROKE* out_keystroke) {
+  if (user_index != 0) {
+    return X_ERROR_DEVICE_NOT_CONNECTED;
+  }
+
+  if (!is_active()) {
+    return X_ERROR_EMPTY;
+  }
+
+  X_RESULT result = X_ERROR_EMPTY;
+
+  ui::VirtualKey xinput_virtual_key = ui::VirtualKey::kNone;
+  uint16_t unicode = 0;
+  uint16_t keystroke_flags = 0;
+  uint8_t hid_code = 0;
+
+  // Pop from the queue.
+  KeyEvent evt;
+  {
+    auto global_lock = global_critical_region_.Acquire();
+    if (key_events_.empty()) {
+      // No keys!
+      return X_ERROR_EMPTY;
+    }
+    evt = key_events_.front();
+    key_events_.pop();
+  }
+
+  for (const KeyBinding& b : key_bindings_) {
+    if (b.input_key == evt.virtual_key && b.uppercase == evt.is_capital) {
+      xinput_virtual_key = b.output_key;
+    }
+  }
+
+  if (xinput_virtual_key != ui::VirtualKey::kNone) {
+    if (evt.transition == true) {
+      keystroke_flags |= 0x0001;  // XINPUT_KEYSTROKE_KEYDOWN
+    } else if (evt.transition == false) {
+      keystroke_flags |= 0x0002;  // XINPUT_KEYSTROKE_KEYUP
+    }
+
+    if (evt.prev_state == evt.transition) {
+      keystroke_flags |= 0x0004;  // XINPUT_KEYSTROKE_REPEAT
+    }
+
+    result = X_ERROR_SUCCESS;
+  }
+
+  out_keystroke->virtual_key = uint16_t(xinput_virtual_key);
+  out_keystroke->unicode = unicode;
+  out_keystroke->flags = keystroke_flags;
+  out_keystroke->user_index = 0;
+  out_keystroke->hid_code = hid_code;
+
+  // X_ERROR_EMPTY if no new keys
+  // X_ERROR_DEVICE_NOT_CONNECTED if no device
+  // X_ERROR_SUCCESS if key
+  return result;
+}
+
+void KeyboardInputDriver::KeyboardWindowInputListener::OnKeyDown(
+    ui::KeyEvent& e) {
+  driver_.OnKey(e, true);
+}
+
+void KeyboardInputDriver::KeyboardWindowInputListener::OnKeyUp(
+    ui::KeyEvent& e) {
+  driver_.OnKey(e, false);
+}
+
+void KeyboardInputDriver::OnKey(ui::KeyEvent& e, bool is_down) {
+  if (!is_active()) {
+    return;
+  }
+
+  KeyEvent key;
+  key.virtual_key = e.virtual_key();
+  key.transition = is_down;
+  key.prev_state = e.prev_state();
+  key.repeat_count = e.repeat_count();
+  key.is_capital = e.is_shift_pressed();
+
+  auto global_lock = global_critical_region_.Acquire();
+  key_events_.push(key);
+  bool found = false;
+  for (auto& key_binding : key_bindings_) {
+    if (key_binding.input_key == key.virtual_key) {
+      found = true;
+      if (key_binding.uppercase == e.is_shift_pressed()) {
+        key_binding.is_pressed = is_down;
+      }
+    }
+  }
+}
+
+InputType KeyboardInputDriver::GetInputType() const {
+  return InputType::Controller;
+}
+
+}  // namespace keyboard
+}  // namespace hid
+}  // namespace xe
diff --git a/src/xenia/hid/keyboard/keyboard_input_driver.h b/src/xenia/hid/keyboard/keyboard_input_driver.h
new file mode 100644
index 000000000..0ae55f5fd
--- /dev/null
+++ b/src/xenia/hid/keyboard/keyboard_input_driver.h
@@ -0,0 +1,86 @@
+/**
+ ******************************************************************************
+ * Xenia : Xbox 360 Emulator Research Project                                 *
+ ******************************************************************************
+ * Copyright 2022 Ben Vanik. All rights reserved.                             *
+ * Released under the BSD license - see LICENSE in the root for more details. *
+ ******************************************************************************
+ */
+
+#ifndef XENIA_HID_KEYBOARD_KEYBOARD_INPUT_DRIVER_H_
+#define XENIA_HID_KEYBOARD_KEYBOARD_INPUT_DRIVER_H_
+
+#include <queue>
+
+#include "xenia/base/mutex.h"
+#include "xenia/hid/input_driver.h"
+#include "xenia/ui/virtual_key.h"
+
+namespace xe {
+namespace hid {
+namespace keyboard {
+
+class KeyboardInputDriver final : public InputDriver {
+ public:
+  explicit KeyboardInputDriver(xe::ui::Window* window, size_t window_z_order);
+  ~KeyboardInputDriver() override;
+
+  X_STATUS Setup() override;
+
+  X_RESULT GetCapabilities(uint32_t user_index, uint32_t flags,
+                           X_INPUT_CAPABILITIES* out_caps) override;
+  X_RESULT GetState(uint32_t user_index, X_INPUT_STATE* out_state) override;
+  X_RESULT SetState(uint32_t user_index, X_INPUT_VIBRATION* vibration) override;
+  X_RESULT GetKeystroke(uint32_t user_index, uint32_t flags,
+                        X_INPUT_KEYSTROKE* out_keystroke) override;
+  virtual InputType GetInputType() const override;
+
+ protected:
+  class KeyboardWindowInputListener final : public ui::WindowInputListener {
+   public:
+    explicit KeyboardWindowInputListener(KeyboardInputDriver& driver)
+        : driver_(driver) {}
+
+    void OnKeyDown(ui::KeyEvent& e) override;
+    void OnKeyUp(ui::KeyEvent& e) override;
+
+   private:
+    KeyboardInputDriver& driver_;
+  };
+
+  struct KeyEvent {
+    ui::VirtualKey virtual_key = ui::VirtualKey::kNone;
+    int repeat_count = 0;
+    bool transition = false;  // going up(false) or going down(true)
+    bool prev_state = false;  // down(true) or up(false)
+    bool is_capital = false;
+  };
+
+  struct KeyBinding {
+    ui::VirtualKey input_key = ui::VirtualKey::kNone;
+    ui::VirtualKey output_key = ui::VirtualKey::kNone;
+    bool uppercase = false;
+    bool is_pressed = false;
+  };
+
+  void ParseKeyBinding(ui::VirtualKey virtual_key,
+                       const std::string_view description,
+                       const std::string_view binding);
+
+  void OnKey(ui::KeyEvent& e, bool is_down);
+
+  xe::global_critical_region global_critical_region_;
+
+  std::queue<KeyEvent> key_events_;
+  std::vector<KeyBinding> key_bindings_;
+
+  KeyboardWindowInputListener window_input_listener_;
+
+  uint32_t packet_number_ = 1;
+};
+
+}  // namespace keyboard
+}  // namespace hid
+}  // namespace xe
+
+#endif  // XENIA_HID_WINKEY_WINKEY_INPUT_DRIVER_H_
diff --git a/src/xenia/hid/keyboard/premake5.lua b/src/xenia/hid/keyboard/premake5.lua
new file mode 100644
index 000000000..1516da586
--- /dev/null
+++ b/src/xenia/hid/keyboard/premake5.lua
@@ -0,0 +1,16 @@
+project_root = "../../../.."
+include(project_root.."/tools/build")
+
+group("src")
+project("xenia-hid-keyboard")
+  uuid("ab5cfd8d-7877-44cd-9526-63f7c5738609")
+  kind("StaticLib")
+  language("C++")
+  links({
+    "xenia-base",
+    "xenia-hid",
+    "xenia-ui",
+  })
+  defines({
+  })
+  local_platform_files()