diff --git a/devTools/FC.ts b/devTools/FC.ts
index a3125ee3a5ec7815ea39d6a05b111eb68da4d2dc..36c899b9169ff5c84097c4634ea8c8fdb6b7c93c 100644
--- a/devTools/FC.ts
+++ b/devTools/FC.ts
@@ -255,6 +255,7 @@ namespace App {
 			declare function makeElement<K extends keyof HTMLElementTagNameMap>(tag: K, content: string | Node, classNames?: string | string[]): HTMLElementTagNameMap[K];
 			declare function appendNewElement<K extends keyof HTMLElementTagNameMap>(tag: K, parent: ParentNode, content?: string | Node, classNames?: string | string[]): HTMLElementTagNameMap[K];
 		}
+		namespace Hotkeys { }
 		namespace View { }
 		namespace SlaveSummary {
 			type AppendRenderer = (slave: FC.SlaveState, parentNode: Node) => void;
diff --git a/src/001-lib/mousetrap/mousetrap.js b/src/001-lib/mousetrap/0_mousetrap.js
similarity index 100%
rename from src/001-lib/mousetrap/mousetrap.js
rename to src/001-lib/mousetrap/0_mousetrap.js
diff --git a/src/001-lib/mousetrap/1_mousetrap-record.min.js b/src/001-lib/mousetrap/1_mousetrap-record.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..76a7d7d8d532e8c43fc6471e6bc897c12f090691
--- /dev/null
+++ b/src/001-lib/mousetrap/1_mousetrap-record.min.js
@@ -0,0 +1 @@
+!function(n){var t=[],e=null,r=[],i=!1,o=null,l=n.prototype.handleKey;function u(n){var t;for(t=0;t<r.length;++t)if(r[t]===n)return;r.push(n),1===n.length&&(i=!0)}function p(){t.push(r),i=!(r=[]),clearTimeout(o),o=setTimeout(h,1e3)}function h(){e&&(function(n){var t;for(t=0;t<n.length;++t)n[t].sort(function(n,t){return!(1<n.length&&1===t.length)&&(1===n.length&&1<t.length||t<n)?1:-1}),n[t]=n[t].join("+")}(t),e(t)),t=[],e=null,r=[]}n.prototype.record=function(n){var t=this;t.recording=!0,e=function(){t.recording=!1,n.apply(t,arguments)}},n.prototype.handleKey=function(){(function(n,t,e){var o;if(this.recording)if("keydown"===e.type){for(e.preventDefault(),1===n.length&&i&&p(),o=0;o<t.length;++o)u(t[o]);u(n)}else"keyup"===e.type&&0<r.length&&p();else l.apply(this,arguments)}).apply(this,arguments)},n.init()}(Mousetrap);
diff --git a/src/002-config/mousetrapConfig.js b/src/002-config/mousetrapConfig.js
index df31dc5fb398961e29312bfaca764e0dab3ac096..c82ac4be193661aef007b894ecbb0a50630f938a 100644
--- a/src/002-config/mousetrapConfig.js
+++ b/src/002-config/mousetrapConfig.js
@@ -1,29 +1,305 @@
-/* eslint-disable */
-Mousetrap.bind("enter", function() {
-	$("#story-caption #endWeekButton a.macro-link").trigger("click");
+/**
+ * Expand mousetrap with multiple binds per key, passage dependent bindings and a menu for custom rebinding.
+ */
+App.UI.Hotkeys = (function() {
+	/**
+	 * @typedef action
+	 * @property {Function} callback
+	 * @property {Array<string>} combinations 0 <= length <= 2
+	 * @property {Array<string>} [passages] not existing means everywhere
+	 * @property {string|function(): string} [uiName] allow different name in hotkey settings
+	 */
+
+	/**
+	 * The key is used in the hotkey settings to describe the action
+	 * @type {object.<string, action>}
+	 */
+	const actions = {};
+
+	/**
+	 * Contains the default combinations for every action
+	 * @type {object.<string, Array<string>>}
+	 */
+	const defaultCombinations = {};
+
+	/**
+	 * References a key combination to a set of actions
+	 * @type {object.<string, Array<string>>}
+	 */
+	const bindings = {};
+
+	/**
+	 * To ensure we only record one at at time
+	 * @type {boolean}
+	 */
+	let recording = false;
+
+	/**
+	 * @param {string} name used as key
+	 * @param {action} action
+	 */
+	function addDefault(name, action) {
+		actions[name] = action;
+		for (const binding of action.combinations) {
+			addBinding(name, binding);
+		}
+		defaultCombinations[name] = [...action.combinations];
+	}
+
+	/**
+	 * @param {string} actionKey
+	 * @param {string} combination
+	 */
+	function addBinding(actionKey, combination) {
+		if (bindings[combination]) {
+			bindings[combination].push(actionKey);
+		} else {
+			bindings[combination] = [actionKey];
+			Mousetrap.bind(combination, e => {
+				e.preventDefault();
+				for (const binding of bindings[combination]) {
+					const action = actions[binding];
+					// only activate callback if we are on the right passage
+					if (!action.passages || action.passages.includes(State.passage)) {
+						action.callback();
+					}
+				}
+			});
+		}
+	}
+
+	/**
+	 * @param {string} actionKey
+	 * @param {string} combination
+	 */
+	function removeBinding(actionKey, combination) {
+		if (bindings[combination]) {
+			const index = bindings[combination].indexOf(actionKey);
+			if (index > -1) {
+				bindings[combination].splice(index, 1);
+				if (bindings[combination].length === 0) {
+					delete bindings[combination];
+					Mousetrap.unbind(combination);
+				}
+			}
+		}
+	}
+
+	/**
+	 * @param {string} name
+	 * @returns {string}
+	 */
+	function hotkeysForAction(name) {
+		if (!actions[name]) {
+			return "";
+		}
+		const c = actions[name].combinations;
+		if (c.length === 0) {
+			return "";
+		}
+		if (c.length === 1) {
+			return `[${formatHotkey(c[0])}]`;
+		}
+		return `[${formatHotkey(c[0])},${formatHotkey(c[1])}]`;
+	}
+
+	/**
+	 * @param {string} combination
+	 * @returns {string}
+	 */
+	function formatHotkey(combination) {
+		const parts = combination.split("+");
+
+		for (let i = 0; i < parts.length; i++) {
+			parts[i] = capFirstChar(parts[i]);
+		}
+
+		return parts.join("+");
+	}
+
+	/**
+	 * @returns {HTMLDivElement}
+	 */
+	function settingsMenu() {
+		const div = document.createElement("div");
+		div.className = "hotkey-settings";
+
+		for (const actionsKey in actions) {
+			settingsRow(div, actionsKey);
+		}
+
+		return div;
+	}
+
+	/**
+	 * @param {HTMLDivElement} container
+	 * @param {string} actionKey
+	 */
+	function settingsRow(container, actionKey) {
+		const action = actions[actionKey];
+		// get correct name
+		let name = actionKey;
+		if (action.uiName) {
+			if (typeof action.uiName === "string") {
+				name = action.uiName;
+			} else {
+				name = action.uiName();
+			}
+		}
+		App.UI.DOM.appendNewElement("div", container, name, "description");
+
+		settingsCell(container, actionKey, 0);
+		settingsCell(container, actionKey, 1);
+
+		const button = App.UI.DOM.appendNewElement("button", container, "Reset");
+		if (isDefault(actionKey)) {
+			button.className = "inactive";
+		} else {
+			button.onclick = () => {
+				action.combinations = [...defaultCombinations[actionKey]];
+				saveToStorage();
+				App.UI.reload();
+			};
+		}
+	}
+
+	/**
+	 * Checks if the combinations assigned to an action are the default ones.
+	 * @param {string} actionKey
+	 * @returns {boolean}
+	 */
+	function isDefault(actionKey) {
+		if (defaultCombinations[actionKey].length !== actions[actionKey].combinations.length) {
+			return false;
+		}
+		if (defaultCombinations[actionKey].length === 0) {
+			return true;
+		}
+		if (defaultCombinations[actionKey][0] !== actions[actionKey].combinations[0]) {
+			return false;
+		}
+		if (defaultCombinations[actionKey].length === 1) {
+			return true;
+		}
+		return defaultCombinations[actionKey][1] === actions[actionKey].combinations[1];
+	}
+
+	/**
+	 * @param {HTMLDivElement} container
+	 * @param {string} actionKey
+	 * @param {number} index
+	 */
+	function settingsCell(container, actionKey, index) {
+		const action = actions[actionKey];
+		const button = App.UI.DOM.appendNewElement("button", container,
+			action.combinations[index] ? formatHotkey(action.combinations[index]) : "", "combination");
+		button.onclick = () => {
+			if (recording) { return; }
+			recording = true;
+
+			$(button).empty();
+			Mousetrap.record(function(sequence) {
+				// sequence is an array like ['ctrl+k', 'c']
+				const combination = sequence.join(" ");
+				if (action.combinations[index]) {
+					removeBinding(actionKey, action.combinations[index]);
+				}
+				action.combinations[index] = combination;
+				addBinding(actionKey, combination);
+				saveToStorage();
+				App.UI.reload();
+				recording = false;
+			});
+		};
+	}
+
+	/**
+	 * Saves custom hotkeys to browser storage
+	 */
+	function saveToStorage() {
+		const save = {};
+
+		for (const actionsKey in actions) {
+			if (!isDefault(actionsKey)) {
+				save[actionsKey] = actions[actionsKey].combinations;
+			}
+		}
+
+		SugarCube.storage.set("hotkeys", save);
+	}
+
+	/**
+	 * Loads custom hotkeys from browser storage
+	 */
+	function loadFromStorage() {
+		const save = SugarCube.storage.get("hotkeys");
+
+		for (const saveKey in save) {
+			// discard obsolete hotkeys
+			if (actions[saveKey]) {
+				actions[saveKey].combinations = save[saveKey];
+			}
+		}
+	}
+
+	/**
+	 * Initialize custom hotkeys
+	 */
+	function init() {
+		loadFromStorage();
+		// :storyready is to late to influence the page, but it's the earliest where SugarCube.storage is available so
+		// we refresh the passage if we happen to be on the settings passage.
+		if (State.passage === "Hotkey Settings") {
+			App.UI.reload();
+		}
+	}
+
+	return {
+		add: addDefault,
+		hotkeys: hotkeysForAction,
+		init: init,
+		settings: settingsMenu,
+	};
+})();
+
+// add hotkeys
+App.UI.Hotkeys.add("endWeek", {
+	callback: function() {
+		$("#story-caption #endWeekButton a.macro-link").trigger("click");
+	}, combinations: ["enter"], uiName: "Next Week"
 });
-Mousetrap.bind("space", function() {
-	$("#story-caption #nextButton a.macro-link").trigger("click");
+App.UI.Hotkeys.add("nextLink", {
+	callback: function() {
+		$("#story-caption #nextButton a.macro-link").trigger("click");
+	}, combinations: ["space"], uiName: "Continue/Back"
 });
-Mousetrap.bind("left", function() {
-	$("#prevSlave a.macro-link").trigger("click");
-	$("#prevChild a.macro-link").trigger("click");
+App.UI.Hotkeys.add("prevSlave", {
+	callback: function() {
+		$("#prevSlave a.macro-link").trigger("click");
+	}, combinations: ["left", "q"], uiName: "Previous Slave"
 });
-Mousetrap.bind("q", function() {
-	$("#prevSlave a.macro-link").trigger("click");
-	$("#prevChild a.macro-link").trigger("click");
+App.UI.Hotkeys.add("nextSlave", {
+	callback: function() {
+		$("#nextSlave a.macro-link").trigger("click");
+	}, combinations: ["right", "e"], uiName: "Next Slave"
 });
-Mousetrap.bind("right", function() {
-	$("#nextSlave a.macro-link").trigger("click");
-	$("#nextChild a.macro-link").trigger("click");
+App.UI.Hotkeys.add("prevChild", {
+	callback: function() {
+		$("#prevChild a.macro-link").trigger("click");
+	}, combinations: ["left", "q"], uiName: "Previous Child"
 });
-Mousetrap.bind("e", function() {
-	$("#nextSlave a.macro-link").trigger("click");
-	$("#nextChild a.macro-link").trigger("click");
+App.UI.Hotkeys.add("nextChild", {
+	callback: function() {
+		$("#nextChild a.macro-link").trigger("click");
+	}, combinations: ["right", "e"], uiName: "Next Child"
 });
-Mousetrap.bind("f", function() {
-	$("#walkpast a.macro-link").trigger("click");
+App.UI.Hotkeys.add("walkpast", {
+	callback: function() {
+		$("#walkpast a.macro-link").trigger("click");
+	}, combinations: ["f"]
 });
-Mousetrap.bind("h", function() {
-	$("#manageHG a").trigger("click");
+App.UI.Hotkeys.add("HG Select", {
+	callback: function() {
+		$("#manageHG a").trigger("click");
+	}, combinations: ["h"]
 });
diff --git a/src/Mods/SpecialForce/SpecialForce.js b/src/Mods/SpecialForce/SpecialForce.js
index 205e8ee1df8a2656e75f1a5a034982bd40889e2f..66d6dd0d2a7242233299df78e9ad1926f17c9963 100644
--- a/src/Mods/SpecialForce/SpecialForce.js
+++ b/src/Mods/SpecialForce/SpecialForce.js
@@ -1,7 +1,10 @@
 // T=SugarCube.State.temporary;
 App.SF.Caps = function() {
-	return capFirstChar(V.SF.Lower);
-}
+	if (V.SF.Lower) {
+		return capFirstChar(V.SF.Lower);
+	}
+	return "no one";
+};
 
 App.SF.Init = function() {
 	V.SF.Toggle = V.SF.Toggle || 0;
diff --git a/src/arcologyBuilding/penthouse.js b/src/arcologyBuilding/penthouse.js
index f4d9e75ca14cfddc5bb908064c603dc88f2c4463..89c3126993199518f6beb71b0535d4894a520fb7 100644
--- a/src/arcologyBuilding/penthouse.js
+++ b/src/arcologyBuilding/penthouse.js
@@ -25,7 +25,7 @@ App.Arcology.Cell.Penthouse = class extends App.Arcology.Cell.BaseCell {
 		const fragment = document.createDocumentFragment();
 
 		const link = App.UI.DOM.passageLink("Penthouse", "Manage Penthouse");
-		const hotkey = App.UI.DOM.makeElement("span", "[P]", "hotkey");
+		const hotkey = App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("Manage Penthouse"), "hotkey");
 		if (V.verticalizeArcologyLinks === 0) {
 			const div = document.createElement("div");
 			div.append(link, " ", hotkey);
diff --git a/src/facilities/nursery/childInteract.tw b/src/facilities/nursery/childInteract.tw
index db203972cd30f9a6dc2811261df9ee9d49be7cde..eac1877bcd5b558ddd48419e6a4672e5ec0af104 100644
--- a/src/facilities/nursery/childInteract.tw
+++ b/src/facilities/nursery/childInteract.tw
@@ -18,7 +18,9 @@
 <</if>>
 
 <center>
-@@.cyan;[←,Q] @@
+<span class="hotkey">
+	<<print App.UI.Hotkeys.hotkeys("prevChild")>>
+</span>
 <span id="prevChild">
 	<b>
 	<<link "Prev" "Previous Child In Line">><</link>>
@@ -30,7 +32,9 @@
 	<<link "Next" "Next Child In Line">><</link>>
 	</b>
 </span>
-@@.cyan; [E,→]@@
+<span class="hotkey">
+	<<print App.UI.Hotkeys.hotkeys("nextChild")>>
+</span>
 </center>
 <br>
 
diff --git a/src/uncategorized/descriptionOptions.tw b/src/gui/options/descriptionOptions.tw
similarity index 100%
rename from src/uncategorized/descriptionOptions.tw
rename to src/gui/options/descriptionOptions.tw
diff --git a/src/gui/options/hotkeySettings.css b/src/gui/options/hotkeySettings.css
new file mode 100644
index 0000000000000000000000000000000000000000..08df500e4bc6e6c637c2739c4ff61c1f29494414
--- /dev/null
+++ b/src/gui/options/hotkeySettings.css
@@ -0,0 +1,42 @@
+div.hotkey-settings {
+    display: grid;
+    grid-template-columns: repeat(3, max-content) auto;
+}
+
+@media only screen and (min-width: 1600px) {
+    div.hotkey-settings {
+        grid-template-columns: repeat(3, max-content) auto repeat(3, max-content) auto;
+    }
+}
+
+div.hotkey-settings div.description {
+    margin-right: 10px;
+    /* center text vertically */
+    display: flex;
+    justify-content: center;
+    flex-direction: column;
+}
+
+div.hotkey-settings button {
+    margin: 5px;
+    border-width: 2px;
+    background-color: var(--button-color);
+    border-color: var(--button-border-color);
+    width: 70px;
+}
+
+div.hotkey-settings button.combination {
+    min-width: 150px;
+    border-width: 0;
+}
+
+div.hotkey-settings button.inactive, div.hotkey-settings button.inactive:hover {
+    background-color: var(--button-selected-color);
+    cursor: default;
+
+}
+
+div.hotkey-settings button:hover {
+    background-color: var(--button-hover-color);
+    border-color: var(--button-border-color);
+}
diff --git a/src/gui/options/hotkeySettings.tw b/src/gui/options/hotkeySettings.tw
new file mode 100644
index 0000000000000000000000000000000000000000..b1bf6388eacc1f4df01bd3ee5d965dceab84e39e
--- /dev/null
+++ b/src/gui/options/hotkeySettings.tw
@@ -0,0 +1,27 @@
+:: Hotkey Settings [nobr jump-to-safe jump-from-safe]
+
+<<set $nextButton = "Back", $nextLink = "Main">>
+
+<h1>Hotkey Settings</h1>
+
+<p>
+    <ul>
+        <li>
+            On keyboard layouts other than the <a href="https://en.wikipedia.org/wiki/File:KB_United_States.svg"
+            target="_blank">US-QWERTY layout</a>  there may be keys or combinations of keys where the recorded key is
+            different from the key used to listen to key events. You will have to find these keys yourself through trial
+            and error.
+        </li>
+        <li>
+            Custom hotkeys are browser specific and are not part of your save.
+        </li>
+        <li>
+            While we try not to overwrite browser or OS level key combinations it is possible to do so with custom
+            hotkeys. This also means that during recording of custom hotkeys no browser or OS level key combinations are
+            available. There are however keys that cannot be overwritten, the <code>Win key</code> on Windows is an
+            example for this.
+        </li>
+    </ul>
+</p>
+
+<<includeDOM App.UI.Hotkeys.settings()>>
diff --git a/src/gui/options.js b/src/gui/options/options.js
similarity index 100%
rename from src/gui/options.js
rename to src/gui/options/options.js
diff --git a/src/uncategorized/options.tw b/src/gui/options/options.tw
similarity index 100%
rename from src/uncategorized/options.tw
rename to src/gui/options/options.tw
diff --git a/src/uncategorized/summaryOptions.tw b/src/gui/options/summaryOptions.tw
similarity index 100%
rename from src/uncategorized/summaryOptions.tw
rename to src/gui/options/summaryOptions.tw
diff --git a/src/gui/quicklinks.js b/src/gui/quicklinks.js
index 5fda6b661f6c7dc1a85bb4b9f96c3142d5a7b9de..f8e0347679c5c0c2ad1d6d7f37ff9be19edc7847 100644
--- a/src/gui/quicklinks.js
+++ b/src/gui/quicklinks.js
@@ -17,7 +17,7 @@ App.UI.quickMenu = (function() {
 	const noHistory = Story.lookup("tags", "no-history").map(passage => passage.title);
 
 	// if property name is a passage name, then it's a link, otherwise only text.
-	// category titles  are never links to passages
+	// category titles are never links to passages
 	// Only two values are allowed: true or an object following the same rules
 	const layout = addOtherCategory({
 		Main: true,
@@ -77,6 +77,7 @@ App.UI.quickMenu = (function() {
 			"Summary Options": true,
 			"Description Options": true,
 			"Universal Rules": true,
+			"Hotkey Settings": true,
 		}
 	});
 
@@ -177,49 +178,6 @@ App.UI.quickMenu = (function() {
 		"Manage Corporation": () => V.corp.SpecToken > 0 && V.corp.SpecTimer === 0,
 	});
 
-	// setup hotkeys list, upper/lower case is important!
-	// Due to limitation to the key capture library keys cannot be used when they are already used in
-	// src/002-config/mousetrapConfig.js
-	const hotkeys = cleanPassageMapping({
-		"BG Select": "b",
-		"Buy Slaves": "s",
-		edicts: "E",
-		Firebase: "z",
-		"Future Society": "f",
-		Main: "m",
-		"Manage Arcology": "c",
-		"Manage Corporation":"C",
-		"Manage Penthouse": "p",
-		"Manage Personal Affairs": "x",
-		"Neighbor Interact": "d",
-		Options: "o",
-		"Personal assistant options": "t",
-		"Personal Attention Select": "a",
-		Policies: "y",
-		propagandaHub: "H",
-		"Recruiter Select": "u",
-		riotControlCenter: "R",
-		"Rules Assistant": "r",
-		secBarracks: "A",
-		securityHQ: "S",
-		"Universal Rules": "v",
-		// Facilities
-		Brothel: "1",
-		Club: "2",
-		Arcade: "3",
-		Dairy: "4",
-		Farmyard: "5",
-		"Servants' Quarters": "6",
-		"Master Suite": "7",
-		Schoolroom: "8",
-		Spa: "9",
-		Nursery: "0",
-		Clinic: "shift+1",
-		Cellblock: "shift+2",
-		Incubator: "shift+3",
-		Pit: "shift+4",
-	});
-
 	/**
 	 * The DOM element of name of the currently played passage or any of it's parents. Used during generation to
 	 * uncollapse the category with the current passage.
@@ -235,14 +193,75 @@ App.UI.quickMenu = (function() {
 	let hotkeysEnabled = false;
 
 	// register hotkeys
-	for (const passage in hotkeys) {
-		Mousetrap.bind(hotkeys[passage], () => {
-			if (hotkeysEnabled
-				// the passage is accessible
-				&& !(hiddenPassages[passage] && hiddenPassages[passage]())) {
-				Engine.play(passage);
-			}
+	// this is in it's own scope as we can forget the hotkeys object immediately afterwards
+	{
+		// setup hotkeys list, upper/lower case is important!
+		const hotkeys = cleanPassageMapping({
+			"BG Select": "b",
+			"Buy Slaves": "s",
+			edicts: "shift+e",
+			Firebase: "z",
+			"Future Society": "f",
+			Main: "m",
+			"Manage Arcology": "c",
+			"Manage Corporation": "shift+c",
+			"Manage Penthouse": "p",
+			"Manage Personal Affairs": "x",
+			"Neighbor Interact": "d",
+			Options: "o",
+			"Personal assistant options": "t",
+			"Personal Attention Select": "a",
+			Policies: "y",
+			propagandaHub: "shift+h",
+			"Recruiter Select": "u",
+			riotControlCenter: "shift+r",
+			"Rules Assistant": "r",
+			secBarracks: "shift+a",
+			securityHQ: "shift+s",
+			"Universal Rules": "v",
+			// Facilities
+			Brothel: "1",
+			Club: "2",
+			Arcade: "3",
+			Dairy: "4",
+			Farmyard: "5",
+			"Servants' Quarters": "6",
+			"Master Suite": "7",
+			Schoolroom: "8",
+			Spa: "9",
+			Nursery: "0",
+			Clinic: "shift+1",
+			Cellblock: "shift+2",
+			Incubator: "shift+3",
+			Pit: "shift+4",
 		});
+
+		// register
+		for (const passage of jumpTo) {
+			if (!hidden.includes(passage)) {
+				const action = {
+					callback: () => {
+						if (hotkeysEnabled
+							// we are not already on the passage
+							&& State.passage !== passage
+							// the passage is accessible
+							&& !(hiddenPassages[passage] && hiddenPassages[passage]())) {
+							Engine.play(passage);
+						}
+					},
+					combinations: [],
+				};
+				// add hotkey if there is one
+				if (hotkeys[passage]) {
+					action.combinations.push(hotkeys[passage]);
+				}
+				// custom ui text
+				if (uiNames[passage]) {
+					action.uiName = uiNames[passage];
+				}
+				App.UI.Hotkeys.add(passage, action);
+			}
+		}
 	}
 
 	// setup history
@@ -268,10 +287,7 @@ App.UI.quickMenu = (function() {
 			history.shift();
 		}
 	});
-	Mousetrap.bind("backspace", () => {
-		// jump back in history
-		goBack();
-	});
+	App.UI.Hotkeys.add("historyBack", {callback: goBack, combinations: ["backspace"], uiName: "Back in history"});
 
 	/**
 	 * Goes back in history if possible.
@@ -294,7 +310,10 @@ App.UI.quickMenu = (function() {
 			const a = document.createElement("a");
 			a.append("Return");
 			a.onclick = goBack;
-			div.append(a, " ", App.UI.DOM.makeElement("span", "[backspace]", "hotkey"));
+			const hotkey = App.UI.Hotkeys.hotkeys("historyBack");
+			if (hotkey !== "") {
+				div.append(a, " ", App.UI.DOM.makeElement("span", hotkey, "hotkey"));
+			}
 			// insert at second position
 			linkList.splice(1, 0, div);
 		}
@@ -447,8 +466,9 @@ App.UI.quickMenu = (function() {
 			Engine.play(passage);
 		};
 		div.prepend(a);
-		if (hotkeys[passage]) {
-			div.append(" ", App.UI.DOM.makeElement("span", `[${hotkeys[passage]}]`, "hotkey"));
+		const hotkeyString = App.UI.Hotkeys.hotkeys(passage);
+		if (hotkeyString !== "") {
+			div.append(" ", App.UI.DOM.makeElement("span", hotkeyString, "hotkey"));
 		}
 	}
 
diff --git a/src/gui/storyCaptionWidgets.tw b/src/gui/storyCaptionWidgets.tw
index a8616c836cd97a0ccaa8c7a5a96f418853a33889..3242e2b91f508d773fa094bc13b6f1a8f560b3f0 100644
--- a/src/gui/storyCaptionWidgets.tw
+++ b/src/gui/storyCaptionWidgets.tw
@@ -7,7 +7,14 @@
 	<<if _Pass != "End Week">>
 		<<if _Pass == "Main">>
 			<strong>
-				<div id="endWeekButton"><<link "END WEEK">><<run endWeek()>><</link>> @@.cyan;[Ent]@@</div>
+				<div id="endWeekButton">	
+					<<link "END WEEK">>
+						<<run endWeek()>>
+					<</link>>
+					<span class="hotkey">
+						<<print App.UI.Hotkeys.hotkeys("endWeek")>>
+					</span>
+				</div>
 			</strong>
 			<<if $rulesAssistantAuto == 1 && DefaultRulesError()>>
 				<div>@@.yellow;WARNING: Rules Assistant has rules with errors!@@</div>
@@ -17,7 +24,10 @@
 			<<if $nextButton != " ">>
 				<<link "$nextButton">>
 					<<goto $nextLink>>
-				<</link>> @@.cyan;[Space]@@
+				<</link>> 
+				<span class="hotkey">
+					<<print App.UI.Hotkeys.hotkeys("nextLink")>>
+				</span>
 			<</if>>
 			</div></strong>
 		<</if>>
diff --git a/src/interaction/main/mainLinks.js b/src/interaction/main/mainLinks.js
index a41eff26a9c25f0eebc58391665fbfcca9973862..d2be8893bee20de2008b2bc227d5a8d62731d11f 100644
--- a/src/interaction/main/mainLinks.js
+++ b/src/interaction/main/mainLinks.js
@@ -85,7 +85,7 @@ App.UI.View.mainLinks = function() {
 	if (V.PC.health.shortDamage < 30) {
 		const link = App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Change plans", "Personal Attention Select"), "major-link");
 		link.id = "managePA";
-		fragment.append(" ", link, " ", App.UI.DOM.makeElement("span", "[A]", "hotkey"));
+		fragment.append(" ", link, " ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("Personal Attention"), "hotkey"));
 	}
 
 	if (V.useSlaveSummaryOverviewTab === 0) {
@@ -97,7 +97,7 @@ App.UI.View.mainLinks = function() {
 			}
 			div.append(". ",
 				App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Manage Head Girl", "HG Select"), "major-link"),
-				" ", App.UI.DOM.makeElement("span", "[H]", "hotkey"));
+				" ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("HG Select"), "hotkey"));
 			div.id = "manageHG";
 		} else if (V.slaves.length > 1) {
 			div.append(`You have not selected a Head Girl`);
@@ -106,7 +106,7 @@ App.UI.View.mainLinks = function() {
 			}
 			div.append(". ",
 				App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Select One", "HG Select"), "major-link"),
-				" ", App.UI.DOM.makeElement("span", "[H]", "hotkey"));
+				" ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("HG Select"), "hotkey"));
 			div.id = "manageHG";
 		} else {
 			div.append(App.UI.DOM.makeElement("span", "You do not have enough slaves to keep a Head Girl", "note"));
@@ -121,7 +121,7 @@ App.UI.View.mainLinks = function() {
 			div.append("You have not selected a Recruiter. ",
 				App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Select one", "Recruiter Select"), "major-link"));
 		}
-		div.append(" ", App.UI.DOM.makeElement("span", "[U]", "hotkey"));
+		div.append(" ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("Recruiter Select"), "hotkey"));
 		div.id = "manageRecruiter";
 		fragment.append(div);
 
@@ -134,7 +134,7 @@ App.UI.View.mainLinks = function() {
 				div.append("You have not selected a Bodyguard. ",
 					App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Select one", "BG Select"), "major-link"));
 			}
-			div.append(" ", App.UI.DOM.makeElement("span", "[B]", "hotkey"));
+			div.append(" ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("BG Select"), "hotkey"));
 			div.id = "manageBG";
 			fragment.append(div);
 		}
diff --git a/src/js/main.js b/src/js/main.js
index 1854c4e6343b57ee130cb45a5faad2fcb66b0a0e..2dea7157fec6271d7db5200973c606b85d512b5d 100644
--- a/src/js/main.js
+++ b/src/js/main.js
@@ -225,7 +225,7 @@ App.MainView.full = function() {
 			const raLink = document.createElement("span");
 			raLink.id = "RAButton";
 			raLink.append(" | ", App.UI.DOM.passageLink("Rules Assistant Options", "Rules Assistant"),
-				" ", App.UI.DOM.makeElement("span", "[R]", ["clear-formatting", "hotkey"]));
+				" ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("Rules Assistant"), ["clear-formatting", "hotkey"]));
 			span.append(raLink);
 
 			if (V.rulesAssistantAuto !== 1) {
diff --git a/src/js/slaveListing.js b/src/js/slaveListing.js
index 09d47f01788c95c72f6eff1d037af2e1ac7842f6..ee2d2088cd5d0ce7e2ca51b30cb08ea2a8239823 100644
--- a/src/js/slaveListing.js
+++ b/src/js/slaveListing.js
@@ -864,7 +864,7 @@ App.UI.SlaveList.penthousePage = function() {
 			slaveWrapper.append(". ");
 			const link = App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Manage Head Girl", "HG Select"), "major-link");
 			link.id = "manageHG";
-			slaveWrapper.append(link, " ", App.UI.DOM.makeElement("span", "[H]", "hotkey"));
+			slaveWrapper.append(link, " ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("HG Select"), "hotkey"));
 			slaveWrapper.append(App.UI.SlaveList.render.listDOM([HG.ID], [],
 				App.UI.SlaveList.SlaveInteract.penthouseInteract));
 		} else {
@@ -874,7 +874,7 @@ App.UI.SlaveList.penthousePage = function() {
 					slaveWrapper.append(" and Consort");
 				}
 				slaveWrapper.append(". ", App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Select One", "HG Select"), "major-link"),
-					" ", App.UI.DOM.makeElement("span", "[H]", "hotkey"));
+					" ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("HG Select"), "hotkey"));
 				slaveWrapper.id = "manageHG";
 				if (V.slavePanelStyle === 2) {
 					slaveWrapper.classList.add("slaveSummary", "card");
@@ -907,13 +907,13 @@ App.UI.SlaveList.penthousePage = function() {
 			slaveWrapper.append(". ");
 			const link = App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Manage Recruiter", "Recruiter Select"), "major-link");
 			link.id = "manageRecruiter";
-			slaveWrapper.append(link, " ", App.UI.DOM.makeElement("span", "[U]", "hotkey"));
+			slaveWrapper.append(link, " ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("Recruiter Select"), "hotkey"));
 			slaveWrapper.append(App.UI.SlaveList.render.listDOM([RC.ID], [],
 				App.UI.SlaveList.SlaveInteract.penthouseInteract));
 		} else {
 			slaveWrapper.append("You have ", App.UI.DOM.makeElement("span", "not", "warning"), " selected a Recruiter. ",
 				App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Select one", "Recruiter Select"), "major-link"),
-				" ", App.UI.DOM.makeElement("span", "[U]", "hotkey"));
+				" ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("Recruiter Select"), "hotkey"));
 			slaveWrapper.id = "manageRecruiter";
 			if (V.slavePanelStyle === 2) {
 				slaveWrapper.classList.add("slaveSummary", "card");
@@ -930,14 +930,14 @@ App.UI.SlaveList.penthousePage = function() {
 					" is serving as your bodyguard. ");
 				const link = App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Manage Bodyguard", "BG Select"), "major-link");
 				link.id = "manageBG";
-				slaveWrapper.append(link, " ", App.UI.DOM.makeElement("span", "[B]", "hotkey"));
+				slaveWrapper.append(link, " ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("BG Select"), "hotkey"));
 				slaveWrapper.append(App.UI.SlaveList.render.listDOM([BG.ID], [],
 					App.UI.SlaveList.SlaveInteract.penthouseInteract));
 				slaveWrapper.append(App.MainView.useGuard());
 			} else {
 				slaveWrapper.append("You have ", App.UI.DOM.makeElement("span", "not", "warning"), " selected a Bodyguard. ",
 					App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Select one", "BG Select"), "major-link"),
-					" ", App.UI.DOM.makeElement("span", "[B]", "hotkey"));
+					" ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("BG Select"), "hotkey"));
 				slaveWrapper.id = "manageBG";
 				if (V.slavePanelStyle === 2) {
 					slaveWrapper.classList.add("slaveSummary", "card");
diff --git a/src/uncategorized/slaveInteract.tw b/src/uncategorized/slaveInteract.tw
index 3052912793446c8b4de549988409c765eda39cec..ea3c2115bf8e613d1f03a6fc621b52ab78d9124f 100644
--- a/src/uncategorized/slaveInteract.tw
+++ b/src/uncategorized/slaveInteract.tw
@@ -62,8 +62,8 @@
 			[[Cheat Edit Slave Alternative|MOD_Edit Slave Cheat New][$cheater = 1]]
 		</div>
 	<</if>>
-	<span class="cyan">
-		[←,Q]
+	<span class="hotkey">
+		<<print App.UI.Hotkeys.hotkeys("prevSlave")>>
 	</span>
 	<span id="prevSlave" style="font-weight:bold">
 		<<link "Prev" "Slave Interact">><<set $activeSlave = getSlave(_slavesInLine[0])>><</link>>
@@ -76,8 +76,8 @@
 	<span id="nextSlave" style="font-weight:bold">
 		<<link "Next" "Slave Interact">><<set $activeSlave = getSlave(_slavesInLine[1])>><</link>>
 	</span>
-	<span class="cyan">
-		[E,→]
+	<span class="hotkey">
+		<<print App.UI.Hotkeys.hotkeys("nextSlave")>>
 	</span>
 </p>
 <div class="tabbar">
diff --git a/src/zz1-last/setupEventHandlers.js b/src/zz1-last/setupEventHandlers.js
index 0a6ef4160227ad480de47f3b895c24cc870cd2ba..593787eae840212bd4fdbfc1beb51a72738348e5 100644
--- a/src/zz1-last/setupEventHandlers.js
+++ b/src/zz1-last/setupEventHandlers.js
@@ -4,6 +4,7 @@ Config.saves.onSave = App.EventHandlers.onSave;
 
 $(document).on(':storyready', function() {
 	App.EventHandlers.storyReady();
+	App.UI.Hotkeys.init();
 });
 
 $(document).one(':passagestart', function() {