summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--ABOUT_THIS_BRANCH14
-rw-r--r--ranger/api/keys.py1
-rw-r--r--ranger/container/__init__.py3
-rw-r--r--ranger/container/commandlist.py225
-rw-r--r--ranger/container/keybuffer.py71
-rw-r--r--ranger/container/keymap.py346
-rw-r--r--ranger/core/actions.py50
-rw-r--r--ranger/core/environment.py4
-rw-r--r--ranger/defaults/keys.py220
-rw-r--r--ranger/ext/direction.py121
-rw-r--r--ranger/ext/tree.py135
-rw-r--r--ranger/gui/ui.py42
-rw-r--r--ranger/gui/widgets/browserview.py8
-rw-r--r--ranger/gui/widgets/console.py5
-rw-r--r--ranger/gui/widgets/pager.py11
-rw-r--r--ranger/gui/widgets/taskview.py4
-rw-r--r--test/tc_commandlist.py100
-rw-r--r--test/tc_newkeys.py491
-rw-r--r--test/tc_ui.py2
19 files changed, 1411 insertions, 442 deletions
diff --git a/ABOUT_THIS_BRANCH b/ABOUT_THIS_BRANCH
new file mode 100644
index 00000000..ae09d54d
--- /dev/null
+++ b/ABOUT_THIS_BRANCH
@@ -0,0 +1,14 @@
+I put my personal branch online, maybe you find some of the
+parts useful. To see whats different, type:
+
+git diff master..hut
+
+This branch is being regularily rebased on the master branch,
+which rewrites history, so maybe its better to pick single commits
+from this branch into your own branch rather than working directly
+on this one:
+
+git log master..hut
+# search for a commit you like, write down SHA1 identifier
+git checkout <your branch>
+git cherry-pick <SHA1 of the commit>
diff --git a/ranger/api/keys.py b/ranger/api/keys.py
index 87186378..86911569 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
 
 class Wrapper(object):
 	def __init__(self, firstattr):
diff --git a/ranger/container/__init__.py b/ranger/container/__init__.py
index 51122291..4c8f08ba 100644
--- a/ranger/container/__init__.py
+++ b/ranger/container/__init__.py
@@ -17,6 +17,5 @@
 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, 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
deleted file mode 100644
index 2992aea2..00000000
--- a/ranger/container/keybuffer.py
+++ /dev/null
@@ -1,71 +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 to_string(i):
-	try:
-		return chr(i)
-	except ValueError:
-		return '?'
-
-from collections import deque
-from curses.ascii import ascii
-
-ZERO = ord('0')
-NINE = ord('9')
-
-class KeyBuffer(object):
-	def __init__(self):
-		self.number = None
-		self.queue = deque()
-		self.queue_with_numbers = deque()
-
-	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:
-				return
-		else:
-			self.queue.append(key)
-
-	def tuple_with_numbers(self):
-		"""Get a tuple of ascii codes."""
-		return tuple(self.queue_with_numbers)
-
-	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)
-
-	def __str__(self):
-		"""returns a concatenation of all characters"""
-		return "".join( map( to_string, self.queue_with_numbers ) )
diff --git a/ranger/container/keymap.py b/ranger/container/keymap.py
new file mode 100644
index 00000000..e8cf6119
--- /dev/null
+++ b/ranger/container/keymap.py
@@ -0,0 +1,346 @@
+# 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
+from string import ascii_lowercase
+from inspect import isfunction, getargspec
+from ranger.ext.tree import Tree
+from ranger.ext.direction import Direction
+
+MAX_ALIAS_RECURSION = 20
+PASSIVE_ACTION = 9003
+DIRKEY = 9001
+ANYKEY = 9002
+FUNC = 'func'
+DIRECTION = 'direction'
+DIRARG = 'dir'
+ALIASARG = 'alias'
+
+def to_string(i):
+	"""convert a ord'd integer to a string"""
+	try:
+		return chr(i)
+	except ValueError:
+		return '?'
+
+def is_ascii_digit(n):
+	return n >= 48 and n <= 57
+
+class CommandArgs(object):
+	"""The arguments which are passed to a keybinding function"""
+	def __init__(self, fm, widget, keybuffer):
+		self.fm = fm
+		self.wdg = widget
+		self.keybuffer = keybuffer
+		self.n = keybuffer.quant
+		self.direction = keybuffer.directions and keybuffer.directions[0] or None
+		self.directions = keybuffer.directions
+		self.keys = str(keybuffer)
+		self.matches = keybuffer.matches
+		self.match = keybuffer.matches and keybuffer.matches[0] or None
+		self.binding = keybuffer.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 isfunction(firstarg):
+			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(translate_keys(key), bind)
+
+	def __getitem__(self, key):
+		return self.traverse(translate_keys(key))
+
+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
+			self.has_direction = False
+		else:
+			argnames = getargspec(self.function)[0]
+			try:
+				self.has_direction = actions['with_direction']
+			except KeyError:
+				self.has_direction = DIRECTION in argnames
+		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(translate_keys(alias))
+
+class KeyBuffer(object):
+	"""The evaluator and storage for pressed keys"""
+	def __init__(self, keymap, direction_keys):
+		self.assign(keymap, direction_keys)
+
+	def assign(self, keymap, direction_keys):
+		self.keymap = keymap
+		self.direction_keys = direction_keys
+
+	def add(self, key):
+		if self.failure:
+			return None
+		assert isinstance(key, int)
+		assert key >= 0
+		self.all_keys.append(key)
+
+		# 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):
+		# swap quant and direction_quant in bindings like '<dir>'
+		if self.quant is not None and self.command is None \
+		and self.direction_quant is None:
+			self.direction_quant = self.quant
+			self.quant = None
+
+		try:
+			assert isinstance(self.dir_tree_pointer, dict)
+			self.dir_tree_pointer = self.dir_tree_pointer[key]
+		except KeyError:
+			self.failure = True
+		else:
+			self._direction_try_to_finish()
+
+	def _direction_try_to_finish(self, rec=MAX_ALIAS_RECURSION):
+		if rec <= 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:
+				try:
+					self.dir_tree_pointer = self.direction_keys[match.alias]
+					self._direction_try_to_finish(rec - 1)
+				except KeyError:
+					self.failure = True
+					return None
+			else:
+				if self.direction_quant is not None:
+					direction = match.actions['dir'] * self.direction_quant
+					direction.has_explicit_direction = True
+				else:
+					direction = match.actions['dir'].copy()
+				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 is_ascii_digit(key) 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:
+			print(self.tree_pointer)
+			self.failure = True
+			return None
+		except KeyError:
+			try:
+				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, rec=MAX_ALIAS_RECURSION):
+		if rec <= 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:
+				try:
+					self.tree_pointer = self.keymap[self.tree_pointer.alias]
+					self._try_to_finish(rec - 1)
+				except KeyError:
+					self.failure = True
+					return None
+			else:
+				self.command = self.tree_pointer
+				self.done = True
+
+	def clear(self):
+		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
+
+		self.eval_quantifier = True
+		self.eval_command = True
+
+	def __str__(self):
+		"""returns a concatenation of all characters"""
+		return "".join(to_string(c) for c in self.all_keys)
+
+	def simulate_press(self, string):
+		for char in translate_keys(string):
+			self.add(char)
+			if self.done:
+				return self.command
+			if self.failure:
+				break
+		return self.command
+
+special_keys = {
+	'dir': DIRKEY,
+	'any': ANYKEY,
+	'bg': PASSIVE_ACTION,
+	'cr': ord("\n"),
+	'enter': ord("\n"),
+	'space': ord(" "),
+	'down': curses.KEY_DOWN,
+	'up': curses.KEY_UP,
+	'left': curses.KEY_LEFT,
+	'right': curses.KEY_RIGHT,
+	'mouse': curses.KEY_MOUSE,
+	'resize': curses.KEY_RESIZE,
+	'pagedown': curses.KEY_NPAGE,
+	'pageup': curses.KEY_PPAGE,
+	'home': curses.KEY_HOME,
+	'end': curses.KEY_END,
+	'tab': ord('\t'),
+}
+for char in ascii_lowercase:
+	special_keys['c-' + char] = ord(char) - 96
+
+def translate_keys(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:
+						yield special_keys[string]
+					except KeyError:
+						yield ord('<')
+						for c in bracket_content:
+							yield ord(c)
+						yield ord('>')
+				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)
diff --git a/ranger/core/actions.py b/ranger/core/actions.py
index c4d82e58..e55d65b1 100644
--- a/ranger/core/actions.py
+++ b/ranger/core/actions.py
@@ -148,8 +148,10 @@ class Actions(EnvironmentAware, SettingsAware):
 		"""Delete the bookmark with the name <key>"""
 		self.bookmarks.delete(key)
 
-	def move_left(self, narg=1):
+	def move_left(self, narg=None):
 		"""Enter the parent directory"""
+		if narg is None:
+			narg = 1
 		try:
 			directory = os.path.join(*(['..'] * narg))
 		except:
@@ -274,6 +276,44 @@ class Actions(EnvironmentAware, SettingsAware):
 		self.env.cwd.move(relative=relative,
 				absolute=absolute, narg=narg)
 
+	def move(self, dir, narg=None):
+		if narg is not None:
+			dir = dir * narg
+
+		self.notify(str(dir))
+
+		if dir.right is not None:
+			if dir.right >= 0:
+				if dir.has_explicit_direction:
+					self.move_right(narg=dir.right)
+				else:
+					self.move_right(narg=dir.original_right - 1)
+			elif dir.right < 0:
+				self.move_left(narg=dir.left)
+		else:
+			if dir.percent:
+				if dir.absolute:
+					self.move_pointer_by_percentage( \
+							absolute=dir.down, narg=narg)
+				else:
+					self.move_pointer_by_percentage( \
+							relative=dir.down, narg=narg)
+			elif dir.pages:
+				self.move_pointer_by_pages(dir.down)
+			elif dir.absolute:
+				if dir.has_explicit_direction:
+					self.move_pointer(absolute=dir.down)
+				else:
+					self.move_pointer(absolute=dir.original_down)
+			else:
+				self.move_pointer(relative=dir.down)
+
+	def draw_bookmarks(self):
+		self.ui.browser.draw_bookmarks = True
+
+	def hide_bookmarks(self):
+		self.ui.browser.draw_bookmarks = False
+
 	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]))
@@ -288,9 +328,12 @@ class Actions(EnvironmentAware, SettingsAware):
 		if narg is not None:
 			absolute = narg
 
+		if absolute is not None:
+			absolute = int(absolute * factor)
+
 		self.env.cwd.move(
 				relative=int(relative * factor),
-				absolute=int(absolute * factor))
+				absolute=absolute)
 
 	def scroll(self, relative):
 		"""Scroll down by <relative> lines"""
@@ -369,6 +412,9 @@ class Actions(EnvironmentAware, SettingsAware):
 		if hasattr(self.ui, 'notify'):
 			self.ui.notify(text, duration=duration, bad=bad)
 
+	def hint(self, text):
+		self.notify(text)
+
 	def mark(self, all=False, toggle=False, val=None, movedown=None, narg=1):
 		"""
 		A wrapper for the directory.mark_xyz functions.
diff --git a/ranger/core/environment.py b/ranger/core/environment.py
index a30b8dcd..4301d237 100644
--- a/ranger/core/environment.py
+++ b/ranger/core/environment.py
@@ -45,7 +45,7 @@ class Environment(SettingsAware):
 		self.path = abspath(expanduser(path))
 		self.pathway = ()
 		self.directories = {}
-		self.keybuffer = KeyBuffer()
+		self.keybuffer = KeyBuffer(None, None)
 		self.copy = set()
 		self.history = History(self.settings.max_history_size)
 
@@ -66,7 +66,7 @@ class Environment(SettingsAware):
 		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/defaults/keys.py b/ranger/defaults/keys.py
index 6042fd8b..f48c7012 100644
--- a/ranger/defaults/keys.py
+++ b/ranger/defaults/keys.py
@@ -187,6 +187,21 @@ def initialize_commands(map):
 	map('?', KEY_F1, fm.display_help())
 	map('w', lambda arg: arg.fm.ui.open_taskview())
 
+	# ---------------------------------------------------------- custom
+	# This is useful to track watched episode of a series.
+	@bind(']')
+	def tag_next_and_run(arg):
+		fm = arg.fm
+		fm.tag_remove()
+		fm.tag_remove(movedown=False)
+		fm.tag_toggle()
+		fm.move_pointer(relative=-2)
+		fm.move_right()
+		fm.move_pointer(relative=1)
+
+	# "enter" = shortcut for "1l"
+	bind(KEY_ENTER, ctrl('j'), fm.move_right(mode=1))
+
 	# ------------------------------------------------ system functions
 	_system_functions(map)
 	map('ZZ', fm.exit())
@@ -307,3 +322,208 @@ def _basic_movement(map):
 	map(KEY_UP, wdg.move(relative=-1))
 	map(KEY_HOME, wdg.move(absolute=0))
 	map(KEY_END, wdg.move(absolute=-1))
+
+
+
+# ------ newkey:
+
+
+def base_directions():
+	# Direction Keys
+	map = KeyMap()
+	map('<down>', dir=Direction(down=1))
+	map('<up>', dir=Direction(down=-1))
+	map('<left>', dir=Direction(right=-1))
+	map('<right>', dir=Direction(right=1))
+	map('<home>', dir=Direction(down=0, absolute=True))
+	map('<end>', dir=Direction(down=-1, absolute=True))
+	map('<pagedown>', dir=Direction(down=1, pages=True))
+	map('<pageup>', dir=Direction(down=-1, pages=True))
+	map('%<any>', dir=Direction(down=1, percent=True, absolute=True))
+	map('<space>', dir=Direction(down=1, pages=True))
+	map('<CR>', dir=Direction(down=1))
+
+	return map
+
+def vim():
+	# Direction Keys
+	map = KeyMap()
+	map.merge(base_directions())
+	map('j', alias='<down>')
+	map('k', alias='<up>')
+	map('h', alias='<left>')
+	map('l', alias='<right>')
+	map('gg', alias='<home>')
+	map('G', alias='<end>')
+	map('J', dir=Direction(down=20))
+	map('K', dir=Direction(down=-20))
+
+	return map
+
+def system_keys():
+	map = KeyMap()
+	map('Q', fm.exit())
+	map('<mouse>', fm.handle_mouse())
+	map('<C-L>', fm.redraw_window())
+	map('<resize>', fm.resize())
+
+	return map
+
+def browser_keys():
+	map = KeyMap()
+	map.merge(system_keys())
+
+	@map('<dir>')
+	def move(arg):
+		arg.fm.move(dir=arg.direction, narg=arg.n)
+	map(fm.exit(), 'Q')
+
+	map('<cr>', fm.move(dir=Direction(right=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('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('b<bg>', fm.hint("bind_//h//idden //p//review_files" \
+		"//d//irectories_first //c//ollapse_preview flush//i//nput"))
+	map('bh', fm.toggle_boolean_option('show_hidden'))
+	map('bp', fm.toggle_boolean_option('preview_files'))
+	map('bi', fm.toggle_boolean_option('flushinput'))
+	map('bd', fm.toggle_boolean_option('directories_first'))
+	map('bc', 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.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)'))
+
+
+	# --------------------------------------------- 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('gt', fm.cd('/tmp'))
+	map('gs', fm.cd('/srv'))
+	map('gR', fm.cd(RANGERDIR))
+
+	# ------------------------------------------------------- searching
+	map('/', fm.open_console(cmode.SEARCH))
+
+	map('n', fm.search())
+	map('N', fm.search(forward=False))
+
+	map(TAB, 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())
+
+
+	map(':', ';', fm.open_console(cmode.COMMAND))
+
+	# ---------------------------------------------------- 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
+	map('ZZ', fm.exit())
+	map(ctrl('R'), fm.reset())
+	map('R', fm.reload_cwd())
+	map(ctrl('C'), fm.exit())
+
+	map(':', ';', fm.open_console(cmode.COMMAND))
+	map('>', fm.open_console(cmode.COMMAND_QUICK))
+	map('!', fm.open_console(cmode.OPEN))
+	map('r', fm.open_console(cmode.OPEN_QUICK))
+
+	return map
+
+def console_keys():
+	map = KeyMap()
+	map.merge(system_keys())
+
+	@map('<any>')
+	def type_key(arg):
+		arg.wdg.type_key(arg.match)
+
+	map('<up>', wdg.history_move(-1))
+	map('<down>', wdg.history_move(1))
+	map('<tab>', wdg.tab())
+
+#from pprint import pprint
+#pprint(browser_keys()._tree[106].__dict__)
+#raise SystemExit()
+
+ui_keys = browser_keys()
+taskview_keys = ui_keys
+pager_keys = ui_keys
+embedded_pager_keys = ui_keys
+console_keys = console_keys()
+directions = vim()
diff --git a/ranger/ext/direction.py b/ranger/ext/direction.py
new file mode 100644
index 00000000..30eb87ce
--- /dev/null
+++ b/ranger/ext/direction.py
@@ -0,0 +1,121 @@
+# 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 NoDefault(object):
+	pass
+
+class Direction(object):
+	"""An object with a down and right method"""
+	def __init__(self, right=None, down=None, absolute=False,
+			percent=False, pages=False, **keywords):
+		self.has_explicit_direction = False
+
+		if 'up' in keywords:
+			self.down = -keywords['up']
+		else:
+			self.down = down
+
+		if 'left' in keywords:
+			self.right = -keywords['left']
+		else:
+			self.right = right
+
+		if 'relative' in keywords:
+			self.absolute = not relative
+		else:
+			self.absolute = absolute
+
+		if 'default' in keywords:
+			self.default = keywords['default']
+		else:
+			self.default = NoDefault
+
+		self.original_down = self.down
+		self.original_right = self.right
+
+		self.percent = percent
+		self.pages = pages
+	
+	@property
+	def up(self):
+		if self.down is None:
+			return None
+		return -self.down
+
+	@property
+	def left(self):
+		if self.right is None:
+			return None
+		return -self.right
+
+	@property
+	def relative(self):
+		return not self.absolute
+
+	def down_or_default(self, default):
+		if self.has_been_modified:
+			return self.down
+		return default
+
+	def steps_down(self, page_length=10):
+		if self.pages:
+			return self.down * page_length
+		else:
+			return self.down
+
+	def steps_right(self, page_length=10):
+		if self.pages:
+			return self.right * page_length
+		else:
+			return self.right
+
+	def copy(self):
+		new = type(self)()
+		new.__dict__.update(self.__dict__)
+		return new
+
+	def __mul__(self, other):
+		copy = self.copy()
+		if self.absolute:
+			if self.down is not None:
+				copy.down = other
+			if self.right is not None:
+				copy.right = other
+		else:
+			if self.down is not None:
+				copy.down *= other
+			if self.right is not None:
+				copy.right *= other
+		copy.original_down = self.original_down
+		copy.original_right = self.original_right
+		return copy
+	__rmul__ = __mul__
+
+	def __str__(self):
+		s = ['<Direction']
+		if self.down is not None:
+			s.append(" down=" + str(self.down))
+		if self.right is not None:
+			s.append(" right=" + str(self.right))
+		if self.absolute:
+			s.append(" absolute")
+		else:
+			s.append(" relative")
+		if self.pages:
+			s.append(" pages")
+		if self.percent:
+			s.append(" percent")
+		s.append('>')
+		return ''.join(s)
diff --git a/ranger/ext/tree.py b/ranger/ext/tree.py
new file mode 100644
index 00000000..6d841c2a
--- /dev/null
+++ b/ranger/ext/tree.py
@@ -0,0 +1,135 @@
+# 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)
+				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/gui/ui.py b/ranger/gui/ui.py
index af72ee68..2d86736c 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",
@@ -31,7 +31,7 @@ class UI(DisplayableContainer):
 	is_set_up = False
 	mousemask = curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION
 	load_mode = False
-	def __init__(self, commandlist=None, env=None, fm=None):
+	def __init__(self, keymap=None, env=None, fm=None):
 		self._draw_title = os.environ["TERM"] in TERMINALS_WITH_TITLE
 		os.environ['ESCDELAY'] = '25'   # don't know a cleaner way
 
@@ -40,13 +40,13 @@ class UI(DisplayableContainer):
 		if fm is not None:
 			self.fm = fm
 
-
-		if commandlist is None:
-			self.commandlist = CommandList()
-			self.settings.keys.initialize_commands(self.commandlist)
+		if keymap is None:
+			self.keymap = self.settings.keys.browser_keys()
 		else:
-			self.commandlist = commandlist
+			self.keymap = keymap
 		self.win = curses.initscr()
+		self.env.keybuffer.assign(self.keymap, self.settings.keys.directions)
+		self.env.keybuffer.clear()
 
 		DisplayableContainer.__init__(self, None)
 
@@ -138,28 +138,28 @@ class UI(DisplayableContainer):
 		if DisplayableContainer.press(self, key):
 			return
 
-		try:
-			tup = self.env.keybuffer.tuple_without_numbers()
+		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))
 			except Exception as error:
 				self.fm.notify(error)
-			self.env.key_clear()
+			if kbuf.done:
+				kbuf.clear()
+		else:
+			kbuf.clear()
 
 	def get_next_key(self):
 		"""Waits for key input and returns the pressed key"""
diff --git a/ranger/gui/widgets/browserview.py b/ranger/gui/widgets/browserview.py
index ebce9900..8d6dc611 100644
--- a/ranger/gui/widgets/browserview.py
+++ b/ranger/gui/widgets/browserview.py
@@ -24,6 +24,7 @@ class BrowserView(Widget, DisplayableContainer):
 	ratios = None
 	preview = True
 	preview_available = True
+	draw_bookmarks = False
 	stretch_ratios = None
 	need_clear = False
 
@@ -64,10 +65,9 @@ class BrowserView(Widget, DisplayableContainer):
 		self.add_child(self.pager)
 
 	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.old_cf != self.env.cf:
 				self.need_clear = True
 			if self.settings.draw_borders:
diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py
index 20a75c4e..4dea98c7 100644
--- a/ranger/gui/widgets/console.py
+++ b/ranger/gui/widgets/console.py
@@ -54,10 +54,9 @@ class Console(Widget):
 	allow_close = False
 
 	def __init__(self, win):
-		from ranger.container import CommandList, History
+		from ranger.container import History
 		Widget.__init__(self, win)
-		self.commandlist = CommandList()
-		self.settings.keys.initialize_console_commands(self.commandlist)
+		self.keymap = self.settings.keys.console_keys
 		self.clear()
 		self.histories = [None] * 4
 		self.histories[DEFAULT_HISTORY] = History()
diff --git a/ranger/gui/widgets/pager.py b/ranger/gui/widgets/pager.py
index e376a2a6..c5ed8af1 100644
--- a/ranger/gui/widgets/pager.py
+++ b/ranger/gui/widgets/pager.py
@@ -18,7 +18,6 @@ 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 import log
 
@@ -42,14 +41,10 @@ class Pager(Widget):
 		self.markup = None
 		self.lines = []
 
-		self.commandlist = CommandList()
-
 		if embedded:
-			keyfnc = self.settings.keys.initialize_embedded_pager_commands
+			self.keymap = self.settings.keys.embedded_pager_keys
 		else:
-			keyfnc = self.settings.keys.initialize_pager_commands
-
-		keyfnc(self.commandlist)
+			self.keymap = self.settings.keys.pager_keys
 
 	def open(self):
 		self.scroll_begin = 0
@@ -166,7 +161,7 @@ class Pager(Widget):
 		try:
 			tup = self.env.keybuffer.tuple_without_numbers()
 			if tup:
-				cmd = self.commandlist[tup]
+				cmd = self.keymap[tup]
 			else:
 				return
 
diff --git a/ranger/gui/widgets/taskview.py b/ranger/gui/widgets/taskview.py
index 6e86465c..f7937e11 100644
--- a/ranger/gui/widgets/taskview.py
+++ b/ranger/gui/widgets/taskview.py
@@ -22,7 +22,6 @@ from collections import deque
 
 from . import Widget
 from ranger.ext.accumulator import Accumulator
-from ranger.container import CommandList
 
 class TaskView(Widget, Accumulator):
 	old_lst = None
@@ -31,8 +30,7 @@ class TaskView(Widget, Accumulator):
 		Widget.__init__(self, win)
 		Accumulator.__init__(self)
 		self.scroll_begin = 0
-		self.commandlist = CommandList()
-		self.settings.keys.initialize_taskview_commands(self.commandlist)
+		self.keymap = self.settings.keys.taskview_keys
 
 	def draw(self):
 		base_clr = deque()
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_newkeys.py b/test/tc_newkeys.py
new file mode 100644
index 00000000..0c810af5
--- /dev/null
+++ b/test/tc_newkeys.py
@@ -0,0 +1,491 @@
+# 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 *
+
+import sys
+
+class PressTestCase(TestCase):
+	"""Some useful methods for the actual test"""
+	def _mkpress(self, keybuffer, keymap):
+		def press(keys):
+			keybuffer.clear()
+			match = keybuffer.simulate_press(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()
+		kb.simulate_press(keys)
+		self.assertTrue(kb.failure, "Keypress did not fail as expected")
+		kb.clear()
+
+	def assertPressIncomplete(self, kb, keys):
+		kb.clear()
+		kb.simulate_press(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 = kb.simulate_press('pp')
+		args = CommandArgs(0, 0, kb)
+		self.assert_(match)
+		self.assert_(match.function)
+		self.assertEqual(8, match.function(args))
+
+	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_translate_keys(self):
+		def test(string, *args):
+			if not args:
+				args = (string, )
+			self.assertEqual(ordtuple(*args), tuple(translate_keys(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)
+
+		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('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, 50):
+			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(translate_keys('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, kb.simulate_press, '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(40, press('40j'))
+
+		km.map('<dir><dir><any><any>', func=move)
+
+		self.assertEqual(40, press('40jkhl'))
+
+	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)
+
+
+if __name__ == '__main__': main()
diff --git a/test/tc_ui.py b/test/tc_ui.py
index affec907..98ddff93 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, keymap=Fake())
 
 		def fakesetup():
 			self.ui.widget = Fake()