summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--TODO2
-rwxr-xr-xranger.py4
-rw-r--r--ranger/__main__.py15
-rw-r--r--ranger/api/keys.py27
-rw-r--r--ranger/container/__init__.py4
-rw-r--r--ranger/container/commandlist.py225
-rw-r--r--ranger/container/keybuffer.py197
-rw-r--r--ranger/container/keymap.py165
-rw-r--r--ranger/core/actions.py159
-rw-r--r--ranger/core/environment.py11
-rw-r--r--ranger/core/fm.py20
-rw-r--r--ranger/defaults/commands.py4
-rw-r--r--ranger/defaults/keys.py614
-rw-r--r--ranger/defaults/options.py4
-rw-r--r--ranger/ext/accumulator.py42
-rw-r--r--ranger/ext/direction.py141
-rw-r--r--ranger/ext/keybinding_parser.py96
-rw-r--r--ranger/ext/move.py23
-rw-r--r--ranger/ext/tree.py136
-rw-r--r--ranger/fsobject/directory.py3
-rw-r--r--ranger/gui/defaultui.py4
-rw-r--r--ranger/gui/ui.py83
-rw-r--r--ranger/gui/widgets/browsercolumn.py12
-rw-r--r--ranger/gui/widgets/browserview.py8
-rw-r--r--ranger/gui/widgets/console.py82
-rw-r--r--ranger/gui/widgets/pager.py119
-rw-r--r--ranger/gui/widgets/statusbar.py2
-rw-r--r--ranger/gui/widgets/taskview.py44
-rw-r--r--ranger/shared/settings.py1
-rw-r--r--test/tc_commandlist.py100
-rw-r--r--test/tc_direction.py88
-rw-r--r--test/tc_newkeys.py605
-rw-r--r--test/tc_ui.py2
33 files changed, 2065 insertions, 977 deletions
diff --git a/TODO b/TODO
index 715ebc1b..cab7eb04 100644
--- a/TODO
+++ b/TODO
@@ -51,6 +51,8 @@ General
    (X) #71  10/03/21  previews: black/whitelist + read file
    (X) #79  10/04/08  tab number zero
    ( ) #80  10/04/08  when closing tabs, avoid gaps?
+   ( ) #81  10/04/15  system crash when previewing /proc/kcore with root permissions
+
 
 
 Bugs
diff --git a/ranger.py b/ranger.py
index 636bd384..06b86531 100755
--- a/ranger.py
+++ b/ranger.py
@@ -34,13 +34,11 @@ return 1
 # embed a shellscript.
 __doc__ = """Ranger - file browser for the unix terminal"""
 
-
 # Importing the main method may fail if the ranger directory
 # is neither in the same directory as this file, nor in one of
 # pythons global import paths.
 try:
 	from ranger.__main__ import main
-
 except ImportError:
 	import sys
 	if '-d' not in sys.argv and '--debug' not in sys.argv:
@@ -49,7 +47,5 @@ except ImportError:
 		print("launch ranger.py in the top directory.")
 	else:
 		raise
-
 else:
 	main()
-
diff --git a/ranger/__main__.py b/ranger/__main__.py
index 11d2d288..ae3f3221 100644
--- a/ranger/__main__.py
+++ b/ranger/__main__.py
@@ -67,13 +67,19 @@ def load_settings(fm):
 		except ImportError:
 			from ranger.defaults import commands
 		try:
-			import keys
-		except ImportError:
-			from ranger.defaults import keys
-		try:
 			import apps
 		except ImportError:
 			from ranger.defaults import apps
+
+		from ranger import shared, api
+		from ranger.api import keys
+		keymanager = shared.EnvironmentAware.env.keymanager
+		keys.keymanager = keymanager
+		from ranger.defaults import keys
+		try:
+			import keys
+		except ImportError:
+			pass
 		del sys.path[0]
 	else:
 		from ranger.defaults import commands, keys, apps
@@ -137,6 +143,7 @@ def main():
 		path = '.'
 
 	EnvironmentAware._assign(Environment(path))
+	SettingsAware._setup_keys()
 
 	try:
 		fm = FM()
diff --git a/ranger/api/keys.py b/ranger/api/keys.py
index 5b833c34..92a0269c 100644
--- a/ranger/api/keys.py
+++ b/ranger/api/keys.py
@@ -21,6 +21,7 @@ from inspect import getargspec, ismethod
 from ranger import RANGERDIR
 from ranger.gui.widgets import console_mode as cmode
 from ranger.container.bookmarks import ALLOWED_KEYS as ALLOWED_BOOKMARK_KEYS
+from ranger.container.keymap import KeyMap, Direction, KeyMapWithDirections
 
 class Wrapper(object):
 	def __init__(self, firstattr):
@@ -33,10 +34,11 @@ class Wrapper(object):
 			def function(command_argument):
 				args, kws = real_args, real_keywords
 				number = command_argument.n
+				direction = command_argument.direction
 				obj = getattr(command_argument, self.__firstattr__)
 				fnc = getattr(obj, attr)
-				if number is not None:
-					args, kws = replace_narg(number, fnc, args, kws)
+				if number is not None or direction is not None:
+					args, kws = replace_narg(number, direction, fnc, args, kws)
 				return fnc(*args, **kws)
 			return function
 		return wrapper
@@ -54,14 +56,15 @@ class Wrapper(object):
 # If the method has an argument named "narg", pressing a number before
 # the key will pass that number as the narg argument. If you want the
 # same behaviour in a custom lambda function, you can write:
-# bind('gg', fm.move_pointer(absolute=0))
+# bind('gg', fm.move(to=0))
 # as:
-# bind('gg', lambda arg: narg(arg.n, arg.fm.move_pointer, absolute=0))
+# bind('gg', lambda arg: narg(arg.n, arg.fm.move, to=0))
 
 fm = Wrapper('fm')
 wdg = Wrapper('wdg')
 
 
+DIRARG_KEYWORD = 'dirarg'
 NARG_KEYWORD = 'narg'
 
 def narg(number_, function_, *args_, **keywords_):
@@ -77,7 +80,7 @@ def narg(number_, function_, *args_, **keywords_):
 	args, keywords = replace_narg(number_, function_, args_, keywords_)
 	return function_(*args, **keywords)
 
-def replace_narg(number, function, args, keywords):
+def replace_narg(number, direction, function, args, keywords):
 	"""
 	This function returns (args, keywords) with one little change:
 	if <function> has a named argument called "narg", args and keywords
@@ -92,10 +95,10 @@ def replace_narg(number, function, args, keywords):
 	=> (1, 666), {}
 	"""
 	argspec = getargspec(function).args
-	if NARG_KEYWORD in argspec:
+	args = list(args)
+	if number is not None and NARG_KEYWORD in argspec:
 		try:
 			# is narg in args?
-			args = list(args)
 			index = argspec.index(NARG_KEYWORD)
 			if ismethod(function):
 				index -= 1  # because of 'self'
@@ -104,4 +107,14 @@ def replace_narg(number, function, args, keywords):
 			# is narg in keywords?
 			keywords = dict(keywords)
 			keywords[NARG_KEYWORD] = number
+	if direction is not None and DIRARG_KEYWORD in argspec:
+		try:
+			index = argspec.index(DIRARG_KEYWORD)
+			if ismethod(function):
+				index -= 1  # because of 'self'
+			args[index] = direction
+		except (ValueError, IndexError):
+			# is narg in keywords?
+			keywords = dict(keywords)
+			keywords[DIRARG_KEYWORD] = direction
 	return args, keywords
diff --git a/ranger/container/__init__.py b/ranger/container/__init__.py
index 51122291..3351cc63 100644
--- a/ranger/container/__init__.py
+++ b/ranger/container/__init__.py
@@ -17,6 +17,6 @@
 used to manage stored data
 """
 from ranger.container.history import History
-from ranger.container.keybuffer import KeyBuffer
-from .commandlist import CommandList
+from .keymap import KeyMap, KeyManager
+from .keybuffer import KeyBuffer
 from .bookmarks import Bookmarks
diff --git a/ranger/container/commandlist.py b/ranger/container/commandlist.py
deleted file mode 100644
index eb289c46..00000000
--- a/ranger/container/commandlist.py
+++ /dev/null
@@ -1,225 +0,0 @@
-# Copyright (C) 2009, 2010  Roman Zimbelmann <romanz@lavabit.com>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-from ranger.ext.openstruct import OpenStruct
-
-class CommandArgument(object):
-	def __init__(self, fm, displayable, keybuffer):
-		self.fm = fm
-		self.wdg = displayable
-		self.keybuffer = keybuffer
-		self.n = keybuffer.number
-		self.keys = str(keybuffer)
-
-def cmdarg(displayable):
-	return CommandArgument(displayable.fm, \
-			displayable, displayable.env.keybuffer)
-
-class CommandList(object):
-	"""
-	CommandLists are dictionary-like objects which give you a command
-	for a given key combination.  CommandLists must be filled before use.
-	"""
-
-	dummy_object = None
-	dummies_in_paths = False
-	paths = {}
-	commandlist = []
-
-	def __init__(self):
-		self.commandlist = []
-		self.paths = {}
-
-	def __getitem__(self, key):
-		"""Returns the command with the given key combination"""
-		if isinstance(key, str):
-			key = self._str_to_tuple(key)
-		return self.paths[key]
-
-	def rebuild_paths(self):
-		"""
-		Fill the path dictionary with dummie objects.
-		We need to know when to clear the keybuffer (when a wrong key is pressed)
-		and when to wait for the rest of the key combination.  For "gg" we
-		will assign "g" to a dummy which tells the program to do the latter
-		and wait.
-		"""
-		if self.dummies_in_paths:
-			self.remove_dummies()
-
-		for cmd in self.commandlist:
-			for key in cmd.keys:
-				for path in self._keypath(key):
-					if path not in self.paths:
-						self.paths[path] = self.dummy_object
-
-		self.dummies_in_paths = True
-
-	def _keypath(self, tup):
-		"""split a tuple like (a,b,c,d) into [(a,), (a,b), (a,b,c)]"""
-		length = len(tup)
-
-		if length == 0:
-			return ()
-		if length == 1:
-			return (tup, )
-
-		current = []
-		all = []
-
-		for i in range(len(tup) - 1):
-			current.append(tup[i])
-			all.append(tuple(current))
-
-		return all
-
-	def remove_dummies(self):
-		"""
-		Remove dummie objects in case you have to rebuild a path dictionary
-		which already contains dummie objects.
-		"""
-		for k in tuple(self.paths.keys()):
-			if self.paths[k] == self.dummy_object: del self.paths[k]
-		self.dummies_in_paths = False
-
-	def __call__(self, *args, **keywords):
-		if keywords:
-			self.show(*args, **keywords)
-		else:
-			lastarg = args[-1]
-			if hasattr(lastarg, '__call__'):
-				# do the binding
-				self.bind(lastarg, *args[:-1])
-			else:
-				# act as a decorator. eg:
-				#    @bind('a')
-				#    def do_stuff(arg):
-				#       arg.fm.ui.do_stuff()
-				#
-				# is equivalent to:
-				#    bind('a', lambda arg: arg.fm.ui.do_stuff())
-				return lambda fnc: self.bind(fnc, *args)
-
-	def _str_to_tuple(self, obj):
-		"""splits a string into a tuple of integers"""
-		if isinstance(obj, tuple):
-			return obj
-		elif isinstance(obj, str):
-			return tuple(map(ord, obj))
-		elif isinstance(obj, int):
-			return (obj, )
-		else:
-			raise TypeError('need a str, int or tuple for str_to_tuple')
-
-	def bind(self, fnc, *keys):
-		"""create a Command object and assign it to the given key combinations."""
-		if len(keys) == 0: return
-
-		keys = tuple(map(self._str_to_tuple, keys))
-
-		cmd = Command(fnc, keys)
-
-		self.commandlist.append(cmd)
-		for key in cmd.keys:
-			self.paths[key] = cmd
-
-	def show(self, *keys, **keywords):
-		"""create a Show object and assign it to the given key combinations."""
-		if len(keys) == 0: return
-
-		keys = tuple(map(self._str_to_tuple, keys))
-
-		obj = Show(keywords, keys)
-
-		self.commandlist.append(obj)
-		for key in obj.keys:
-			self.paths[key] = obj
-
-	def alias(self, existing, *new):
-		"""bind the <new> keys to the command of the <existing> key"""
-		existing = self._str_to_tuple(existing)
-		new = tuple(map(self._str_to_tuple, new))
-
-		obj = AliasedCommand(_make_getter(self.paths, existing), new)
-
-		self.commandlist.append(obj)
-		for key in new:
-			self.paths[key] = obj
-
-	def unbind(self, *keys):
-		i = len(self.commandlist)
-		keys = set(map(self._str_to_tuple, keys))
-
-		while i > 0:
-			i -= 1
-			cmd = self.commandlist[i]
-			cmd.keys -= keys
-			if not cmd.keys:
-				del self.commandlist[i]
-
-		for k in keys:
-			del self.paths[k]
-
-	def clear(self):
-		"""remove all bindings"""
-		self.paths.clear()
-		del self.commandlist[:]
-
-
-class Command(object):
-	"""Command objects store information about a command"""
-
-	keys = []
-
-	def __init__(self, fnc, keys):
-		self.keys = set(keys)
-		self.execute = fnc
-
-	def execute(self, *args):
-		"""Execute the command"""
-
-	def execute_wrap(self, displayable):
-		self.execute(cmdarg(displayable))
-
-
-class AliasedCommand(Command):
-	def __init__(self, getter, keys):
-		self.getter = getter
-		self.keys = set(keys)
-
-	def get_execute(self):
-		return self.getter()
-
-	execute = property(get_execute)
-
-
-class Show(object):
-	"""Show objects do things without clearing the keybuffer"""
-
-	keys = []
-	text = ''
-
-	def __init__(self, dictionary, keys):
-		self.keys = set(keys)
-		self.show_obj = OpenStruct(dictionary)
-
-
-def _make_getter(paths, key):
-	def getter():
-		try:
-			return paths[key].execute
-		except:
-			return lambda: None
-	return getter
diff --git a/ranger/container/keybuffer.py b/ranger/container/keybuffer.py
index 2992aea2..23b82a16 100644
--- a/ranger/container/keybuffer.py
+++ b/ranger/container/keybuffer.py
@@ -13,59 +13,168 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-def to_string(i):
-	try:
-		return chr(i)
-	except ValueError:
-		return '?'
-
+import curses.ascii
 from collections import deque
-from curses.ascii import ascii
+from string import digits
+from ranger.ext.keybinding_parser import parse_keybinding, \
+		DIRKEY, ANYKEY, PASSIVE_ACTION
+from ranger.container.keymap import Binding, KeyMap # mainly for assertions
 
-ZERO = ord('0')
-NINE = ord('9')
+MAX_ALIAS_RECURSION = 20
+digitlist = set(ord(n) for n in digits)
 
 class KeyBuffer(object):
-	def __init__(self):
-		self.number = None
-		self.queue = deque()
-		self.queue_with_numbers = deque()
+	"""The evaluator and storage for pressed keys"""
+	def __init__(self, keymap, direction_keys):
+		self.assign(keymap, direction_keys)
 
-	def clear(self):
-		"""Clear the keybuffer and restore the initial state"""
-		self.number = None
-		self.queue.clear()
-		self.queue_with_numbers.clear()
-
-	def append(self, key):
-		"""
-		Append a key to the keybuffer, or initial numbers to
-		the number attribute.
-		"""
-		self.queue_with_numbers.append(key)
-
-		if not self.queue and key >= ZERO and key <= NINE:
-			if self.number is None:
-				self.number = 0
-			try:
-				self.number = self.number * 10 + int(chr(key))
-			except ValueError:
+	def assign(self, keymap, direction_keys):
+		"""Change the keymap and direction keys of the keybuffer"""
+		self.keymap = keymap
+		self.direction_keys = direction_keys
+
+	def add(self, key):
+		"""Add a key and evaluate it"""
+		assert isinstance(key, int)
+		assert key >= 0
+		self.all_keys.append(key)
+		self.key_queue.append(key)
+		while self.key_queue:
+			key = self.key_queue.popleft()
+
+			# evaluate quantifiers
+			if self.eval_quantifier and self._do_eval_quantifier(key):
 				return
+
+			# evaluate the command
+			if self.eval_command and self._do_eval_command(key):
+				return
+
+			# evaluate (the first number of) the direction-quantifier
+			if self.eval_quantifier and self._do_eval_quantifier(key):
+				return
+
+			# evaluate direction keys {j,k,gg,pagedown,...}
+			if not self.eval_command:
+				self._do_eval_direction(key)
+
+	def _do_eval_direction(self, key):
+		try:
+			assert isinstance(self.dir_tree_pointer, dict)
+			self.dir_tree_pointer = self.dir_tree_pointer[key]
+		except KeyError:
+			self.failure = True
 		else:
-			self.queue.append(key)
+			self._direction_try_to_finish()
+
+	def _direction_try_to_finish(self):
+		if self.max_alias_recursion <= 0:
+			self.failure = True
+			return None
+		match = self.dir_tree_pointer
+		assert isinstance(match, (Binding, dict, KeyMap))
+		if isinstance(match, KeyMap):
+			self.dir_tree_pointer = self.dir_tree_pointer._tree
+			match = self.dir_tree_pointer
+		if isinstance(self.dir_tree_pointer, Binding):
+			if match.alias:
+				self.key_queue.extend(parse_keybinding(match.alias))
+				self.dir_tree_pointer = self.direction_keys._tree
+				self.max_alias_recursion -= 1
+			else:
+				direction = match.actions['dir'].copy()
+				if self.direction_quant is not None:
+					direction.multiply(self.direction_quant)
+				self.directions.append(direction)
+				self.direction_quant = None
+				self.eval_command = True
+				self._try_to_finish()
+
+	def _do_eval_quantifier(self, key):
+		if self.eval_command:
+			tree = self.tree_pointer
+		else:
+			tree = self.dir_tree_pointer
+		if key in digitlist and ANYKEY not in tree:
+			attr = self.eval_command and 'quant' or 'direction_quant'
+			if getattr(self, attr) is None:
+				setattr(self, attr, 0)
+			setattr(self, attr, getattr(self, attr) * 10 + key - 48)
+		else:
+			self.eval_quantifier = False
+			return None
+		return True
+
+	def _do_eval_command(self, key):
+		assert isinstance(self.tree_pointer, dict), self.tree_pointer
+		try:
+			self.tree_pointer = self.tree_pointer[key]
+		except TypeError:
+			self.failure = True
+			return None
+		except KeyError:
+			try:
+				key in digitlist or self.direction_keys._tree[key]
+				self.tree_pointer = self.tree_pointer[DIRKEY]
+			except KeyError:
+				try:
+					self.tree_pointer = self.tree_pointer[ANYKEY]
+				except KeyError:
+					self.failure = True
+					return None
+				else:
+					self.matches.append(key)
+					assert isinstance(self.tree_pointer, (Binding, dict))
+					self._try_to_finish()
+			else:
+				assert isinstance(self.tree_pointer, (Binding, dict))
+				self.eval_command = False
+				self.eval_quantifier = True
+				self.dir_tree_pointer = self.direction_keys._tree
+		else:
+			if isinstance(self.tree_pointer, dict):
+				try:
+					self.command = self.tree_pointer[PASSIVE_ACTION]
+				except (KeyError, TypeError):
+					self.command = None
+			self._try_to_finish()
+
+	def _try_to_finish(self):
+		if self.max_alias_recursion <= 0:
+			self.failure = True
+			return None
+		assert isinstance(self.tree_pointer, (Binding, dict, KeyMap))
+		if isinstance(self.tree_pointer, KeyMap):
+			self.tree_pointer = self.tree_pointer._tree
+		if isinstance(self.tree_pointer, Binding):
+			if self.tree_pointer.alias:
+				keys = parse_keybinding(self.tree_pointer.alias)
+				self.key_queue.extend(keys)
+				self.tree_pointer = self.keymap._tree
+				self.max_alias_recursion -= 1
+			else:
+				self.command = self.tree_pointer
+				self.done = True
+
+	def clear(self):
+		"""Reset the keybuffer.  Do this once before the first usage."""
+		self.max_alias_recursion = MAX_ALIAS_RECURSION
+		self.failure = False
+		self.done = False
+		self.quant = None
+		self.matches = []
+		self.command = None
+		self.direction_quant = None
+		self.directions = []
+		self.all_keys = []
+		self.tree_pointer = self.keymap._tree
+		self.dir_tree_pointer = self.direction_keys._tree
 
-	def tuple_with_numbers(self):
-		"""Get a tuple of ascii codes."""
-		return tuple(self.queue_with_numbers)
+		self.key_queue = deque()
 
-	def tuple_without_numbers(self):
-		"""
-		Get a tuple of ascii codes.
-		If the keybuffer starts with numbers, those will
-		be left out. To access them, use keybuffer.number
-		"""
-		return tuple(self.queue)
+		self.eval_quantifier = True
+		self.eval_command = True
 
 	def __str__(self):
 		"""returns a concatenation of all characters"""
-		return "".join( map( to_string, self.queue_with_numbers ) )
+		return "".join("{0:c}".format(c) for c in self.all_keys)
diff --git a/ranger/container/keymap.py b/ranger/container/keymap.py
new file mode 100644
index 00000000..e44fcfd7
--- /dev/null
+++ b/ranger/container/keymap.py
@@ -0,0 +1,165 @@
+# Copyright (C) 2009, 2010  Roman Zimbelmann <romanz@lavabit.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from ranger.ext.tree import Tree
+from ranger.ext.direction import Direction
+from ranger.ext.keybinding_parser import parse_keybinding, DIRKEY, ANYKEY
+
+FUNC = 'func'
+DIRARG = 'dir'
+ALIASARG = 'alias'
+
+class CommandArgs(object):
+	"""
+	A CommandArgs object is passed to the keybinding function.
+
+	This object simply aggregates information about the pressed keys
+	and the current environment.
+
+	Attributes:
+	fm: the FM instance
+	wdg: the currently focused widget (or fm, if none is focused)
+	keybuffer: the keybuffer object
+	n: the prefixed number, eg 5 in the command "5yy"
+	directions: a list of directions which are entered for "<dir>"
+	direction: the first direction object from that list
+	keys: a string representation of the keybuffer
+	matches: all keys which are entered for "<any>"
+	match: the first match
+	binding: the used Binding object
+	"""
+	def __init__(self, fm, widget, keybuf):
+		self.fm = fm
+		self.wdg = widget
+		self.keybuffer = keybuf
+		self.n = keybuf.quant
+		self.direction = keybuf.directions and keybuf.directions[0] or None
+		self.directions = keybuf.directions
+		self.keys = str(keybuf)
+		self.matches = keybuf.matches
+		self.match = keybuf.matches and keybuf.matches[0] or None
+		self.binding = keybuf.command
+
+	@staticmethod
+	def from_widget(widget):
+		return CommandArgs(widget.fm, \
+				widget, widget.env.keybuffer)
+
+
+class KeyMap(Tree):
+	"""Contains a tree with all the keybindings"""
+	def map(self, *args, **keywords):
+		if keywords:
+			return self._add_binding(*args, **keywords)
+		firstarg = args[-1]
+		if hasattr(firstarg, '__call__'):
+			keywords[FUNC] = firstarg
+			return self._add_binding(*args[:-1], **keywords)
+		def decorator_function(func):
+			keywords = {FUNC:func}
+			self.map(*args, **keywords)
+			return func
+		return decorator_function
+
+	__call__ = map
+
+	def _add_binding(self, *keys, **actions):
+		assert keys
+		bind = Binding(keys, actions)
+		for key in keys:
+			self.set(parse_keybinding(key), bind)
+
+	def unmap(self, *keys):
+		for key in keys:
+			self.unset(parse_keybinding(key))
+
+	def __getitem__(self, key):
+		return self.traverse(parse_keybinding(key))
+
+
+class KeyMapWithDirections(KeyMap):
+	def __init__(self, *args, **keywords):
+		Tree.__init__(self, *args, **keywords)
+		self.directions = KeyMap()
+
+	def merge(self, other):
+		assert hasattr(other, 'directions'), 'Merging with wrong type?'
+		Tree.merge(self, other)
+		Tree.merge(self.directions, other.directions)
+
+	def dir(self, *args, **keywords):
+		if ALIASARG in keywords:
+			self.directions.map(*args, **keywords)
+		else:
+			self.directions.map(*args, dir=Direction(**keywords))
+
+
+class KeyManager(object):
+	def __init__(self, keybuffer, contexts):
+		self._keybuffer = keybuffer
+		self._list_of_contexts = contexts
+		self.clear()
+
+	def clear(self):
+		self.contexts = dict()
+		for context in self._list_of_contexts:
+			self.contexts[context] = KeyMapWithDirections()
+
+	def map(self, context, *args, **keywords):
+		self.get_context(context).map(*args, **keywords)
+
+	def dir(self, context, *args, **keywords):
+		self.get_context(context).dir(*args, **keywords)
+
+	def unmap(self, context, *args, **keywords):
+		self.get_context(context).unmap(*args, **keywords)
+
+	def merge_all(self, keymapwithdirection):
+		for context, keymap in self.contexts.items():
+			keymap.merge(keymapwithdirection)
+
+	def get_context(self, context):
+		assert isinstance(context, str)
+		assert context in self.contexts, "no such context: " + context
+		return self.contexts[context]
+	__getitem__ = get_context
+
+	def use_context(self, context):
+		context = self.get_context(context)
+		if self._keybuffer.keymap is not context:
+			self._keybuffer.assign(context, context.directions)
+			self._keybuffer.clear()
+
+
+class Binding(object):
+	"""The keybinding object"""
+	def __init__(self, keys, actions):
+		assert hasattr(keys, '__iter__')
+		assert isinstance(actions, dict)
+		self.actions = actions
+		try:
+			self.function = self.actions[FUNC]
+		except KeyError:
+			self.function = None
+		try:
+			self.direction = self.actions[DIRARG]
+		except KeyError:
+			self.direction = None
+		try:
+			alias = self.actions[ALIASARG]
+		except KeyError:
+			self.alias = None
+		else:
+			self.alias = tuple(parse_keybinding(alias))
diff --git a/ranger/core/actions.py b/ranger/core/actions.py
index 029493b0..c87021bc 100644
--- a/ranger/core/actions.py
+++ b/ranger/core/actions.py
@@ -20,6 +20,7 @@ from os import symlink, getcwd
 from inspect import cleandoc
 
 import ranger
+from ranger.ext.direction import Direction
 from ranger import fsobject
 from ranger.shared import FileManagerAware, EnvironmentAware, SettingsAware
 from ranger.gui.widgets import console_mode as cmode
@@ -27,19 +28,46 @@ from ranger.fsobject import File
 from ranger.ext import shutil_generatorized as shutil_g
 from ranger.fsobject.loader import LoadableObject
 
-class Actions(EnvironmentAware, SettingsAware):
+class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 	search_method = 'ctime'
 	search_forward = False
 
 	# --------------------------
 	# -- Backwards Compatibility
 	# --------------------------
+	# All methods defined here are just for backwards compatibility,
+	# allowing old configuration files to work with newer versions.
+	# You can delete them and they should change nothing if you use
+	# an up-to-date configuration file.
 
 	def dummy(self, *args, **keywords):
 		"""For backwards compatibility only."""
 
 	handle_mouse = resize = dummy
 
+	def move_left(self, narg=1):
+		"""Enter the parent directory"""
+		self.move(left=1, narg=narg)
+
+	def move_right(self, narg=None):
+		"""Enter the current directory or execute the current file"""
+		self.move(right=1, narg=narg)
+
+	def move_pointer(self, relative=0, absolute=None, narg=None):
+		"""Move the pointer down by <relative> or to <absolute>"""
+		dct = dict(down=relative, narg=narg)
+		if absolute is not None:
+			dct['to'] = absolute
+		self.move(**dct)
+
+	def move_pointer_by_pages(self, relative):
+		"""Move the pointer down by <relative> pages"""
+		self.move(down=relative, pages=True)
+
+	def move_pointer_by_percentage(self, relative=0, absolute=None, narg=None):
+		"""Move the pointer down to <absolute>%"""
+		self.move(to=absolute, percentage=True, narg=narg)
+
 	# --------------------------
 	# -- Basic Commands
 	# --------------------------
@@ -76,10 +104,10 @@ class Actions(EnvironmentAware, SettingsAware):
 		"""Redraw the window"""
 		self.ui.redraw_window()
 
-	def open_console(self, mode=':', string=''):
+	def open_console(self, mode=':', string='', prompt=None):
 		"""Open the console if the current UI supports that"""
 		if hasattr(self.ui, 'open_console'):
-			self.ui.open_console(mode, string)
+			self.ui.open_console(mode, string, prompt=prompt)
 
 	def execute_file(self, files, **kw):
 		"""Execute a file.
@@ -99,48 +127,55 @@ class Actions(EnvironmentAware, SettingsAware):
 	# -- Moving Around
 	# --------------------------
 
-	def move_left(self, narg=1):
-		"""Enter the parent directory"""
-		try:
-			directory = os.path.join(*(['..'] * narg))
-		except:
-			return
-		self.env.enter_dir(directory)
-
-	def move_right(self, mode=0, narg=None):
-		"""Enter the current directory or execute the current file"""
-		cf = self.env.cf
-		sel = self.env.get_selection()
-
-		if isinstance(narg, int):
-			mode = narg
-		if not self.env.enter_dir(cf):
-			if sel:
-				if self.execute_file(sel, mode=mode) is False:
-					self.open_console(cmode.OPEN_QUICK)
+	def move(self, narg=None, **kw):
+		"""
+		A universal movement method.
 
-	def move_pointer(self, relative = 0, absolute = None, narg=None):
-		"""Move the pointer down by <relative> or to <absolute>"""
-		self.env.cwd.move(relative=relative,
-				absolute=absolute, narg=narg)
+		Accepts these parameters:
+		(int) down, (int) up, (int) left, (int) right, (int) to,
+		(bool) absolute, (bool) relative, (bool) pages,
+		(bool) percentage
 
-	def move_pointer_by_pages(self, relative):
-		"""Move the pointer down by <relative> pages"""
-		self.env.cwd.move(relative=int(relative * self.env.termsize[0]))
+		to=X is translated to down=X, absolute=True
 
-	def move_pointer_by_percentage(self, relative=0, absolute=None, narg=None):
-		"""Move the pointer down by <relative>% or to <absolute>%"""
-		try:
-			factor = len(self.env.cwd) / 100.0
-		except:
+		Example:
+		self.move(down=4, pages=True)  # moves down by 4 pages.
+		self.move(to=2, pages=True)  # moves to page 2.
+		self.move(to=1, percentage=True)  # moves to 80%
+		"""
+		cwd = self.env.cwd
+		if not cwd or not cwd.accessible or not cwd.content_loaded:
 			return
 
-		if narg is not None:
-			absolute = narg
+		direction = Direction(kw)
+		if 'left' in direction or direction.left() > 0:
+			steps = direction.left()
+			if narg is not None:
+				steps *= narg
+			try:
+				directory = os.path.join(*(['..'] * steps))
+			except:
+				return
+			self.env.enter_dir(directory)
+
+		elif 'right' in direction:
+			mode = 0
+			if narg is not None:
+				mode = narg
+			cf = self.env.cf
+			selection = self.env.get_selection()
+			if not self.env.enter_dir(cf) and selection:
+				if self.execute_file(selection, mode=mode) is False:
+					self.open_console(cmode.OPEN_QUICK)
 
-		self.env.cwd.move(
-				relative=int(relative * factor),
-				absolute=int(absolute * factor))
+		elif direction.vertical():
+			newpos = direction.move(
+					direction=direction.down(),
+					override=narg,
+					maximum=len(cwd),
+					current=cwd.pointer,
+					pagesize=self.ui.browser.hei)
+			cwd.move(to=newpos)
 
 	def history_go(self, relative):
 		"""Move back and forth in the history"""
@@ -172,16 +207,16 @@ class Actions(EnvironmentAware, SettingsAware):
 			self.enter_dir(cf.path)
 		elif cwd.pointer >= len(cwd) - 1:
 			while True:
-				self.enter_dir('..')
+				self.move(left=1)
 				cwd = self.env.cwd
 				if cwd.pointer < len(cwd) - 1:
 					break
 				if cwd.path == '/':
 					break
-			self.move_pointer(1)
+			self.move(down=1)
 			self.traverse()
 		else:
-			self.move_pointer(1)
+			self.move(down=1)
 			self.traverse()
 
 	# --------------------------
@@ -201,6 +236,9 @@ class Actions(EnvironmentAware, SettingsAware):
 			return
 		self.execute_file(file, app = 'editor')
 
+	def hint(self, text):
+		self.ui.hint(text)
+
 	def toggle_boolean_option(self, string):
 		"""Toggle a boolean option named <string>"""
 		if isinstance(self.env.settings[string], bool):
@@ -262,7 +300,7 @@ class Actions(EnvironmentAware, SettingsAware):
 						cwd.mark_item(item, val)
 
 		if movedown:
-			self.move_pointer(relative=narg)
+			self.move(down=narg)
 
 		if hasattr(self.ui, 'redraw_main_column'):
 			self.ui.redraw_main_column()
@@ -338,7 +376,7 @@ class Actions(EnvironmentAware, SettingsAware):
 		if movedown is None:
 			movedown = len(sel) == 1
 		if movedown:
-			self.move_pointer(relative=1)
+			self.move(down=1)
 
 		if hasattr(self.ui, 'redraw_main_column'):
 			self.ui.redraw_main_column()
@@ -355,7 +393,7 @@ class Actions(EnvironmentAware, SettingsAware):
 		if movedown is None:
 			movedown = len(sel) == 1
 		if movedown:
-			self.move_pointer(relative=1)
+			self.move(down=1)
 
 		if hasattr(self.ui, 'redraw_main_column'):
 			self.ui.redraw_main_column()
@@ -384,6 +422,12 @@ class Actions(EnvironmentAware, SettingsAware):
 		"""Delete the bookmark with the name <key>"""
 		self.bookmarks.delete(key)
 
+	def draw_bookmarks(self):
+		self.ui.browser.draw_bookmarks = True
+
+	def hide_bookmarks(self):
+		self.ui.browser.draw_bookmarks = False
+
 	# --------------------------
 	# -- Pager
 	# --------------------------
@@ -508,16 +552,29 @@ class Actions(EnvironmentAware, SettingsAware):
 		self.env.cut = False
 		self.ui.browser.main_column.request_redraw()
 
-	def copy(self):
+	def copy(self, narg=None, dirarg=None):
 		"""Copy the selected items"""
-
-		selected = self.env.get_selection()
-		self.env.copy = set(f for f in selected if f in self.env.cwd.files)
+		cwd = self.env.cwd
+		if not narg and not dirarg:
+			selected = (f for f in self.env.get_selection() if f in cwd.files)
+		else:
+			if not dirarg and narg:
+				direction = Direction(down=1)
+				offset = 0
+			else:
+				direction = Direction(dirarg)
+				offset = 1
+			pos, selected = direction.select(
+					override=narg, lst=cwd.files, current=cwd.pointer,
+					pagesize=self.env.termsize[0], offset=offset)
+			self.env.cwd.pointer = pos
+			self.env.cwd.correct_pointer()
+		self.env.copy = set(selected)
 		self.env.cut = False
 		self.ui.browser.main_column.request_redraw()
 
-	def cut(self):
-		self.copy()
+	def cut(self, narg=None, dirarg=None):
+		self.copy(narg=narg, dirarg=dirarg)
 		self.env.cut = True
 		self.ui.browser.main_column.request_redraw()
 
diff --git a/ranger/core/environment.py b/ranger/core/environment.py
index 49d92502..49bc8a5c 100644
--- a/ranger/core/environment.py
+++ b/ranger/core/environment.py
@@ -20,10 +20,13 @@ import socket
 from os.path import abspath, normpath, join, expanduser, isdir
 
 from ranger.fsobject.directory import Directory, NoDirectoryGiven
-from ranger.container import KeyBuffer, History
+from ranger.container import KeyBuffer, KeyManager, History
 from ranger.ext.signal_dispatcher import SignalDispatcher
 from ranger.shared import SettingsAware
 
+ALLOWED_CONTEXTS = ('general', 'pager', 'embedded_pager', 'taskview',
+		'console')
+
 class Environment(SettingsAware, SignalDispatcher):
 	"""A collection of data which is relevant for more than
 	one class.
@@ -40,6 +43,7 @@ class Environment(SettingsAware, SignalDispatcher):
 	pathway = None
 	path = None
 	keybuffer = None
+	keymanager = None
 
 	def __init__(self, path):
 		SignalDispatcher.__init__(self)
@@ -47,7 +51,8 @@ class Environment(SettingsAware, SignalDispatcher):
 		self._cf = None
 		self.pathway = ()
 		self.directories = {}
-		self.keybuffer = KeyBuffer()
+		self.keybuffer = KeyBuffer(None, None)
+		self.keymanager = KeyManager(self.keybuffer, ALLOWED_CONTEXTS)
 		self.copy = set()
 		self.history = History(self.settings.max_history_size)
 
@@ -81,7 +86,7 @@ class Environment(SettingsAware, SignalDispatcher):
 		if key == curses.KEY_RESIZE:
 			self.keybuffer.clear()
 
-		self.keybuffer.append(key)
+		self.keybuffer.add(key)
 
 	def key_clear(self):
 		"""Clear the keybuffer"""
diff --git a/ranger/core/fm.py b/ranger/core/fm.py
index 2cb3eea7..ab418c89 100644
--- a/ranger/core/fm.py
+++ b/ranger/core/fm.py
@@ -19,7 +19,6 @@ The File Manager, putting the pieces together
 
 from time import time
 from collections import deque
-from curses import KEY_MOUSE, KEY_RESIZE
 import os
 import sys
 
@@ -99,6 +98,11 @@ class FM(Actions, SignalDispatcher):
 		self.input_blocked = sec != 0
 		self.input_blocked_until = time() + sec
 
+	def input_is_blocked(self):
+		if self.input_blocked and time() > self.input_blocked_until:
+			self.input_blocked = False
+		return self.input_blocked
+
 	def loop(self):
 		"""
 		The main loop consists of:
@@ -135,19 +139,7 @@ class FM(Actions, SignalDispatcher):
 
 				ui.set_load_mode(loader.has_work())
 
-				key = ui.get_next_key()
-
-				if key > 0:
-					if key == KEY_MOUSE:
-						ui.handle_mouse()
-					elif key == KEY_RESIZE:
-						ui.update_size()
-					else:
-						if self.input_blocked and \
-								time() > self.input_blocked_until:
-							self.input_blocked = False
-						if not self.input_blocked:
-							ui.handle_key(key)
+				ui.handle_input()
 
 				gc_tick += 1
 				if gc_tick > TICKS_BEFORE_COLLECTING_GARBAGE:
diff --git a/ranger/defaults/commands.py b/ranger/defaults/commands.py
index 2bb860bd..f04c4889 100644
--- a/ranger/defaults/commands.py
+++ b/ranger/defaults/commands.py
@@ -202,7 +202,7 @@ class find(Command):
 		self.fm.search_method = 'search'
 
 		if self.count == 1:
-			self.fm.move_right()
+			self.fm.move(right=1)
 			self.fm.block_input(0.5)
 
 	def quick_open(self):
@@ -227,7 +227,7 @@ class find(Command):
 			if arg in filename:
 				self.count += 1
 				if self.count == 1:
-					cwd.move(absolute=(cwd.pointer + i) % len(cwd.files))
+					cwd.move(to=(cwd.pointer + i) % len(cwd.files))
 					self.fm.env.cf = cwd.pointed_obj
 			if self.count > 1:
 				return False
diff --git a/ranger/defaults/keys.py b/ranger/defaults/keys.py
index 0c805cb6..61138007 100644
--- a/ranger/defaults/keys.py
+++ b/ranger/defaults/keys.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 # Copyright (C) 2009, 2010  Roman Zimbelmann <romanz@lavabit.com>
 #
 # This program is free software: you can redistribute it and/or modify
@@ -17,314 +18,327 @@
 This is the default key configuration file of ranger.
 Syntax for binding keys: map(*keys, fnc)
 
-keys are one or more key-combinations which are either:
-* a string
-* an integer which represents an ascii code
-* a tuple of integers
+Examples for keys: "x", "gg", "<C-J><A-4>", "<tab>", "<down><up><right>"
 
-fnc is a function which is called with the CommandArgument object.
+fnc is a function which is called with the CommandArgs object.
 
-The CommandArgument object has these attributes:
+The CommandArgs object has these attributes:
 arg.fm: the file manager instance
 arg.wdg: the current widget
 arg.n: the number typed before the key combination (if allowed)
+arg.direction: the direction object (if applicable)
 arg.keys: the string representation of the used key combination
 arg.keybuffer: the keybuffer instance
 
-Check ranger.keyapi for more information
+Direction keys are special.  They must be mapped with: map.dir(*keys, **args)
+where args is a dict of values such as up, down, to, absolute, relative...
+Example: map.dir('gg', to=0)
+Direction keys can be accessed in a mapping that contians "<dir>".
+
+Additionally, there are shortcuts for accessing methods of the current
+file manager and widget instance:
+map('xyz', fm.method(foo=bar))
+will be translated to:
+map('xyz', lamdba arg: arg.fm.method(foo=bar))
+If possible, arg.n and arg.direction are automatically inserted.
+
+
+Example scenario
+----------------
+If this keys are defined:
+map("dd", "d<dir>", fm.cut(foo=bar))
+map.dir("gg", to=0)
+
+Type in the keys on the left and the function on the right will be executed:
+dd        => fm.cut(foo=bar)
+5dd       => fm.cut(foo=bar, narg=5)
+dgg       => fm.cut(foo=bar, dirarg=Direction(to=0))
+5dgg      => fm.cut(foo=bar, narg=5, dirarg=Direction(to=0))
+5d3gg     => fm.cut(foo=bar, narg=5, dirarg=Direction(to=3))
 """
 
-# NOTE: The "map" object used below is a callable CommandList
-# object and NOT the builtin python map function!
-
 from ranger.api.keys import *
-
-def _vimlike_aliases(map):
-	alias = map.alias
-
-	# the key 'k' will always do the same as KEY_UP, etc.
-	alias(KEY_UP, 'k')
-	alias(KEY_DOWN, 'j')
-	alias(KEY_LEFT, 'h')
-	alias(KEY_RIGHT, 'l')
-
-	alias(KEY_NPAGE, ctrl('f'))
-	alias(KEY_PPAGE, ctrl('b'))
-	alias(KEY_HOME, 'gg')
-	alias(KEY_END, 'G')
-
-def initialize_commands(map):
-	"""Initialize the commands for the main user interface"""
-
-	# -------------------------------------------------------- movement
-	_vimlike_aliases(map)
-	map.alias(KEY_LEFT, KEY_BACKSPACE, DEL)
-
-	map(KEY_DOWN, fm.move_pointer(relative=1))
-	map(KEY_UP, fm.move_pointer(relative=-1))
-	map(KEY_RIGHT, KEY_ENTER, ctrl('j'), fm.move_right())
-	map(KEY_LEFT, KEY_BACKSPACE, DEL, fm.move_left(1))
-
-	map(KEY_HOME, fm.move_pointer(absolute=0))
-	map(KEY_END, fm.move_pointer(absolute=-1))
-
-	map('%', fm.move_pointer_by_percentage(absolute=50))
-	map(KEY_NPAGE, ctrl('f'), fm.move_pointer_by_pages(1))
-	map(KEY_PPAGE, ctrl('b'), fm.move_pointer_by_pages(-1))
-	map(ctrl('d'), 'J', fm.move_pointer_by_pages(0.5))
-	map(ctrl('u'), 'K', fm.move_pointer_by_pages(-0.5))
-
-	def move_parent(n):
-		def fnc(arg):
-			arg.fm.move_left()
-			arg.fm.move_pointer(n)
-			if arg.fm.env.cf.is_directory:
-				arg.fm.move_right()
-		return fnc
-
-	map(']', move_parent(1))
-	map('[', move_parent(-1))
-	map('}', fm.traverse())
-	map('{', fm.history_go(-1))
-
-	# --------------------------------------------------------- history
-	map('H', fm.history_go(-1))
-	map('L', fm.history_go(1))
-
-	# ----------------------------------------------- tagging / marking
-	map('t', fm.tag_toggle())
-	map('T', fm.tag_remove())
-
-	map(' ', fm.mark(toggle=True))
-	map('v', fm.mark(all=True, toggle=True))
-	map('V', fm.mark(all=True, val=False))
-
-	# ------------------------------------------ file system operations
-	map('yy', fm.copy())
-	map('dd', fm.cut())
-	map('ud', fm.uncut())
-	map('pp', fm.paste())
-	map('po', fm.paste(overwrite=True))
-	map('pl', fm.paste_symlink())
-	map('p', hint='press //p// once again to confirm pasting' \
-			', or //l// to create symlinks')
-
-	# ---------------------------------------------------- run programs
-	map('S', fm.execute_command(os.environ['SHELL']))
-	map('E', fm.edit_file())
-	map(',term', fm.execute_command('x-terminal-emulator', flags='d'))
-	map('du', fm.execute_command('du --max-depth=1 -h | less'))
-
-	# -------------------------------------------------- toggle options
-	map('b', fm.notify('Warning: settings are now changed with z!', bad=True))
-	map('z', hint="show_//h//idden //p//review_files //d//irectories_first " \
-		"//c//ollapse_preview flush//i//nput ca//s//e_insensitive")
-	map('zh', fm.toggle_boolean_option('show_hidden'))
-	map('zp', fm.toggle_boolean_option('preview_files'))
-	map('zP', fm.toggle_boolean_option('preview_directories'))
-	map('zi', fm.toggle_boolean_option('flushinput'))
-	map('zd', fm.toggle_boolean_option('sort_directories_first'))
-	map('zc', fm.toggle_boolean_option('collapse_preview'))
-	map('zs', fm.toggle_boolean_option('sort_case_insensitive'))
-
-	# ------------------------------------------------------------ sort
-	map('o', 'O', hint="//s//ize //b//ase//n//ame //m//time //t//ype //r//everse")
-	sort_dict = {
-		's': 'size',
-		'b': 'basename',
-		'n': 'basename',
-		'm': 'mtime',
-		't': 'type',
-	}
-
-	for key, val in sort_dict.items():
-		for key, is_capital in ((key, False), (key.upper(), True)):
-			# reverse if any of the two letters is capital
-			map('o' + key, fm.sort(func=val, reverse=is_capital))
-			map('O' + key, fm.sort(func=val, reverse=True))
-
-	map('or', 'Or', 'oR', 'OR', lambda arg: \
-			arg.fm.sort(reverse=not arg.fm.settings.sort_reverse))
-
-	# ----------------------------------------------- console shortcuts
-	@map("A")
-	def append_to_filename(arg):
-		command = 'rename ' + arg.fm.env.cf.basename
-		arg.fm.open_console(cmode.COMMAND, command)
-
-	map('cw', fm.open_console(cmode.COMMAND, 'rename '))
-	map('cd', fm.open_console(cmode.COMMAND, 'cd '))
-	map('f', fm.open_console(cmode.COMMAND_QUICK, 'find '))
-	map('tf', fm.open_console(cmode.COMMAND, 'filter '))
-	map('d', hint='d//u// (disk usage) d//d// (cut)')
-	map('@', fm.open_console(cmode.OPEN, '@'))
-	map('#', fm.open_console(cmode.OPEN, 'p!'))
-
-	# --------------------------------------------- jump to directories
-	map('gh', fm.cd('~'))
-	map('ge', fm.cd('/etc'))
-	map('gu', fm.cd('/usr'))
-	map('gd', fm.cd('/dev'))
-	map('gl', fm.cd('/lib'))
-	map('go', fm.cd('/opt'))
-	map('gv', fm.cd('/var'))
-	map('gr', 'g/', fm.cd('/'))
-	map('gm', fm.cd('/media'))
-	map('gn', fm.cd('/mnt'))
-	map('gs', fm.cd('/srv'))
-	map('gR', fm.cd(RANGERDIR))
-
-	# ------------------------------------------------------------ tabs
-	map('gc', ctrl('W'), fm.tab_close())
-	map('gt', TAB, fm.tab_move(1))
-	map('gT', KEY_BTAB, fm.tab_move(-1))
-	map('gn', ctrl('N'), fm.tab_new())
-	for n in range(1, 10):
-		map('g' + str(n), fm.tab_open(n))
-
-	# ------------------------------------------------------- searching
-	map('/', fm.open_console(cmode.SEARCH))
-
-	map('n', fm.search())
-	map('N', fm.search(forward=False))
-
-	map('ct', fm.search(order='tag'))
-	map('cc', fm.search(order='ctime'))
-	map('cm', fm.search(order='mimetype'))
-	map('cs', fm.search(order='size'))
-	map('c', hint='//c//time //m//imetype //s//ize //t//agged')
-
-	# ------------------------------------------------------- bookmarks
-	for key in ALLOWED_BOOKMARK_KEYS:
-		map("`" + key, "'" + key, fm.enter_bookmark(key))
-		map("m" + key, fm.set_bookmark(key))
-		map("um" + key, fm.unset_bookmark(key))
-	map("`", "'", "m", "um", draw_bookmarks=True)
-
-	# ---------------------------------------------------- change views
-	map('i', fm.display_file())
-	map(ctrl('p'), fm.display_log())
-	map('?', KEY_F1, fm.display_help())
-	map('w', lambda arg: arg.fm.ui.open_taskview())
-
-	# ------------------------------------------------ system functions
-	_system_functions(map)
-	map('ZZ', 'ZQ', fm.exit())
-	map(ctrl('R'), fm.reset())
-	map('R', fm.reload_cwd())
-	@map(ctrl('C'))
-	def ctrl_c(arg):
-		try:
-			item = arg.fm.loader.queue[0]
-		except:
-			arg.fm.notify("Type Q or :quit<Enter> to exit Ranger")
-		else:
-			arg.fm.notify("Aborting: " + item.get_description())
-			arg.fm.loader.remove(index=0)
-
-	map(':', ';', fm.open_console(cmode.COMMAND))
-	map('>', fm.open_console(cmode.COMMAND_QUICK))
-	map('!', 's', fm.open_console(cmode.OPEN))
-	map('r', fm.open_console(cmode.OPEN_QUICK))
-
-	map.rebuild_paths()
-
-
-def initialize_console_commands(map):
-	"""Initialize the commands for the console widget only"""
-
-	# -------------------------------------------------------- movement
-	map(KEY_UP, wdg.history_move(-1))
-	map(KEY_DOWN, wdg.history_move(1))
-
-	map(ctrl('b'), KEY_LEFT, wdg.move(relative = -1))
-	map(ctrl('f'), KEY_RIGHT, wdg.move(relative = 1))
-	map(ctrl('a'), KEY_HOME, wdg.move(absolute = 0))
-	map(ctrl('e'), KEY_END, wdg.move(absolute = -1))
-
-	# ----------------------------------------- deleting / pasting text
-	map(ctrl('d'), KEY_DC, wdg.delete(0))
-	map(ctrl('h'), KEY_BACKSPACE, DEL, wdg.delete(-1))
-	map(ctrl('w'), wdg.delete_word())
-	map(ctrl('k'), wdg.delete_rest(1))
-	map(ctrl('u'), wdg.delete_rest(-1))
-	map(ctrl('y'), wdg.paste())
-
-	# ------------------------------------------------ system functions
-	_system_functions(map)
-	map.unbind('Q')  # we don't want to quit with Q in the console...
-
-	map(KEY_F1, lambda arg: arg.fm.display_command_help(arg.wdg))
-	map(ctrl('c'), ESC, wdg.close())
-	map(ctrl('j'), KEY_ENTER, wdg.execute())
-	map(TAB, wdg.tab())
-	map(KEY_BTAB, wdg.tab(-1))
-
-	map.rebuild_paths()
-
-
-def initialize_taskview_commands(map):
-	"""Initialize the commands for the TaskView widget"""
-	_basic_movement(map)
-	_vimlike_aliases(map)
-	_system_functions(map)
-
-	# -------------------------------------------------- (re)move tasks
-	map('K', wdg.task_move(0))
-	map('J', wdg.task_move(-1))
-	map('dd', wdg.task_remove())
-
-	# ------------------------------------------------ system functions
-	map('?', fm.display_help())
-	map('w', 'q', ESC, ctrl('d'), ctrl('c'),
-			lambda arg: arg.fm.ui.close_taskview())
-
-	map.rebuild_paths()
-
-
-def initialize_pager_commands(map):
-	_base_pager_commands(map)
-	map('q', 'i', ESC, KEY_F1, lambda arg: arg.fm.ui.close_pager())
-	map.rebuild_paths()
-
-
-def initialize_embedded_pager_commands(map):
-	_base_pager_commands(map)
-	map('q', 'i', ESC, lambda arg: arg.fm.ui.close_embedded_pager())
-	map.rebuild_paths()
-
-def _base_pager_commands(map):
-	_basic_movement(map)
-	_vimlike_aliases(map)
-	_system_functions(map)
-
-	# -------------------------------------------------------- movement
-	map(KEY_LEFT, wdg.move_horizontal(relative=-4))
-	map(KEY_RIGHT, wdg.move_horizontal(relative=4))
-	map(KEY_NPAGE, ctrl('f'), wdg.move(relative=1, pages=True))
-	map(KEY_PPAGE, ctrl('b'), wdg.move(relative=-1, pages=True))
-	map(ctrl('d'), wdg.move(relative=0.5, pages=True))
-	map(ctrl('u'), wdg.move(relative=-0.5, pages=True))
-	map(' ', wdg.move(relative=0.8, pages=True))
-
-	# ---------------------------------------------------------- others
-	map('E', fm.edit_file())
-	map('?', fm.display_help())
-
-	# --------------------------------------------- less-like shortcuts
-	map.alias(KEY_NPAGE, 'f')
-	map.alias(KEY_PPAGE, 'b')
-	map.alias(ctrl('d'), 'd')
-	map.alias(ctrl('u'), 'u')
-
-
-def _system_functions(map):
-	map('Q', fm.exit())
-	map(ctrl('L'), fm.redraw_window())
-
-
-def _basic_movement(map):
-	map(KEY_DOWN, wdg.move(relative=1))
-	map(KEY_UP, wdg.move(relative=-1))
-	map(KEY_HOME, wdg.move(absolute=0))
-	map(KEY_END, wdg.move(absolute=-1))
+from ranger import log
+
+# ===================================================================
+# == Define keys for everywhere:
+# ===================================================================
+map = global_keys = KeyMapWithDirections()
+map('Q', fm.exit())
+map('<C-L>', fm.redraw_window())
+map('<backspace2>', alias='<backspace>')  # Backspace is bugged sometimes
+
+#map('<dir>', wdg.move())
+@map('<dir>') # move around with direction keys
+def move(arg):
+	arg.wdg.move(narg=arg.n, **arg.direction)
+
+# -------------------------------------------------- direction keys
+map.dir('<down>', down=1)
+map.dir('<up>', up=1)
+map.dir('<left>', left=1)
+map.dir('<right>', right=1)
+map.dir('<home>', down=0, absolute=True)
+map.dir('<end>', down=-1, absolute=True)
+map.dir('<pagedown>', down=1, pages=True)
+map.dir('<pageup>', down=-1, pages=True)
+map.dir('%', down=1, percentage=True, absolute=True)
+
+
+# ===================================================================
+# == Define aliases
+# ===================================================================
+map = vim_aliases = KeyMapWithDirections()
+map.dir('j', alias='<down>')
+map.dir('k', alias='<up>')
+map.dir('h', alias='<left>')
+map.dir('l', alias='<right>')
+map.dir('gg', alias='<home>')
+map.dir('G', alias='<end>')
+map.dir('<C-F>', alias='<pagedown>')
+map.dir('<C-B>', alias='<pageup>')
+
+map = readline_aliases = KeyMapWithDirections()
+map.dir('<C-B>', alias='<left>')
+map.dir('<C-F>', alias='<right>')
+map.dir('<C-A>', alias='<home>')
+map.dir('<C-E>', alias='<end>')
+map.dir('<C-D>', alias='<delete>')
+map.dir('<C-H>', alias='<backspace>')
+
+
+# ===================================================================
+# == Define keys in "general" context:
+# ===================================================================
+map = keymanager['general']
+map.merge(global_keys)
+map.merge(vim_aliases)
+
+# -------------------------------------------------------- movement
+map('gg', fm.move(to=0))
+map('<C-D>', 'J', fm.move(down=0.5, pages=True))
+map('<C-U>', 'K', fm.move(up=0.5, pages=True))
+map(']', fm.move_parent(1))
+map('[', fm.move_parent(-1))
+map('}', fm.traverse())
+map('{', fm.history_go(-1))
+
+# --------------------------------------------------------- history
+map('H', fm.history_go(-1))
+map('L', fm.history_go(1))
+
+# ----------------------------------------------- tagging / marking
+map('t', fm.tag_toggle())
+map('T', fm.tag_remove())
+
+map(' ', fm.mark(toggle=True))
+map('v', fm.mark(all=True, toggle=True))
+map('V', fm.mark(all=True, val=False))
+
+# ------------------------------------------ file system operations
+map('yy', 'y<dir>', fm.copy())
+map('dd', 'd<dir>', fm.cut())
+map('ud', fm.uncut())
+map('pp', fm.paste())
+map('po', fm.paste(overwrite=True))
+map('pl', fm.paste_symlink())
+map('p<bg>', fm.hint('press *p* once again to confirm pasting' \
+		', or *l* to create symlinks'))
+
+# ---------------------------------------------------- run programs
+map('S', fm.execute_command(os.environ['SHELL']))
+map('E', fm.edit_file())
+map('.term', fm.execute_command('x-terminal-emulator', flags='d'))
+map('du', fm.execute_command('du --max-depth=1 -h | less'))
+
+# -------------------------------------------------- toggle options
+map('z<bg>', fm.hint("show_*h*idden *p*review_files" \
+	"*d*irectories_first *c*ollapse_preview flush*i*nput"))
+map('zh', fm.toggle_boolean_option('show_hidden'))
+map('zp', fm.toggle_boolean_option('preview_files'))
+map('zi', fm.toggle_boolean_option('flushinput'))
+map('zd', fm.toggle_boolean_option('directories_first'))
+map('zc', fm.toggle_boolean_option('collapse_preview'))
+
+# ------------------------------------------------------------ sort
+map('o<bg>', 'O<bg>', fm.hint("*s*ize *b*ase*n*ame *m*time" \
+	" *t*ype *r*everse"))
+sort_dict = {
+	's': 'size',
+	'b': 'basename',
+	'n': 'basename',
+	'm': 'mtime',
+	't': 'type',
+}
+
+for key, val in sort_dict.items():
+	for key, is_capital in ((key, False), (key.upper(), True)):
+		# reverse if any of the two letters is capital
+		map('o' + key, fm.sort(func=val, reverse=is_capital))
+		map('O' + key, fm.sort(func=val, reverse=True))
+
+map('or', 'Or', 'oR', 'OR', lambda arg: \
+		arg.fm.sort(reverse=not arg.fm.settings.sort_reverse))
+
+# ----------------------------------------------- console shortcuts
+@map("A")
+def append_to_filename(arg):
+	command = 'rename ' + arg.fm.env.cf.basename
+	arg.fm.open_console(cmode.COMMAND, command)
+
+map('cw', fm.open_console(cmode.COMMAND, 'rename '))
+map('cd', fm.open_console(cmode.COMMAND, 'cd '))
+map('f', fm.open_console(cmode.COMMAND_QUICK, 'find '))
+map('bf', fm.open_console(cmode.COMMAND, 'filter '))
+map('d<bg>', fm.hint('d*u* (disk usage) d*d* (cut)'))
+map('@', fm.open_console(cmode.OPEN, '@'))
+map('#', fm.open_console(cmode.OPEN, 'p!'))
+
+# --------------------------------------------- jump to directories
+map('gh', fm.cd('~'))
+map('ge', fm.cd('/etc'))
+map('gu', fm.cd('/usr'))
+map('gd', fm.cd('/dev'))
+map('gl', fm.cd('/lib'))
+map('go', fm.cd('/opt'))
+map('gv', fm.cd('/var'))
+map('gr', 'g/', fm.cd('/'))
+map('gm', fm.cd('/media'))
+map('gn', fm.cd('/mnt'))
+map('gs', fm.cd('/srv'))
+map('gR', fm.cd(RANGERDIR))
+
+# ------------------------------------------------------------ tabs
+map('gc', '<C-W>', fm.tab_close())
+map('gt', '<TAB>', fm.tab_move(1))
+map('gT', '<S-TAB>', fm.tab_move(-1))
+map('gn', '<C-N>', fm.tab_new())
+for n in range(1, 10):
+	map('g' + str(n), fm.tab_open(n))
+	map('<A-' + str(n) + '>', fm.tab_open(n))
+
+# ------------------------------------------------------- searching
+map('/', fm.open_console(cmode.SEARCH))
+
+map('n', fm.search())
+map('N', fm.search(forward=False))
+
+map('ct', fm.search(order='tag'))
+map('cc', fm.search(order='ctime'))
+map('cm', fm.search(order='mimetype'))
+map('cs', fm.search(order='size'))
+map('c<bg>', fm.hint('*c*time *m*imetype *s*ize'))
+
+# ------------------------------------------------------- bookmarks
+for key in ALLOWED_BOOKMARK_KEYS:
+	map("`" + key, "'" + key, fm.enter_bookmark(key))
+	map("m" + key, fm.set_bookmark(key))
+	map("um" + key, fm.unset_bookmark(key))
+map("`<bg>", "'<bg>", "m<bg>", fm.draw_bookmarks())
+
+# ---------------------------------------------------- change views
+map('i', fm.display_file())
+map('<C-P>', fm.display_log())
+map('?', KEY_F1, fm.display_help())
+map('w', lambda arg: arg.fm.ui.open_taskview())
+
+# ------------------------------------------------ system functions
+map('ZZ', 'ZQ', fm.exit())
+map('<C-R>', fm.reset())
+map('R', fm.reload_cwd())
+@map('<C-C>')
+def ctrl_c(arg):
+	try:
+		item = arg.fm.loader.queue[0]
+	except:
+		arg.fm.notify("Type Q or :quit<Enter> to exit Ranger")
+	else:
+		arg.fm.notify("Aborting: " + item.get_description())
+		arg.fm.loader.remove(index=0)
+
+map(':', ';', fm.open_console(cmode.COMMAND))
+map('>', fm.open_console(cmode.COMMAND_QUICK))
+map('!', fm.open_console(cmode.OPEN, prompt='!'))
+map('s', fm.open_console(cmode.OPEN, prompt='$'))
+map('r', fm.open_console(cmode.OPEN_QUICK))
+
+
+# ===================================================================
+# == Define keys for the pager
+# ===================================================================
+map = pager_keys = KeyMapWithDirections()
+map.merge(global_keys)
+map.merge(vim_aliases)
+
+# -------------------------------------------------------- movement
+map('<left>', wdg.move(left=4))
+map('<right>', wdg.move(right=4))
+map('<C-D>', 'd', wdg.move(down=0.5, pages=True))
+map('<C-U>', 'u', wdg.move(up=0.5, pages=True))
+map('<C-F>', 'f', '<pagedown>', wdg.move(down=1, pages=True))
+map('<C-B>', 'b', '<pageup>', wdg.move(up=1, pages=True))
+map('<space>', wdg.move(down=0.8, pages=True))
+map('<cr>', wdg.move(down=1))
+
+# ---------------------------------------------------------- others
+map('E', fm.edit_file())
+map('?', fm.display_help())
+
+# --------------------------------------------------- bind the keys
+# There are two different kinds of pagers, each have a different
+# method for exiting:
+
+map = keymanager.get_context('pager')
+map.merge(pager_keys)
+map('q', 'i', '<esc>', lambda arg: arg.fm.ui.close_pager())
+
+map = keymanager.get_context('embedded_pager')
+map.merge(pager_keys)
+map('q', 'i', '<esc>', lambda arg: arg.fm.ui.close_embedded_pager())
+
+
+# ===================================================================
+# == Define keys for the taskview
+# ===================================================================
+map = keymanager.get_context('taskview')
+map('K', wdg.task_move(0))
+map('J', wdg.task_move(-1))
+map('dd', wdg.task_remove())
+
+map('?', fm.display_help())
+map('w', 'q', ESC, ctrl('d'), ctrl('c'),
+		lambda arg: arg.fm.ui.close_taskview())
+
+
+# ===================================================================
+# == Define keys for the console
+# ===================================================================
+map = keymanager.get_context('console')
+map.merge(global_keys)
+map.merge(readline_aliases)
+
+map('<up>', wdg.history_move(-1))
+map('<down>', wdg.history_move(1))
+map('<home>', wdg.move(right=0, absolute=True))
+map('<end>', wdg.move(right=-1, absolute=True))
+map('<tab>', wdg.tab())
+map('<s-tab>', wdg.tab(-1))
+map('<c-c>', '<esc>', wdg.close())
+map('<CR>', '<c-j>', wdg.execute())
+map('<F1>', lambda arg: arg.fm.display_command_help(arg.wdg))
+
+map('<backspace>', wdg.delete(-1))
+map('<delete>', wdg.delete(1))
+map('<C-W>', wdg.delete_word())
+map('<C-K>', wdg.delete_rest(1))
+map('<C-U>', wdg.delete_rest(-1))
+map('<C-Y>', wdg.paste())
+
+# Any key which is still undefined will simply be typed in.
+@map('<any>')
+def type_key(arg):
+	arg.wdg.type_key(arg.match)
+
+# Unmap some global keys so we can type them:
+map.unmap('Q')
+map.directions.unmap('%')
diff --git a/ranger/defaults/options.py b/ranger/defaults/options.py
index cddd70eb..2de8a230 100644
--- a/ranger/defaults/options.py
+++ b/ranger/defaults/options.py
@@ -124,3 +124,7 @@ def colorscheme_overlay(context, fg, bg, attr):
 
 # The above function was just an example, let's set it back to None
 colorscheme_overlay = None
+
+# Enable this if key combinations with the Alt Key don't work for you.
+# (Especially on xterm)
+xterm_alt_key = False
diff --git a/ranger/ext/accumulator.py b/ranger/ext/accumulator.py
index c65370d5..2e599c85 100644
--- a/ranger/ext/accumulator.py
+++ b/ranger/ext/accumulator.py
@@ -13,41 +13,27 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+from ranger.ext.direction import Direction
+
 class Accumulator(object):
 	def __init__(self):
 		self.pointer = 0
 		self.pointed_obj = None
 
-	def move(self, relative=0, absolute=None, pages=None, narg=None):
-		i = self.pointer
+	def move(self, narg=None, **keywords):
+		direction = Direction(keywords)
 		lst = self.get_list()
 		if not lst:
 			return self.pointer
-		length = len(lst)
-
-		if isinstance(absolute, int):
-			if isinstance(narg, int):
-				absolute = narg
-			if absolute < 0: # wrap
-				i = absolute + length
-			else:
-				i = absolute
-
-		if relative != 0:
-			if isinstance(pages, int):
-				relative *= pages * self.get_height()
-			if isinstance(narg, int):
-				relative *= narg
-		i = int(i + relative)
-
-		if i >= length:
-			i = length - 1
-		if i < 0:
-			i = 0
-
-		self.pointer = i
+		pointer = direction.move(
+				direction=direction.down(),
+				maximum=len(lst),
+				override=narg,
+				pagesize=self.get_height(),
+				current=self.pointer)
+		self.pointer = pointer
 		self.correct_pointer()
-		return self.pointer
+		return pointer
 
 	def move_to_obj(self, arg, attr=None):
 		if not arg:
@@ -77,10 +63,10 @@ class Accumulator(object):
 				test = obj
 
 			if test == good:
-				self.move(absolute=i)
+				self.move(to=i)
 				return True
 
-		return self.move(absolute=self.pointer)
+		return self.move(to=self.pointer)
 
 	def correct_pointer(self):
 		lst = self.get_list()
diff --git a/ranger/ext/direction.py b/ranger/ext/direction.py
new file mode 100644
index 00000000..b9fbcac9
--- /dev/null
+++ b/ranger/ext/direction.py
@@ -0,0 +1,141 @@
+# Copyright (C) 2009, 2010  Roman Zimbelmann <romanz@lavabit.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Directions provide convenience methods for movement operations.
+
+Direction objects are handled just like dicts but provide
+methods like up() and down() which give you the correct value
+for the vertical direction, even if only the "up" or "down" key
+has been defined.
+
+Example application:
+d = Direction(down=5)
+print(d.up()) # prints -5
+print(bool(d.horizontal())) # False, since no horizontal direction is defined
+"""
+
+class Direction(dict):
+	__doc__ = __doc__  # for nicer pydoc
+
+	def __init__(self, dictionary=None, **keywords):
+		if dictionary is not None:
+			dict.__init__(self, dictionary)
+		else:
+			dict.__init__(self, keywords)
+		if 'to' in self:
+			self['down'] = self['to']
+			self['absolute'] = True
+
+	def copy(self):
+		return Direction(**self)
+
+	def _get_bool(self, first, second, fallback=None):
+		try: return self[first]
+		except:
+			try: return not self[second]
+			except: return fallback
+
+	def _get_direction(self, first, second, fallback=0):
+		try: return self[first]
+		except:
+			try: return -self[second]
+			except: return fallback
+
+	def up(self):
+		return -Direction.down(self)
+
+	def down(self):
+		return Direction._get_direction(self, 'down', 'up')
+
+	def right(self):
+		return Direction._get_direction(self, 'right', 'left')
+
+	def absolute(self):
+		return Direction._get_bool(self, 'absolute', 'relative')
+
+	def left(self):
+		return -Direction.right(self)
+
+	def relative(self):
+		return not Direction.absolute(self)
+
+	def vertical_direction(self):
+		down = Direction.down(self)
+		return (down > 0) - (down < 0)
+
+	def horizontal_direction(self):
+		right = Direction.right(self)
+		return (right > 0) - (right < 0)
+
+	def vertical(self):
+		return set(self) & set(['up', 'down'])
+
+	def horizontal(self):
+		return set(self) & set(['left', 'right'])
+
+	def pages(self):
+		return 'pages' in self and self['pages']
+
+	def percentage(self):
+		return 'percentage' in self and self['percentage']
+
+	def multiply(self, n):
+		for key in ('up', 'right', 'down', 'left'):
+			try:
+				self[key] *= n
+			except:
+				pass
+
+	def set(self, n):
+		for key in ('up', 'right', 'down', 'left'):
+			if key in self:
+				self[key] = n
+
+	def move(self, direction, override=None, minimum=0, maximum=9999,
+			current=0, pagesize=1, offset=0):
+		"""
+		Calculates the new position in a given boundary.
+
+		Example:
+		d = Direction(pages=True)
+		d.move(direction=3) # = 3
+		d.move(direction=3, current=2) # = 5
+		d.move(direction=3, pagesize=5) # = 15
+		d.move(direction=3, pagesize=5, maximum=10) # = 10
+		d.move(direction=9, override=2) # = 18
+		"""
+		pos = direction
+		if override is not None:
+			if self.absolute():
+				pos = override
+			else:
+				pos *= override
+		if self.pages():
+			pos *= pagesize
+		elif self.percentage():
+			pos *= maximum / 100.0
+		if self.absolute():
+			if pos < minimum:
+				pos += maximum
+		else:
+			pos += current
+		return int(max(min(pos, maximum + offset - 1), minimum))
+
+	def select(self, lst, override, current, pagesize, offset=1):
+		dest = self.move(direction=self.down(), override=override,
+			current=current, pagesize=pagesize, minimum=0, maximum=len(lst))
+		selection = lst[min(current, dest):max(current, dest) + offset]
+		return dest + offset - 1, selection
diff --git a/ranger/ext/keybinding_parser.py b/ranger/ext/keybinding_parser.py
new file mode 100644
index 00000000..c33ac12f
--- /dev/null
+++ b/ranger/ext/keybinding_parser.py
@@ -0,0 +1,96 @@
+# Copyright (C) 2009, 2010  Roman Zimbelmann <romanz@lavabit.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import curses.ascii
+from string import ascii_lowercase
+
+def parse_keybinding(obj):
+	"""
+	Translate a keybinding to a sequence of integers
+
+	Example:
+	lol<CR>   =>   (108, 111, 108, 10)
+	"""
+	assert isinstance(obj, (tuple, int, str))
+	if isinstance(obj, tuple):
+		for char in obj:
+			yield char
+	elif isinstance(obj, int):
+		yield obj
+	elif isinstance(obj, str):
+		in_brackets = False
+		bracket_content = None
+		for char in obj:
+			if in_brackets:
+				if char == '>':
+					in_brackets = False
+					string = ''.join(bracket_content).lower()
+					try:
+						keys = special_keys[string]
+						for key in keys:
+							yield key
+					except KeyError:
+						yield ord('<')
+						for c in bracket_content:
+							yield ord(c)
+						yield ord('>')
+					except TypeError:
+						yield keys  # it was no tuple, just an int
+				else:
+					bracket_content.append(char)
+			else:
+				if char == '<':
+					in_brackets = True
+					bracket_content = []
+				else:
+					yield ord(char)
+		if in_brackets:
+			yield ord('<')
+			for c in bracket_content:
+				yield ord(c)
+
+# Arbitrary numbers which are not used with curses.KEY_XYZ
+DIRKEY = 9001
+ANYKEY = 9002
+PASSIVE_ACTION = 9003
+
+special_keys = {
+	'dir': DIRKEY,
+	'any': ANYKEY,
+	'bg': PASSIVE_ACTION,
+	'backspace': curses.KEY_BACKSPACE,
+	'backspace2': curses.ascii.DEL,
+	'delete': curses.KEY_DC,
+	'cr': ord("\n"),
+	'enter': ord("\n"),
+	'space': ord(" "),
+	'esc': curses.ascii.ESC,
+	'down': curses.KEY_DOWN,
+	'up': curses.KEY_UP,
+	'left': curses.KEY_LEFT,
+	'right': curses.KEY_RIGHT,
+	'pagedown': curses.KEY_NPAGE,
+	'pageup': curses.KEY_PPAGE,
+	'home': curses.KEY_HOME,
+	'end': curses.KEY_END,
+	'tab': ord('\t'),
+	's-tab': curses.KEY_BTAB,
+}
+
+for char in ascii_lowercase:
+	special_keys['c-' + char] = ord(char) - 96
+
+for char in (ascii_lowercase + '0123456789'):
+	special_keys['a-' + char] = (27, ord(char))
diff --git a/ranger/ext/move.py b/ranger/ext/move.py
deleted file mode 100644
index 948adae6..00000000
--- a/ranger/ext/move.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# Copyright (C) 2009, 2010  Roman Zimbelmann <romanz@lavabit.com>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-def move_between(current, minimum, maximum, relative=0, absolute=None):
-	i = current
-	if isinstance(absolute, int):
-		i = absolute
-	if isinstance(relative, int):
-		i += relative
-	i = max(minimum, min(maximum - 1, i))
-	return i
diff --git a/ranger/ext/tree.py b/ranger/ext/tree.py
new file mode 100644
index 00000000..a954136b
--- /dev/null
+++ b/ranger/ext/tree.py
@@ -0,0 +1,136 @@
+# Copyright (C) 2009, 2010  Roman Zimbelmann <romanz@lavabit.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+class Tree(object):
+	def __init__(self, dictionary=None, parent=None, key=None):
+		if dictionary is None:
+			self._tree = dict()
+		else:
+			self._tree = dictionary
+		self.key = key
+		self.parent = parent
+
+	def copy(self):
+		"""Create a deep copy"""
+		def deep_copy_dict(dct):
+			dct = dct.copy()
+			for key, val in dct.items():
+				if isinstance(val, dict):
+					dct[key] = deep_copy_dict(val)
+			return dct
+		newtree = Tree()
+		if isinstance(self._tree, dict):
+			newtree._tree = deep_copy_dict(self._tree)
+		else:
+			newtree._tree = self._tree
+		return newtree
+
+	def merge(self, other, copy=False):
+		"""Merge another Tree into a copy of self"""
+		def deep_merge(branch, otherbranch):
+			assert isinstance(otherbranch, dict)
+			if not isinstance(branch, dict):
+				branch = dict()
+			elif copy:
+				branch = branch.copy()
+			for key, val in otherbranch.items():
+				if isinstance(val, dict):
+					if key not in branch:
+						branch[key] = None
+					branch[key] = deep_merge(branch[key], val)
+				else:
+					branch[key] = val
+			return branch
+
+		if isinstance(self._tree, dict) and isinstance(other._tree, dict):
+			content = deep_merge(self._tree, other._tree)
+		elif copy and hasattr(other._tree, 'copy'):
+			content = other._tree.copy()
+		else:
+			content = other._tree
+		return type(self)(content)
+
+	def set(self, keys, value, force=True):
+		"""Sets the element at the end of the path to <value>."""
+		if not isinstance(keys, (list, tuple)):
+			keys = tuple(keys)
+		if len(keys) == 0:
+			self.replace(value)
+		else:
+			fnc = force and self.plow or self.traverse
+			subtree = fnc(keys)
+			subtree.replace(value)
+
+	def unset(self, iterable):
+		chars = list(iterable)
+		first = True
+
+		while chars:
+			if first or isinstance(subtree, Tree) and subtree.empty():
+				top = chars.pop()
+				subtree = self.traverse(chars)
+				assert top in subtree._tree, "no such key: " + chr(top)
+				del subtree._tree[top]
+			else:
+				break
+			first = False
+
+	def empty(self):
+		return len(self._tree) == 0
+
+	def replace(self, value):
+		if self.parent:
+			self.parent[self.key] = value
+		self._tree = value
+
+	def plow(self, iterable):
+		"""Move along a path, creating nonexistant subtrees"""
+		tree = self._tree
+		last_tree = None
+		char = None
+		for char in iterable:
+			try:
+				newtree = tree[char]
+				if not isinstance(newtree, dict):
+					raise KeyError()
+			except KeyError:
+				newtree = dict()
+				tree[char] = newtree
+			last_tree = tree
+			tree = newtree
+		if isinstance(tree, dict):
+			return type(self)(tree, parent=last_tree, key=char)
+		else:
+			return tree
+
+	def traverse(self, iterable):
+		"""Move along a path, raising exceptions when failed"""
+		tree = self._tree
+		last_tree = tree
+		char = None
+		for char in iterable:
+			last_tree = tree
+			try:
+				tree = tree[char]
+			except TypeError:
+				raise KeyError("trying to enter leaf")
+			except KeyError:
+				raise KeyError(repr(char) + " not in tree " + str(tree))
+		if isinstance(tree, dict):
+			return type(self)(tree, parent=last_tree, key=char)
+		else:
+			return tree
+
+	__getitem__ = traverse
diff --git a/ranger/fsobject/directory.py b/ranger/fsobject/directory.py
index 1c8355cd..3cec1646 100644
--- a/ranger/fsobject/directory.py
+++ b/ranger/fsobject/directory.py
@@ -59,6 +59,7 @@ class Directory(FileSystemObject, Accumulator, SettingsAware):
 
 	order_outdated = False
 	content_outdated = False
+	content_loaded = False
 
 	sort_dict = {
 		'basename': sort_by_basename,
@@ -219,7 +220,7 @@ class Directory(FileSystemObject, Accumulator, SettingsAware):
 					if self.pointed_obj is not None:
 						self.sync_index()
 					else:
-						self.move(absolute=0)
+						self.move(to=0)
 			else:
 				self.filenames = None
 				self.files = None
diff --git a/ranger/gui/defaultui.py b/ranger/gui/defaultui.py
index 08e0b204..a0a5da4e 100644
--- a/ranger/gui/defaultui.py
+++ b/ranger/gui/defaultui.py
@@ -93,8 +93,8 @@ class DefaultUI(UI):
 	def close_embedded_pager(self):
 		self.browser.close_pager()
 
-	def open_console(self, mode, string=''):
-		if self.console.open(mode, string):
+	def open_console(self, mode, string='', prompt=None):
+		if self.console.open(mode, string, prompt=prompt):
 			self.status.msg = None
 			self.console.on_close = self.close_console
 			self.console.visible = True
diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py
index 2e2f5ada..983cffc8 100644
--- a/ranger/gui/ui.py
+++ b/ranger/gui/ui.py
@@ -20,8 +20,8 @@ import curses
 import _curses
 
 from .displayable import DisplayableContainer
+from ranger.container.keymap import CommandArgs
 from .mouse_event import MouseEvent
-from ranger.container import CommandList
 
 TERMINALS_WITH_TITLE = ("xterm", "xterm-256color", "rxvt",
 		"rxvt-256color", "rxvt-unicode", "aterm", "Eterm",
@@ -47,7 +47,7 @@ def _setup_mouse(signal):
 class UI(DisplayableContainer):
 	is_set_up = False
 	load_mode = False
-	def __init__(self, commandlist=None, env=None, fm=None):
+	def __init__(self, env=None, fm=None):
 		self._draw_title = os.environ["TERM"] in TERMINALS_WITH_TITLE
 		os.environ['ESCDELAY'] = '25'   # don't know a cleaner way
 
@@ -56,13 +56,9 @@ class UI(DisplayableContainer):
 		if fm is not None:
 			self.fm = fm
 
-
-		if commandlist is None:
-			self.commandlist = CommandList()
-			self.fm.keys.initialize_commands(self.commandlist)
-		else:
-			self.commandlist = commandlist
 		self.win = curses.initscr()
+		self.env.keymanager.use_context('general')
+		self.env.keybuffer.clear()
 
 		DisplayableContainer.__init__(self, None)
 
@@ -136,41 +132,74 @@ class UI(DisplayableContainer):
 		if hasattr(self, 'hint'):
 			self.hint()
 
-		self.env.key_append(key)
+		if key < 0:
+			self.env.keybuffer.clear()
+			return
 
 		if DisplayableContainer.press(self, key):
 			return
 
-		try:
-			tup = self.env.keybuffer.tuple_without_numbers()
+		self.env.keymanager.use_context('general')
+		self.env.key_append(key)
+		kbuf = self.env.keybuffer
+		cmd = kbuf.command
 
-			if tup:
-				cmd = self.commandlist[tup]
-			else:
-				return
-		except KeyError:
-			self.env.key_clear()
+		self.fm.hide_bookmarks()
+
+		if kbuf.failure:
+			kbuf.clear()
+			return
+		elif not cmd:
 			return
 
 		self.env.cmd = cmd
 
-		if hasattr(cmd, 'show_obj') and hasattr(cmd.show_obj, 'hint'):
-			if hasattr(self, 'hint'):
-				self.hint(cmd.show_obj.hint)
-		elif hasattr(cmd, 'execute'):
+		if cmd.function:
 			try:
-				cmd.execute_wrap(self)
+				cmd.function(CommandArgs.from_widget(self.fm))
 			except Exception as error:
 				self.fm.notify(error)
-			self.env.key_clear()
+			if kbuf.done:
+				kbuf.clear()
+		else:
+			kbuf.clear()
+
+	def handle_keys(self, *keys):
+		for key in keys:
+			self.handle_key(key)
 
-	def get_next_key(self):
-		"""Waits for key input and returns the pressed key"""
+	def handle_input(self):
 		key = self.win.getch()
-		if key is not -1:
+		if key is 27 or key >= 128 and key < 256:
+			# Handle special keys like ALT+X or unicode here:
+			keys = [key]
+			previous_load_mode = self.load_mode
+			self.set_load_mode(True)
+			for n in range(4):
+				getkey = self.win.getch()
+				if getkey is not -1:
+					keys.append(getkey)
+			if len(keys) == 1:
+				keys.append(-1)
+			if self.settings.xterm_alt_key:
+				if len(keys) == 2 and keys[1] in range(127, 256):
+					keys = [27, keys[1] - 128]
+			self.handle_keys(*keys)
+			self.set_load_mode(previous_load_mode)
+			if self.settings.flushinput:
+				curses.flushinp()
+		else:
+			# Handle simple key presses, CTRL+X, etc here:
 			if self.settings.flushinput:
 				curses.flushinp()
-		return key
+			if key > 0:
+				if key == curses.KEY_MOUSE:
+					self.handle_mouse()
+				elif key == curses.KEY_RESIZE:
+					self.update_size()
+				else:
+					if not self.fm.input_is_blocked():
+						self.handle_key(key)
 
 	def setup(self):
 		"""
diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py
index 73f1e61e..d7dbac4f 100644
--- a/ranger/gui/widgets/browsercolumn.py
+++ b/ranger/gui/widgets/browsercolumn.py
@@ -104,7 +104,7 @@ class BrowserColumn(Pager):
 						self.fm.enter_dir(self.target.path)
 
 					if index < len(self.target):
-						self.fm.move_pointer(absolute = index)
+						self.fm.move(to=index)
 				elif event.pressed(3):
 					try:
 						clicked_file = self.target.files[index]
@@ -114,7 +114,7 @@ class BrowserColumn(Pager):
 
 		else:
 			if self.level > 0:
-				self.fm.move_right()
+				self.fm.move(right=0)
 
 		return True
 
@@ -369,11 +369,11 @@ class BrowserColumn(Pager):
 		self.scroll_begin = self._get_scroll_begin()
 		self.target.scroll_begin = self.scroll_begin
 
-	def scroll(self, relative):
-		"""scroll by n lines"""
+	def scroll(self, n):
+		"""scroll down by n lines"""
 		self.need_redraw = True
-		self.target.move(relative=relative)
-		self.target.scroll_begin += 3 * relative
+		self.target.move(down=n)
+		self.target.scroll_begin += 3 * n
 
 	def __str__(self):
 		return self.__class__.__name__ + ' at level ' + str(self.level)
diff --git a/ranger/gui/widgets/browserview.py b/ranger/gui/widgets/browserview.py
index f586596e..33418a2f 100644
--- a/ranger/gui/widgets/browserview.py
+++ b/ranger/gui/widgets/browserview.py
@@ -25,6 +25,7 @@ class BrowserView(Widget, DisplayableContainer):
 	ratios = None
 	preview = True
 	preview_available = True
+	draw_bookmarks = False
 	stretch_ratios = None
 	need_clear = False
 
@@ -89,10 +90,9 @@ class BrowserView(Widget, DisplayableContainer):
 		self.need_clear = True
 
 	def draw(self):
-		try:
-			if self.env.cmd.show_obj.draw_bookmarks:
-				self._draw_bookmarks()
-		except AttributeError:
+		if self.draw_bookmarks:
+			self._draw_bookmarks()
+		else:
 			if self.need_clear:
 				self.win.erase()
 				self.need_redraw = True
diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py
index 22539e75..36090bb5 100644
--- a/ranger/gui/widgets/console.py
+++ b/ranger/gui/widgets/console.py
@@ -28,8 +28,10 @@ from ranger.gui.widgets.console_mode import is_valid_mode, mode_to_class
 from ranger import log, relpath_conf
 from ranger.core.runner import ALLOWED_FLAGS
 from ranger.ext.shell_escape import shell_quote
+from ranger.container.keymap import CommandArgs
 from ranger.ext.get_executables import get_executables
-from ranger.container import CommandList, History
+from ranger.ext.direction import Direction
+from ranger.container import History
 from ranger.container.history import HistoryEmptyException
 import ranger
 
@@ -47,7 +49,6 @@ class _CustomTemplate(string.Template):
 class Console(Widget):
 	mode = None
 	visible = False
-	commandlist = None
 	last_cursor_mode = None
 	prompt = ':'
 	copy = ''
@@ -61,8 +62,6 @@ class Console(Widget):
 
 	def __init__(self, win):
 		Widget.__init__(self, win)
-		self.commandlist = CommandList()
-		self.fm.keys.initialize_console_commands(self.commandlist)
 		self.clear()
 		self.histories = []
 		# load histories from files
@@ -112,9 +111,14 @@ class Console(Widget):
 		except:
 			pass
 
-	def open(self, mode, string=''):
+	def open(self, mode, string='', prompt=None):
 		if not is_valid_mode(mode):
 			return False
+		if prompt is not None:
+			assert isinstance(prompt, str)
+			self.prompt = prompt
+		elif 'prompt' in self.__dict__:
+			del self.prompt
 
 		cls = mode_to_class(mode)
 
@@ -157,37 +161,37 @@ class Console(Widget):
 		self.line = ''
 
 	def press(self, key):
+		self.env.keymanager.use_context('console')
+		self.env.key_append(key)
+		kbuf = self.env.keybuffer
+		cmd = kbuf.command
 
-		keytuple = self.env.keybuffer.tuple_with_numbers()
-		try:
-			cmd = self.commandlist[keytuple]
-		except KeyError:
-			# An unclean hack to allow unicode input.
-			# This whole part should be replaced.
-			try:
-				chrkey = chr(keytuple[0])
-			except:
-				pass
-			else:
-				self.type_key(chrkey)
-			finally:
-				self.env.key_clear()
-				return
-
-		if cmd == self.commandlist.dummy_object:
+		if kbuf.failure:
+			kbuf.clear()
+			return
+		elif not cmd:
 			return
 
-		try:
-			cmd.execute_wrap(self)
-		except Exception as error:
-			self.fm.notify(error)
-		self.env.key_clear()
+		self.env.cmd = cmd
+
+		if cmd.function:
+			try:
+				cmd.function(CommandArgs.from_widget(self))
+			except Exception as error:
+				self.fm.notify(error)
+			if kbuf.done:
+				kbuf.clear()
+		else:
+			kbuf.clear()
 
 	def type_key(self, key):
 		self.tab_deque = None
 
 		if isinstance(key, int):
-			key = chr(key)
+			try:
+				key = chr(key)
+			except ValueError:
+				return
 
 		if self.pos == len(self.line):
 			self.line += key
@@ -215,14 +219,16 @@ class Console(Widget):
 		self.history.fast_forward()
 		self.history.modify(self.line)
 
-	def move(self, relative = 0, absolute = None):
-		if absolute is not None:
-			if absolute < 0:
-				self.pos = len(self.line) + 1 + absolute
-			else:
-				self.pos = absolute
-
-		self.pos = min(max(0, self.pos + relative), len(self.line))
+	def move(self, **keywords):
+		from ranger import log
+		log(keywords)
+		direction = Direction(keywords)
+		if direction.horizontal():
+			self.pos = direction.move(
+					direction=direction.right(),
+					minimum=0,
+					maximum=len(self.line) + 1,
+					current=self.pos)
 
 	def delete_rest(self, direction):
 		self.tab_deque = None
@@ -261,7 +267,7 @@ class Console(Widget):
 		pos = self.pos + mod
 
 		self.line = self.line[0:pos] + self.line[pos+1:]
-		self.move(relative = mod)
+		self.move(right=mod)
 		self.on_line_change()
 
 	def execute(self):
@@ -418,8 +424,6 @@ class OpenConsole(ConsoleWithTab):
 
 	def init(self):
 		self.history = self.histories[OPEN_HISTORY]
-		OpenConsole.prompt = "{0}@{1} $ ".format(self.env.username,
-				self.env.hostname)
 
 	def execute(self):
 		command, flags = self._parse()
diff --git a/ranger/gui/widgets/pager.py b/ranger/gui/widgets/pager.py
index c0646cdf..85022a01 100644
--- a/ranger/gui/widgets/pager.py
+++ b/ranger/gui/widgets/pager.py
@@ -18,8 +18,8 @@ The pager displays text and allows you to scroll inside it.
 """
 import re
 from . import Widget
-from ranger.container.commandlist import CommandList
-from ranger.ext.move import move_between
+from ranger.ext.direction import Direction
+from ranger.container.keymap import CommandArgs
 
 BAR_REGEXP = re.compile(r'\|\d+\?\|')
 QUOTES_REGEXP = re.compile(r'"[^"]+?"')
@@ -41,14 +41,9 @@ class Pager(Widget):
 		self.markup = None
 		self.lines = []
 
-		self.commandlist = CommandList()
-
-		if embedded:
-			keyfnc = self.fm.keys.initialize_embedded_pager_commands
-		else:
-			keyfnc = self.fm.keys.initialize_pager_commands
-
-		keyfnc(self.commandlist)
+	def move_horizontal(self, *a, **k):
+		"""For compatibility"""
+		self.fm.notify("Your keys.py is out of date. Can't scroll!", bad=True)
 
 	def open(self):
 		self.scroll_begin = 0
@@ -116,68 +111,50 @@ class Pager(Widget):
 			if TITLE_REGEXP.match(line):
 				self.color_at(i, 0, -1, 'title', *baseclr)
 
-
-	def move(self, relative=0, absolute=None, pages=None, narg=None):
-		i = self.scroll_begin
-		if isinstance(absolute, int):
-			if isinstance(narg, int):
-				absolute = narg
-			if absolute < 0:
-				i = absolute + len(self.lines)
-			else:
-				i = absolute
-
-		if relative != 0:
-			if isinstance(pages, int):
-				relative *= pages * self.hei
-			if isinstance(narg, int):
-				relative *= narg
-		i = int(i + relative)
-
-		length = len(self.lines) - self.hei
-		if i >= length:
-			self._get_line(i+self.hei)
-
-		length = len(self.lines) - self.hei
-		if i >= length:
-			i = length
-
-		if i < 0:
-			i = 0
-
-		self.scroll_begin = i
-
-	def move_horizontal(self, relative=0, absolute=None, narg=None):
-		if narg is not None:
-			if absolute is None:
-				relative = relative < 0 and -narg or narg
-			else:
-				absolute = narg
-
-		self.startx = move_between(
-				current=self.startx,
-				minimum=0,
-				maximum=999,
-				relative=relative,
-				absolute=absolute)
+	def move(self, narg=None, **kw):
+		direction = Direction(kw)
+		if direction.horizontal():
+			self.startx = direction.move(
+					direction=direction.right(),
+					override=narg,
+					maximum=self._get_max_width(),
+					current=self.startx,
+					pagesize=self.wid,
+					offset=-self.wid)
+		if direction.vertical():
+			if self.source_is_stream:
+				self._get_line(self.scroll_begin + self.hei * 2)
+			self.scroll_begin = direction.move(
+					direction=direction.down(),
+					override=narg,
+					maximum=len(self.lines),
+					current=self.scroll_begin,
+					pagesize=self.hei,
+					offset=-self.hei)
 
 	def press(self, key):
-		try:
-			tup = self.env.keybuffer.tuple_without_numbers()
-			if tup:
-				cmd = self.commandlist[tup]
-			else:
-				return
+		self.env.keymanager.use_context(self.embedded and 'embedded_pager' or 'pager')
+		self.env.key_append(key)
+		kbuf = self.env.keybuffer
+		cmd = kbuf.command
 
-		except KeyError:
-			self.env.key_clear()
+		if kbuf.failure:
+			kbuf.clear()
+			return
+		elif not cmd:
+			return
+
+		self.env.cmd = cmd
+
+		if cmd.function:
+			try:
+				cmd.function(CommandArgs.from_widget(self))
+			except Exception as error:
+				self.fm.notify(error)
+			if kbuf.done:
+				kbuf.clear()
 		else:
-			if hasattr(cmd, 'execute'):
-				try:
-					cmd.execute_wrap(self)
-				except Exception as error:
-					self.fm.notify(error)
-				self.env.key_clear()
+			kbuf.clear()
 
 	def set_source(self, source, strip=False):
 		if self.source and self.source_is_stream:
@@ -207,10 +184,11 @@ class Pager(Widget):
 		n = event.ctrl() and 1 or 3
 		direction = event.mouse_wheel_direction()
 		if direction:
-			self.move(relative=direction)
+			self.move(down=direction * n)
 		return True
 
 	def _get_line(self, n, attempt_to_read=True):
+		assert isinstance(n, int), n
 		try:
 			return self.lines[n]
 		except (KeyError, IndexError):
@@ -237,3 +215,6 @@ class Pager(Widget):
 			except IndexError:
 				raise StopIteration
 			i += 1
+
+	def _get_max_width(self):
+		return max(len(line) for line in self.lines)
diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py
index b414153a..752bfd6b 100644
--- a/ranger/gui/widgets/statusbar.py
+++ b/ranger/gui/widgets/statusbar.py
@@ -123,7 +123,7 @@ class StatusBar(Widget):
 		highlight = True
 		space_left = self.wid
 		starting_point = self.x
-		for string in self.hint.split('//'):
+		for string in self.hint.split('*'):
 			highlight = not highlight
 			if highlight:
 				self.color('in_statusbar', 'text', 'highlight')
diff --git a/ranger/gui/widgets/taskview.py b/ranger/gui/widgets/taskview.py
index ec68cb1a..475b903c 100644
--- a/ranger/gui/widgets/taskview.py
+++ b/ranger/gui/widgets/taskview.py
@@ -22,7 +22,7 @@ from collections import deque
 
 from . import Widget
 from ranger.ext.accumulator import Accumulator
-from ranger.container import CommandList
+from ranger.container.keymap import CommandArgs
 
 class TaskView(Widget, Accumulator):
 	old_lst = None
@@ -31,8 +31,6 @@ class TaskView(Widget, Accumulator):
 		Widget.__init__(self, win)
 		Accumulator.__init__(self)
 		self.scroll_begin = 0
-		self.commandlist = CommandList()
-		self.fm.keys.initialize_taskview_commands(self.commandlist)
 
 	def draw(self):
 		base_clr = deque()
@@ -90,29 +88,35 @@ class TaskView(Widget, Accumulator):
 
 		self.fm.loader.remove(index=i)
 
-	def task_move(self, absolute, i=None):
+	def task_move(self, to, i=None):
 		if i is None:
 			i = self.pointer
 
-		self.fm.loader.move(_from=i, to=absolute)
+		self.fm.loader.move(_from=i, to=to)
 
 	def press(self, key):
-		try:
-			tup = self.env.keybuffer.tuple_without_numbers()
-			if tup:
-				cmd = self.commandlist[tup]
-			else:
-				return
-
-		except KeyError:
-			self.env.key_clear()
+		self.env.keymanager.use_context('taskview')
+		self.env.key_append(key)
+		kbuf = self.env.keybuffer
+		cmd = kbuf.command
+
+		if kbuf.failure:
+			kbuf.clear()
+			return
+		elif not cmd:
+			return
+
+		self.env.cmd = cmd
+
+		if cmd.function:
+			try:
+				cmd.function(CommandArgs.from_widget(self))
+			except Exception as error:
+				self.fm.notify(error)
+			if kbuf.done:
+				kbuf.clear()
 		else:
-			if hasattr(cmd, 'execute'):
-				try:
-					cmd.execute_wrap(self)
-				except Exception as error:
-					self.fm.notify(error)
-				self.env.key_clear()
+			kbuf.clear()
 
 	def get_list(self):
 		return self.fm.loader.queue
diff --git a/ranger/shared/settings.py b/ranger/shared/settings.py
index 57e00142..df97238f 100644
--- a/ranger/shared/settings.py
+++ b/ranger/shared/settings.py
@@ -46,6 +46,7 @@ ALLOWED_SETTINGS = {
 	'colorscheme': str,
 	'colorscheme_overlay': (type(None), type(lambda:0)),
 	'hidden_filter': lambda x: isinstance(x, str) or hasattr(x, 'match'),
+	'xterm_alt_key': bool,
 }
 
 
diff --git a/test/tc_commandlist.py b/test/tc_commandlist.py
deleted file mode 100644
index 9af2cf05..00000000
--- a/test/tc_commandlist.py
+++ /dev/null
@@ -1,100 +0,0 @@
-# Copyright (C) 2009, 2010  Roman Zimbelmann <romanz@lavabit.com>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-if __name__ == '__main__': from __init__ import init; init()
-
-from unittest import TestCase, main
-from ranger.container.commandlist import CommandList as CL
-
-class Test(TestCase):
-	def assertKeyError(self, obj, key):
-		self.assertRaises(KeyError, obj.__getitem__, key)
-
-	def test_commandist(self):
-		cl = CL()
-		fnc = lambda arg: 1
-		fnc2 = lambda arg: 2
-		dmy = cl.dummy_object
-
-		cl.bind(fnc, 'aaaa')
-		cl.rebuild_paths()
-
-		self.assertEqual(dmy, cl['a'])
-		self.assertEqual(dmy, cl['aa'])
-		self.assertEqual(dmy, cl['aaa'])
-		self.assertEqual(fnc, cl['aaaa'].execute)
-		self.assertKeyError(cl, 'aabb')
-		self.assertKeyError(cl, 'aaaaa')
-
-		cl.bind(fnc, 'aabb')
-		cl.rebuild_paths()
-
-		self.assertEqual(dmy, cl['a'])
-		self.assertEqual(dmy, cl['aa'])
-		self.assertEqual(dmy, cl['aab'])
-		self.assertEqual(fnc, cl['aabb'].execute)
-		self.assertEqual(dmy, cl['aaa'])
-		self.assertEqual(fnc, cl['aaaa'].execute)
-
-		cl.unbind('aabb')
-		cl.rebuild_paths()
-
-		self.assertEqual(dmy, cl['a'])
-		self.assertEqual(dmy, cl['aa'])
-		self.assertKeyError(cl, 'aabb')
-		self.assertKeyError(cl, 'aab')
-		self.assertEqual(dmy, cl['aaa'])
-		self.assertEqual(fnc, cl['aaaa'].execute)
-
-		# Hints work different now.  Since a rework of this system
-		# is planned anyway, there is no need to fix the test.
-		# hint_text = 'some tip blablablba'
-		# cl.hint(hint_text, 'aa')
-		# cl.rebuild_paths()
-
-		self.assertEqual(dmy, cl['a'])
-		# self.assertEqual(hint_text, cl['aa'].text)
-		self.assertEqual(dmy, cl['aaa'])
-		self.assertEqual(fnc, cl['aaaa'].execute)
-
-		# ------------------------ test aliases
-		cl.alias('aaaa', 'cc')
-		cl.rebuild_paths()
-
-		self.assertEqual(dmy, cl['c'])
-		self.assertEqual(cl['cc'].execute, cl['aaaa'].execute)
-
-		cl.bind(fnc2, 'aaaa')
-		cl.rebuild_paths()
-
-		self.assertEqual(cl['cc'].execute, cl['aaaa'].execute)
-
-		cl.unbind('cc')
-		cl.rebuild_paths()
-
-		self.assertEqual(fnc2, cl['aaaa'].execute)
-		self.assertKeyError(cl, 'cc')
-
-		# ----------------------- test clearing
-		cl.clear()
-		self.assertKeyError(cl, 'a')
-		self.assertKeyError(cl, 'aa')
-		self.assertKeyError(cl, 'aaa')
-		self.assertKeyError(cl, 'aaaa')
-		self.assertKeyError(cl, 'aab')
-		self.assertKeyError(cl, 'aabb')
-
-
-if __name__ == '__main__': main()
diff --git a/test/tc_direction.py b/test/tc_direction.py
new file mode 100644
index 00000000..124a7001
--- /dev/null
+++ b/test/tc_direction.py
@@ -0,0 +1,88 @@
+# Copyright (C) 2009, 2010  Roman Zimbelmann <romanz@lavabit.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+if __name__ == '__main__': from __init__ import init; init()
+
+import unittest
+from ranger.ext.direction import Direction
+from ranger.ext.openstruct import OpenStruct
+
+class TestDirections(unittest.TestCase):
+	def test_symmetry(self):
+		d1 = Direction(right=4, down=7, relative=True)
+		d2 = Direction(left=-4, up=-7, absolute=False)
+
+		def subtest(d):
+			self.assertEqual(4, d.right())
+			self.assertEqual(7, d.down())
+			self.assertEqual(-4, d.left())
+			self.assertEqual(-7, d.up())
+			self.assertEqual(True, d.relative())
+			self.assertEqual(False, d.absolute())
+
+			self.assertTrue(d.horizontal())
+			self.assertTrue(d.vertical())
+
+		subtest(d1)
+		subtest(d2)
+
+	def test_conflicts(self):
+		d3 = Direction(right=5, left=2, up=3, down=6,
+				absolute=True, relative=True)
+		self.assertEqual(d3.right(), -d3.left())
+		self.assertEqual(d3.left(), -d3.right())
+		self.assertEqual(d3.up(), -d3.down())
+		self.assertEqual(d3.down(), -d3.up())
+		self.assertEqual(d3.absolute(), not d3.relative())
+		self.assertEqual(d3.relative(), not d3.absolute())
+
+	def test_copy(self):
+		d = Direction(right=5)
+		c = d.copy()
+		self.assertEqual(c.right(), d.right())
+		d['right'] += 3
+		self.assertNotEqual(c.right(), d.right())
+		c['right'] += 3
+		self.assertEqual(c.right(), d.right())
+
+		self.assertFalse(d.vertical())
+		self.assertTrue(d.horizontal())
+
+#	Doesn't work in python2?
+#	def test_duck_typing(self):
+#		dct = dict(right=7, down=-3)
+#		self.assertEqual(-7, Direction.left(dct))
+#		self.assertEqual(3, Direction.up(dct))
+
+	def test_move(self):
+		d = Direction(pages=True)
+		self.assertEqual(3, d.move(direction=3))
+		self.assertEqual(5, d.move(direction=3, current=2))
+		self.assertEqual(15, d.move(direction=3, pagesize=5))
+		self.assertEqual(9, d.move(direction=3, pagesize=5, maximum=10))
+		self.assertEqual(18, d.move(direction=9, override=2))
+		d2 = Direction(absolute=True)
+		self.assertEqual(5, d2.move(direction=9, override=5))
+
+	def test_select(self):
+		d = Direction(down=3)
+		lst = list(range(100))
+		self.assertEqual((6, [3,4,5]), d.select(current=3, pagesize=10, override=None, lst=lst))
+		d = Direction(down=3, pages=True)
+		self.assertEqual((9, [3,4,5,6,7,8]), d.select(current=3, pagesize=2, override=None, lst=lst))
+
+if __name__ == '__main__':
+	unittest.main()
+
diff --git a/test/tc_newkeys.py b/test/tc_newkeys.py
new file mode 100644
index 00000000..8aa043ae
--- /dev/null
+++ b/test/tc_newkeys.py
@@ -0,0 +1,605 @@
+# coding=utf-8
+# Copyright (C) 2009, 2010  Roman Zimbelmann <romanz@lavabit.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+if __name__ == '__main__': from __init__ import init; init()
+from unittest import TestCase, main
+
+from ranger.ext.tree import Tree
+from ranger.container.keymap import *
+from ranger.container.keybuffer import KeyBuffer
+from ranger.ext.keybinding_parser import parse_keybinding
+
+import sys
+
+def simulate_press(self, string):
+	for char in parse_keybinding(string):
+		self.add(char)
+		if self.done:
+			return self.command
+		if self.failure:
+			break
+	return self.command
+
+class PressTestCase(TestCase):
+	"""Some useful methods for the actual test"""
+	def _mkpress(self, keybuffer, _=0):
+		def press(keys):
+			keybuffer.clear()
+			match = simulate_press(keybuffer, keys)
+			self.assertFalse(keybuffer.failure,
+					"parsing keys '"+keys+"' did fail!")
+			self.assertTrue(keybuffer.done,
+					"parsing keys '"+keys+"' did not complete!")
+			arg = CommandArgs(None, None, keybuffer)
+			self.assert_(match.function, "No function found! " + \
+					str(match.__dict__))
+			return match.function(arg)
+		return press
+
+	def assertPressFails(self, kb, keys):
+		kb.clear()
+		simulate_press(kb, keys)
+		self.assertTrue(kb.failure, "Keypress did not fail as expected")
+		kb.clear()
+
+	def assertPressIncomplete(self, kb, keys):
+		kb.clear()
+		simulate_press(kb, keys)
+		self.assertFalse(kb.failure, "Keypress failed, expected incomplete")
+		self.assertFalse(kb.done, "Keypress done which was unexpected")
+		kb.clear()
+
+class Test(PressTestCase):
+	"""The test cases"""
+	def test_passive_action(self):
+		km = KeyMap()
+		directions = KeyMap()
+		kb = KeyBuffer(km, directions)
+		def n(value):
+			"""return n or value"""
+			def fnc(arg=None):
+				if arg is None or arg.n is None:
+					return value
+				return arg.n
+			return fnc
+
+		km.map('ppp', n(5))
+		km.map('pp<bg>', n(8))
+		km.map('pp<dir>', n(2))
+		directions.map('j', dir=Direction(down=1))
+
+		press = self._mkpress(kb, km)
+		self.assertEqual(5, press('ppp'))
+		self.assertEqual(3, press('3ppp'))
+
+		self.assertEqual(2, press('ppj'))
+
+		kb.clear()
+		match = simulate_press(kb, 'pp')
+		args = CommandArgs(0, 0, kb)
+		self.assert_(match)
+		self.assert_(match.function)
+		self.assertEqual(8, match.function(args))
+
+	def test_translate_keys(self):
+		def test(string, *args):
+			if not args:
+				args = (string, )
+			self.assertEqual(ordtuple(*args), tuple(parse_keybinding(string)))
+
+		def ordtuple(*args):
+			lst = []
+			for arg in args:
+				if isinstance(arg, str):
+					lst.extend(ord(c) for c in arg)
+				else:
+					lst.append(arg)
+			return tuple(lst)
+
+		# 1 argument means: assume nothing is translated.
+		test('k')
+		test('kj')
+		test('k<dir>', 'k', DIRKEY)
+		test('k<ANY>z<any>', 'k', ANYKEY, 'z', ANYKEY)
+		test('k<anY>z<dir>', 'k', ANYKEY, 'z', DIRKEY)
+		test('<cr>', "\n")
+		test('<tab><tab><cr>', "\t\t\n")
+		test('<')
+		test('>')
+		test('<C-a>', 1)
+		test('<C-b>', 2)
+		for i in range(1, 26):
+			test('<C-' + chr(i+ord('a')-1) + '>', i)
+		test('<A-x>', 27, ord('x'))
+		test('<a-o>', 27, ord('o'))
+		test('k<a')
+		test('k<anz>')
+		test('k<a<nz>')
+		test('k<a<nz>')
+		test('k<a<>nz>')
+		test('>nz>')
+
+	def test_alias(self):
+		def add_dirs(arg):
+			return sum(dir.down() for dir in arg.directions)
+		def return5(_):
+			return 5
+
+		directions = KeyMap()
+		directions.map('j', dir=Direction(down=1))
+		directions.map('k', dir=Direction(down=-1))
+		directions.map('<CR>', alias='j')
+		directions.map('@', alias='<CR>')
+
+		base = KeyMap()
+		base.map('a<dir>', add_dirs)
+		base.map('b<dir>', add_dirs)
+		base.map('x<dir>x<dir>', add_dirs)
+		base.map('f', return5)
+		base.map('yy', alias='y')
+		base.map('!', alias='!')
+
+		other = KeyMap()
+		other.map('b<dir>b<dir>', alias='x<dir>x<dir>')
+		other.map('c<dir>', add_dirs)
+		other.map('g', alias='f')
+
+		km = base.merge(other, copy=True)
+		kb = KeyBuffer(km, directions)
+
+		press = self._mkpress(kb, km)
+
+		self.assertEqual(1, press('aj'))
+		self.assertEqual(2, press('bjbj'))
+		self.assertEqual(1, press('cj'))
+		self.assertEqual(1, press('c<CR>'))
+
+		self.assertEqual(5, press('f'))
+		self.assertEqual(5, press('g'))
+		self.assertEqual(press('c<CR>'), press('c@'))
+		self.assertEqual(press('c<CR>'), press('c@'))
+		self.assertEqual(press('c<CR>'), press('c@'))
+
+		for n in range(1, 10):
+			self.assertPressIncomplete(kb, 'y' * n)
+
+		for n in range(1, 5):
+			self.assertPressFails(kb, '!' * n)
+
+	def test_tree(self):
+		t = Tree()
+		t.set('abcd', "Yes")
+		self.assertEqual("Yes", t.traverse('abcd'))
+		self.assertRaises(KeyError, t.traverse, 'abcde')
+		self.assertRaises(KeyError, t.traverse, 'xyz')
+		self.assert_(isinstance(t.traverse('abc'), Tree))
+
+		t2 = Tree()
+		self.assertRaises(KeyError, t2.set, 'axy', "Lol", force=False)
+		t2.set('axx', 'ololol')
+		t2.set('axyy', "Lol")
+		self.assertEqual("Yes", t.traverse('abcd'))
+		self.assertRaises(KeyError, t2.traverse, 'abcd')
+		self.assertEqual("Lol", t2.traverse('axyy'))
+		self.assertEqual("ololol", t2.traverse('axx'))
+
+		t2.unset('axyy')
+		self.assertEqual("ololol", t2.traverse('axx'))
+		self.assertRaises(KeyError, t2.traverse, 'axyy')
+		self.assertRaises(KeyError, t2.traverse, 'axy')
+
+		t2.unset('a')
+		self.assertRaises(KeyError, t2.traverse, 'abcd')
+		self.assertRaises(KeyError, t2.traverse, 'a')
+		self.assert_(t2.empty())
+
+	def test_merge_trees(self):
+		def makeTreeA():
+			t = Tree()
+			t.set('aaaX', 1)
+			t.set('aaaY', 2)
+			t.set('aaaZ', 3)
+			t.set('bbbA', 11)
+			t.set('bbbB', 12)
+			t.set('bbbC', 13)
+			t.set('bbbD', 14)
+			t.set('bP', 21)
+			t.set('bQ', 22)
+			return t
+
+		def makeTreeB():
+			u = Tree()
+			u.set('aaaX', 0)
+			u.set('bbbC', 'Yes')
+			u.set('bbbD', None)
+			u.set('bbbE', 15)
+			u.set('bbbF', 16)
+			u.set('bQ', 22)
+			u.set('bR', 23)
+			u.set('ffff', 1337)
+			return u
+
+		# test 1
+		t = Tree('a')
+		u = Tree('b')
+		merged = t.merge(u, copy=True)
+		self.assertEqual('b', merged._tree)
+
+		# test 2
+		t = Tree('a')
+		u = makeTreeA()
+		merged = t.merge(u, copy=True)
+		self.assertEqual(u._tree, merged._tree)
+
+		# test 3
+		t = makeTreeA()
+		u = makeTreeB()
+		v = t.merge(u, copy=True)
+
+		self.assertEqual(0, v['aaaX'])
+		self.assertEqual(2, v['aaaY'])
+		self.assertEqual(3, v['aaaZ'])
+		self.assertEqual(11, v['bbbA'])
+		self.assertEqual('Yes', v['bbbC'])
+		self.assertEqual(None, v['bbbD'])
+		self.assertEqual(15, v['bbbE'])
+		self.assertEqual(16, v['bbbF'])
+		self.assertRaises(KeyError, t.__getitem__, 'bbbG')
+		self.assertEqual(21, v['bP'])
+		self.assertEqual(22, v['bQ'])
+		self.assertEqual(23, v['bR'])
+		self.assertEqual(1337, v['ffff'])
+
+		# merge shouldn't be destructive
+		self.assertEqual(makeTreeA()._tree, t._tree)
+		self.assertEqual(makeTreeB()._tree, u._tree)
+
+		v['fff'].replace('Lolz')
+		self.assertEqual('Lolz', v['fff'])
+
+		v['aaa'].replace('Very bad')
+		v.plow('qqqqqqq').replace('eww.')
+
+		self.assertEqual(makeTreeA()._tree, t._tree)
+		self.assertEqual(makeTreeB()._tree, u._tree)
+
+	def test_add(self):
+		c = KeyMap()
+		c.map('aa', 'b', lambda *_: 'lolz')
+		self.assert_(c['aa'].function(), 'lolz')
+		@c.map('a', 'c')
+		def test():
+			return 5
+		self.assert_(c['b'].function(), 'lolz')
+		self.assert_(c['c'].function(), 5)
+		self.assert_(c['a'].function(), 5)
+
+	def test_quantifier(self):
+		km = KeyMap()
+		directions = KeyMap()
+		kb = KeyBuffer(km, directions)
+		def n(value):
+			"""return n or value"""
+			def fnc(arg=None):
+				if arg is None or arg.n is None:
+					return value
+				return arg.n
+			return fnc
+		km.map('p', n(5))
+		press = self._mkpress(kb, km)
+		self.assertEqual(5, press('p'))
+		self.assertEqual(3, press('3p'))
+		self.assertEqual(6223, press('6223p'))
+
+	def test_direction(self):
+		km = KeyMap()
+		directions = KeyMap()
+		kb = KeyBuffer(km, directions)
+		directions.map('j', dir=Direction(down=1))
+		directions.map('k', dir=Direction(down=-1))
+		def nd(arg):
+			""" n * direction """
+			n = arg.n is None and 1 or arg.n
+			dir = arg.direction is None and Direction(down=1) \
+					or arg.direction
+			return n * dir.down()
+		km.map('d<dir>', nd)
+		km.map('dd', func=nd)
+
+		press = self._mkpress(kb, km)
+
+		self.assertPressIncomplete(kb, 'd')
+		self.assertEqual(  1, press('dj'))
+		self.assertEqual(  3, press('3ddj'))
+		self.assertEqual( 15, press('3d5j'))
+		self.assertEqual(-15, press('3d5k'))
+		# supporting this kind of key combination would be too confusing:
+		# self.assertEqual( 15, press('3d5d'))
+		self.assertEqual(  3, press('3dd'))
+		self.assertEqual(  33, press('33dd'))
+		self.assertEqual(  1, press('dd'))
+
+		km.map('x<dir>', nd)
+		km.map('xxxx', func=nd)
+
+		self.assertEqual(1, press('xxxxj'))
+		self.assertEqual(1, press('xxxxjsomeinvalitchars'))
+
+		# these combinations should break:
+		self.assertPressFails(kb, 'xxxj')
+		self.assertPressFails(kb, 'xxj')
+		self.assertPressFails(kb, 'xxkldfjalksdjklsfsldkj')
+		self.assertPressFails(kb, 'xyj')
+		self.assertPressIncomplete(kb, 'x') # direction missing
+
+	def test_any_key(self):
+		km = KeyMap()
+		directions = KeyMap()
+		kb = KeyBuffer(km, directions)
+		directions.map('j', dir=Direction(down=1))
+		directions.map('k', dir=Direction(down=-1))
+
+		directions.map('g<any>', dir=Direction(down=-1))
+
+		def cat(arg):
+			n = arg.n is None and 1 or arg.n
+			return ''.join(chr(c) for c in arg.matches) * n
+
+		km.map('return<any>', cat)
+		km.map('cat4<any><any><any><any>', cat)
+		km.map('foo<dir><any>', cat)
+
+		press = self._mkpress(kb, km)
+
+		self.assertEqual('x', press('returnx'))
+		self.assertEqual('abcd', press('cat4abcd'))
+		self.assertEqual('abcdabcd', press('2cat4abcd'))
+		self.assertEqual('55555', press('5return5'))
+
+		self.assertEqual('x', press('foojx'))
+		self.assertPressFails(kb, 'fooggx')  # ANYKEY forbidden in DIRECTION
+
+		km.map('<any>', lambda _: Ellipsis)
+		self.assertEqual('x', press('returnx'))
+		self.assertEqual('abcd', press('cat4abcd'))
+		self.assertEqual(Ellipsis, press('2cat4abcd'))
+		self.assertEqual(Ellipsis, press('5return5'))
+		self.assertEqual(Ellipsis, press('g'))
+		self.assertEqual(Ellipsis, press('ß'))
+		self.assertEqual(Ellipsis, press('ア'))
+		self.assertEqual(Ellipsis, press('9'))
+
+	def test_multiple_directions(self):
+		km = KeyMap()
+		directions = KeyMap()
+		kb = KeyBuffer(km, directions)
+		directions.map('j', dir=Direction(down=1))
+		directions.map('k', dir=Direction(down=-1))
+
+		def add_dirs(arg):
+			return sum(dir.down() for dir in arg.directions)
+
+		km.map('x<dir>y<dir>', add_dirs)
+		km.map('four<dir><dir><dir><dir>', add_dirs)
+
+		press = self._mkpress(kb, km)
+
+		self.assertEqual(2, press('xjyj'))
+		self.assertEqual(0, press('fourjkkj'))
+		self.assertEqual(2, press('four2j4k2j2j'))
+		self.assertEqual(10, press('four1j2j3j4j'))
+		self.assertEqual(10, press('four1j2j3j4jafslkdfjkldj'))
+
+	def test_corruptions(self):
+		km = KeyMap()
+		directions = KeyMap()
+		kb = KeyBuffer(km, directions)
+		press = self._mkpress(kb, km)
+		directions.map('j', dir=Direction(down=1))
+		directions.map('k', dir=Direction(down=-1))
+		km.map('xxx', lambda _: 1)
+
+		self.assertEqual(1, press('xxx'))
+
+		# corrupt the tree
+		tup = tuple(parse_keybinding('xxx'))
+		x = ord('x')
+		km._tree[x][x][x] = "Boo"
+
+		self.assertPressFails(kb, 'xxy')
+		self.assertPressFails(kb, 'xzy')
+		self.assertPressIncomplete(kb, 'xx')
+		self.assertPressIncomplete(kb, 'x')
+		if not sys.flags.optimize:
+			self.assertRaises(AssertionError, simulate_press, kb, 'xxx')
+		kb.clear()
+
+	def test_directions_as_functions(self):
+		km = KeyMap()
+		directions = KeyMap()
+		kb = KeyBuffer(km, directions)
+		press = self._mkpress(kb, km)
+
+		def move(arg):
+			return arg.direction.down()
+
+		directions.map('j', dir=Direction(down=1))
+		directions.map('s', alias='j')
+		directions.map('k', dir=Direction(down=-1))
+		km.map('<dir>', func=move)
+
+		self.assertEqual(1, press('j'))
+		self.assertEqual(1, press('j'))
+		self.assertEqual(1, press('j'))
+		self.assertEqual(1, press('j'))
+		self.assertEqual(1, press('j'))
+		self.assertEqual(1, press('s'))
+		self.assertEqual(1, press('s'))
+		self.assertEqual(1, press('s'))
+		self.assertEqual(1, press('s'))
+		self.assertEqual(1, press('s'))
+		self.assertEqual(-1, press('k'))
+		self.assertEqual(-1, press('k'))
+		self.assertEqual(-1, press('k'))
+
+		km.map('k', func=lambda _: 'love')
+
+		self.assertEqual(1, press('j'))
+		self.assertEqual('love', press('k'))
+
+		self.assertEqual(1, press('40j'))
+		self.assertEqual(40, kb.quant)
+
+		km.map('<dir><dir><any><any>', func=move)
+
+		self.assertEqual(1, press('40jkhl'))
+		self.assertEqual(40, kb.quant)
+
+	def test_tree_deep_copy(self):
+		t = Tree()
+		s = t.plow('abcd')
+		s.replace('X')
+		u = t.copy()
+		self.assertEqual(t._tree, u._tree)
+		s = t.traverse('abc')
+		s.replace('Y')
+		self.assertNotEqual(t._tree, u._tree)
+
+	def test_keymanager(self):
+		def func(arg):
+			return 5
+		def getdown(arg):
+			return arg.direction.down()
+
+		buffer = KeyBuffer(None, None)
+		press = self._mkpress(buffer)
+		keymanager = KeyManager(buffer, ['foo', 'bar'])
+
+		map = keymanager.get_context('foo')
+		map('a', func)
+		map('b', func)
+		map = keymanager.get_context('bar')
+		map('c', func)
+		map('<dir>', getdown)
+
+		keymanager.dir('foo', 'j', down=1)
+		keymanager.dir('bar', 'j', down=1)
+
+		keymanager.use_context('foo')
+		self.assertEqual(5, press('a'))
+		self.assertEqual(5, press('b'))
+		self.assertPressFails(buffer, 'c')
+
+		keymanager.use_context('bar')
+		self.assertPressFails(buffer, 'a')
+		self.assertPressFails(buffer, 'b')
+		self.assertEqual(5, press('c'))
+		self.assertEqual(1, press('j'))
+		keymanager.use_context('foo')
+		keymanager.use_context('foo')
+		keymanager.use_context('foo')
+		keymanager.use_context('bar')
+		keymanager.use_context('foo')
+		keymanager.use_context('bar')
+		keymanager.use_context('bar')
+		self.assertEqual(1, press('j'))
+
+	def test_alias_to_direction(self):
+		def func(arg):
+			return arg.direction.down()
+
+		km = KeyMapWithDirections()
+		kb = KeyBuffer(km, km.directions)
+		press = self._mkpress(kb)
+
+		km.map('<dir>', func)
+		km.map('d<dir>', func)
+		km.dir('j', down=42)
+		km.dir('k', alias='j')
+		self.assertEqual(42, press('j'))
+
+		km.dir('o', alias='j')
+		km.dir('ick', alias='j')
+		self.assertEqual(42, press('o'))
+		self.assertEqual(42, press('dj'))
+		self.assertEqual(42, press('dk'))
+		self.assertEqual(42, press('do'))
+		self.assertEqual(42, press('dick'))
+		self.assertPressFails(kb, 'dioo')
+
+	def test_both_directory_and_any_key(self):
+		def func(arg):
+			return arg.direction.down()
+		def func2(arg):
+			return "yay"
+
+		km = KeyMap()
+		directions = KeyMap()
+		kb = KeyBuffer(km, directions)
+		press = self._mkpress(kb)
+
+		km.map('abc<dir>', func)
+		directions.map('j', dir=Direction(down=42))
+		self.assertEqual(42, press('abcj'))
+
+		km.unmap('abc<dir>')
+
+		km.map('abc<any>', func2)
+		self.assertEqual("yay", press('abcd'))
+
+		km.map('abc<dir>', func)
+
+		km.map('abc<any>', func2)
+		self.assertEqual("yay", press('abcd'))
+
+	def test_map_collision(self):
+		def add_dirs(arg):
+			return sum(dir.down() for dir in arg.directions)
+		def return5(_):
+			return 5
+
+
+		directions = KeyMap()
+		directions.map('gg', dir=Direction(down=1))
+
+
+		km = KeyMap()
+		km.map('gh', return5)
+		km.map('agh', return5)
+		km.map('a<dir>', add_dirs)
+
+		kb = KeyBuffer(km, directions)
+		press = self._mkpress(kb, km)
+
+		self.assertEqual(5, press('gh'))
+		self.assertEqual(5, press('agh'))
+#		self.assertPressFails(kb, 'agh')
+		self.assertEqual(1, press('agg'))
+
+	def test_keymap_with_dir(self):
+		def func(arg):
+			return arg.direction.down()
+
+		km = KeyMapWithDirections()
+		kb = KeyBuffer(km, km.directions)
+
+		press = self._mkpress(kb)
+
+		km.map('abc<dir>', func)
+		km.dir('j', down=42)
+		self.assertEqual(42, press('abcj'))
+
+if __name__ == '__main__': main()
diff --git a/test/tc_ui.py b/test/tc_ui.py
index affec907..3c659459 100644
--- a/test/tc_ui.py
+++ b/test/tc_ui.py
@@ -28,7 +28,7 @@ class Test(unittest.TestCase):
 	def setUp(self):
 
 		self.fm = Fake()
-		self.ui = ui.UI(env=Fake(), fm=self.fm, commandlist=Fake())
+		self.ui = ui.UI(env=Fake(), fm=self.fm)
 
 		def fakesetup():
 			self.ui.widget = Fake()