diff --git a/Inputs/Keyboard.cpp b/Inputs/Keyboard.cpp
new file mode 100644
index 000000000..c479c6a7f
--- /dev/null
+++ b/Inputs/Keyboard.cpp
@@ -0,0 +1,38 @@
+//
+//  Keyboard.cpp
+//  Clock Signal
+//
+//  Created by Thomas Harte on 10/9/17.
+//  Copyright © 2017 Thomas Harte. All rights reserved.
+//
+
+#include "Keyboard.hpp"
+
+using namespace Inputs;
+
+Keyboard::Keyboard() {}
+
+void Keyboard::set_key_pressed(Key key, bool is_pressed) {
+	size_t key_offset = static_cast<size_t>(key);
+	if(key_offset > key_states_.size()) {
+		key_states_.resize(key_offset, false);
+	}
+	key_states_[key_offset] = is_pressed;
+
+	if(delegate_) delegate_->keyboard_did_change_key(this, key, is_pressed);
+}
+
+void Keyboard::reset_all_keys() {
+	std::fill(key_states_.begin(), key_states_.end(), false);
+	if(delegate_) delegate_->reset_all_keys();
+}
+
+void Keyboard::set_delegate(Delegate *delegate) {
+	delegate_ = delegate;
+}
+
+bool Keyboard::get_key_state(Key key) {
+	size_t key_offset = static_cast<size_t>(key);
+	if(key_offset >= key_states_.size()) return false;
+	return key_states_[key_offset];
+}
diff --git a/Inputs/Keyboard.hpp b/Inputs/Keyboard.hpp
new file mode 100644
index 000000000..85b1ba883
--- /dev/null
+++ b/Inputs/Keyboard.hpp
@@ -0,0 +1,60 @@
+//
+//  Keyboard.hpp
+//  Clock Signal
+//
+//  Created by Thomas Harte on 10/9/17.
+//  Copyright © 2017 Thomas Harte. All rights reserved.
+//
+
+#ifndef Keyboard_hpp
+#define Keyboard_hpp
+
+#include <vector>
+
+namespace Inputs {
+
+/*!
+	Provides an intermediate idealised model of a modern-era computer keyboard
+	(so, heavily indebted to the current Windows and Mac layouts), allowing a host
+	machine to toggle states, while an interested party either observes or polls.
+*/
+class Keyboard {
+	public:
+		Keyboard();
+	
+		enum class Key {
+			Escape, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, PrintScreen, ScrollLock, Pause,
+			BackTick, k1, k2, k3, k4, k5, k6, k7, k8, k9, k0, Hyphen, Equals, BackSpace,
+			Tab, Q, W, E, R, T, Y, U, I, O, P, OpenSquareBracket, CloseSquareBracket, BackSlash,
+			CapsLock, A, S, D, F, G, H, J, K, L, Semicolon, Quote, Hash, Enter,
+			LeftShift, Z, X, C, V, B, N, M, Comma, FullStop, ForwardSlash, RightShift,
+			LeftControl, LeftOption, LeftMeta, Space, RightMeta, RightOption,
+			Left, Right, Up, Down,
+			Insert, Home, PageUp, Delete, End, PageDown,
+			NumLock, KeyPadSlash, KeyPadAsterisk, KeyPadDelete,
+			KeyPad7, KeyPad8, KeyPad9, KeyPadPlus,
+			KeyPad4, KeyPad5, KeyPad6,
+			KeyPad1, KeyPad2, KeyPad3, KeyPadEnter,
+			KeyPad0, KeyPadDecimalPoint
+		};
+
+		// Host interface.
+		void set_key_pressed(Key key, bool is_pressed);
+		void reset_all_keys();
+
+		// Delegate interface.
+		struct Delegate {
+			virtual void keyboard_did_change_key(Keyboard *keyboard, Key key, bool is_pressed) = 0;
+			virtual void reset_all_keys() = 0;
+		};
+		void set_delegate(Delegate *delegate);
+		bool get_key_state(Key key);
+	
+	private:
+		std::vector<bool> key_states_;
+		Delegate *delegate_ = nullptr;
+};
+	
+}
+
+#endif /* Keyboard_hpp */
diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj
index 704fd7b91..20c740482 100644
--- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
+++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
@@ -115,6 +115,7 @@
 		4B8378DF1F33675F005CA9E4 /* CharacterMapper.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8378DD1F33675F005CA9E4 /* CharacterMapper.cpp */; };
 		4B8378E21F336920005CA9E4 /* CharacterMapper.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8378E01F336920005CA9E4 /* CharacterMapper.cpp */; };
 		4B8378E51F3378C4005CA9E4 /* CharacterMapper.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8378E31F3378C4005CA9E4 /* CharacterMapper.cpp */; };
+		4B86E25B1F8C628F006FAA45 /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B86E2591F8C628F006FAA45 /* Keyboard.cpp */; };
 		4B8805F01DCFC99C003085B1 /* Acorn.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8805EE1DCFC99C003085B1 /* Acorn.cpp */; };
 		4B8805F41DCFD22A003085B1 /* Commodore.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8805F21DCFD22A003085B1 /* Commodore.cpp */; };
 		4B8805F71DCFF6C9003085B1 /* Commodore.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8805F51DCFF6C9003085B1 /* Commodore.cpp */; };
@@ -689,6 +690,8 @@
 		4B8378E11F336920005CA9E4 /* CharacterMapper.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = CharacterMapper.hpp; path = Oric/CharacterMapper.hpp; sourceTree = "<group>"; };
 		4B8378E31F3378C4005CA9E4 /* CharacterMapper.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CharacterMapper.cpp; sourceTree = "<group>"; };
 		4B8378E41F3378C4005CA9E4 /* CharacterMapper.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CharacterMapper.hpp; sourceTree = "<group>"; };
+		4B86E2591F8C628F006FAA45 /* Keyboard.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Keyboard.cpp; sourceTree = "<group>"; };
+		4B86E25A1F8C628F006FAA45 /* Keyboard.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Keyboard.hpp; sourceTree = "<group>"; };
 		4B8805EE1DCFC99C003085B1 /* Acorn.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Acorn.cpp; path = Parsers/Acorn.cpp; sourceTree = "<group>"; };
 		4B8805EF1DCFC99C003085B1 /* Acorn.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Acorn.hpp; path = Parsers/Acorn.hpp; sourceTree = "<group>"; };
 		4B8805F21DCFD22A003085B1 /* Commodore.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Commodore.cpp; path = Parsers/Commodore.cpp; sourceTree = "<group>"; };
@@ -1715,6 +1718,16 @@
 			name = Implementation;
 			sourceTree = "<group>";
 		};
+		4B86E2581F8C628F006FAA45 /* Inputs */ = {
+			isa = PBXGroup;
+			children = (
+				4B86E2591F8C628F006FAA45 /* Keyboard.cpp */,
+				4B86E25A1F8C628F006FAA45 /* Keyboard.hpp */,
+			);
+			name = Inputs;
+			path = ../../Inputs;
+			sourceTree = "<group>";
+		};
 		4B8805F11DCFC9A2003085B1 /* Parsers */ = {
 			isa = PBXGroup;
 			children = (
@@ -2070,6 +2083,7 @@
 				4BF660691F281573002CB053 /* ClockReceiver */,
 				4BC9DF4A1D04691600F44158 /* Components */,
 				4B3940E81DA83C8700427841 /* Concurrency */,
+				4B86E2581F8C628F006FAA45 /* Inputs */,
 				4BB73EDC1B587CA500552FC2 /* Machines */,
 				4BB697C81D4B559300248BDF /* NumberTheory */,
 				4B366DFD1B5C165F0026627B /* Outputs */,
@@ -2951,6 +2965,7 @@
 				4BC5E4921D7ED365008CF980 /* StaticAnalyser.cpp in Sources */,
 				4BC830D11D6E7C690000A26F /* Tape.cpp in Sources */,
 				4B69FB441C4D941400B5F0AA /* TapeUEF.cpp in Sources */,
+				4B86E25B1F8C628F006FAA45 /* Keyboard.cpp in Sources */,
 				4B4518851F75E91A00926311 /* DiskController.cpp in Sources */,
 				4B8334841F5DA0360097E338 /* Z80Storage.cpp in Sources */,
 				4BA61EB01D91515900B3C876 /* NSData+StdVector.mm in Sources */,