diff --git a/include/functions.php b/include/functions.php
index f0bd31831ab22d76ec18f3b16c133adf0beda350..ac9cce90f04300e2c9c87f163f36b6c6ebe48c32 100644
--- a/include/functions.php
+++ b/include/functions.php
@@ -1963,12 +1963,66 @@
 		$params["num_feeds"] = (int) $num_feeds;
 
 		$params["collapsed_feedlist"] = (int) get_pref($link, "_COLLAPSED_FEEDLIST");
+		$params["hotkeys"] = get_hotkeys($link);
 
 		$params["csrf_token"] = $_SESSION["csrf_token"];
 
 		return $params;
 	}
 
+	function get_hotkeys($link) {
+		$hotkeys = array(
+			"navigation" => array(
+				"next_feed" => "k",
+				"prev_feed" => "j",
+				"next_article" => "n",
+				"prev_article" => "p",
+				"search_dialog" => "/"),
+			"article" => array(
+				"toggle_mark" => "s",
+				"toggle_publ" => "S",
+				"toggle_unread" => "u",
+				"edit_tags" => "T",
+				"dismiss_selected" => "D",
+				"dismiss_read" => "X",
+				"open_in_new_window" => "o",
+				"catchup_below" => "c p",
+				"catchup_above" => "c n",
+				"email_article" => "e"),
+			"article_selection" => array(
+				"select_all" => "a a",
+				"select_unread" => "a u",
+				"select_marked" => "a U",
+				"select_published" => "a p",
+				"select_invert" => "a i",
+				"select_none" => "a n"),
+			"feed" => array(
+				"feed_refresh" => "f r",
+				"feed_unhide_read" => "f a",
+				"feed_subscribe" => "f s",
+				"feed_edit" => "f e",
+				"feed_catchup" => "f q",
+				"feed_reverse" => "f x",
+				"catchup_all" => "Q",
+				"cat_toggle_collapse" => "x"),
+			"goto" => array(
+				"goto_all" => "g a",
+				"goto_fresh" => "g f",
+				"goto_marked" => "g s",
+				"goto_published" => "g p",
+				"goto_tagcloud" => "g t",
+				"goto_prefs" => "g P"),
+			"other" => array(
+				"select_article_cursor" => "(9)", // tab
+				"create_label" => "c l",
+				"create_filter" => "c f",
+				"collapse_sidebar" => "c s",
+				"help_dialog" => "(191)")
+			);
+
+		return $hotkeys;
+	}
+
 	function make_runtime_info($link) {
 		$data = array();
 
diff --git a/js/tt-rss.js b/js/tt-rss.js
index 56f7df65d5293427cc808cb550da49fae299fac4..67fbea28e4d6d8df677adc743b2253919ee5528b 100644
--- a/js/tt-rss.js
+++ b/js/tt-rss.js
@@ -647,13 +647,134 @@ function hotkey_handler(e) {
 		if (keycode == 16) return; // ignore lone shift
 		if (keycode == 17) return; // ignore lone ctrl
 
-		if ((keycode == 70 || keycode == 67 || keycode == 71 || keycode == 65)
+		if (!shift_key) keychar = keychar.toLowerCase();
+
+		if (!hotkey_prefix && ["a", "f", "g", "c"].indexOf(keychar) != -1) {
+
+			var date = new Date();
+			var ts = Math.round(date.getTime() / 1000);
+
+			hotkey_prefix = keychar;
+			hotkey_prefix_pressed = ts;
+
+			cmdline.innerHTML = keychar;
+			Element.show(cmdline);
+
+			return true;
+		}
+
+		Element.hide(cmdline);
+
+		var hotkey = keychar.search(/[a-zA-Z0-9]/) != -1 ? keychar : "(" + keycode + ")";
+		hotkey = hotkey_prefix ? hotkey_prefix + " " + hotkey : hotkey;
+		hotkey_prefix = false;
+
+		var hotkey_action = false;
+		var hotkeys = getInitParam("hotkeys");
+
+		for (cat in hotkeys) {
+			for (action in hotkeys[cat]) {
+				if (hotkeys[cat][action] == hotkey) {
+					hotkey_action = action;
+					break;
+				}
+			}
+		}
+
+		switch (hotkey_action) {
+		case "next_feed":
+			return true;
+		case "prev_feed":
+			return true;
+		case "next_article":
+			return true;
+		case "prev_article":
+			return true;
+		case "search_dialog":
+			return true;
+		case "toggle_mark":
+			return true;
+		case "toggle_publ":
+			return true;
+		case "toggle_unread":
+			return true;
+		case "edit_tags":
+			return true;
+		case "dismiss_selected":
+			return true;
+		case "dismiss_read":
+			return true;
+		case "open_in_new_window":
+			return true;
+		case "catchup_below":
+			return true;
+		case "catchup_above":
+			return true;
+		case "email_article":
+			return true;
+		case "select_all":
+			return true;
+		case "select_unread":
+			return true;
+		case "select_marked":
+			return true;
+		case "select_published":
+			return true;
+		case "select_invert":
+			return true;
+		case "select_none":
+			return true;
+		case "feed_refresh":
+			return true;
+		case "feed_unhide_read":
+			return true;
+		case "feed_subscribe":
+			quickAddFeed();
+			return true;
+		case "feed_edit":
+			return true;
+		case "feed_catchup":
+			return true;
+		case "feed_reverse":
+			return true;
+		case "catchup_all":
+			return true;
+		case "cat_toggle_collapse":
+			return true;
+		case "goto_all":
+			return true;
+		case "goto_fresh":
+			return true;
+		case "goto_marked":
+			return true;
+		case "goto_published":
+			return true;
+		case "goto_tagcloud":
+			return true;
+		case "goto_prefs":
+			return true;
+		case "select_article_cursor":
+			return true;
+		case "create_label":
+			return true;
+		case "create_filter":
+			return true;
+		case "collapse_sidebar":
+			return true;
+		case "help_dialog":
+			return true;
+		default:
+			console.log("unhandled action: " + hotkey_action + "; hotkey: " + hotkey);
+		}
+
+
+/*		if ((keycode == 70 || keycode == 67 || keycode == 71 || keycode == 65)
 				&& !hotkey_prefix) {
 
 			var date = new Date();
 			var ts = Math.round(date.getTime() / 1000);
 
-			hotkey_prefix = keycode;
+			hotkey_prefix = keychar;
 			hotkey_prefix_pressed = ts;
 
 			cmdline.innerHTML = keychar;
@@ -667,10 +788,12 @@ function hotkey_handler(e) {
 			Element.hide("hotkey_help_overlay");
 		}
 
-		/* Global hotkeys */
-
 		Element.hide(cmdline);
 
+
+		/* Global hotkeys */
+		return;
+
 		if (!hotkey_prefix) {
 
 			if (keycode == 27) { // escape