about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--ranger/api/keys.py4
-rw-r--r--ranger/core/actions.py121
-rw-r--r--ranger/defaults/commands.py4
-rw-r--r--ranger/defaults/keys.py74
-rw-r--r--ranger/ext/accumulator.py42
-rw-r--r--ranger/ext/direction.py135
-rw-r--r--ranger/ext/move.py23
-rw-r--r--ranger/fsobject/directory.py2
-rw-r--r--ranger/gui/ui.py2
-rw-r--r--ranger/gui/widgets/browsercolumn.py12
-rw-r--r--ranger/gui/widgets/console.py21
-rw-r--r--ranger/gui/widgets/pager.py76
-rw-r--r--ranger/gui/widgets/taskview.py4
-rw-r--r--test/tc_direction.py81
14 files changed, 402 insertions, 199 deletions
diff --git a/ranger/api/keys.py b/ranger/api/keys.py
index 5b833c34..00479b0d 100644
--- a/ranger/api/keys.py
+++ b/ranger/api/keys.py
@@ -54,9 +54,9 @@ 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')
diff --git a/ranger/core/actions.py b/ranger/core/actions.py
index 029493b0..d7e33c9b 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
 	# --------------------------
@@ -99,48 +127,51 @@ 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_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)
+	def move(self, narg=None, **kw):
+		"""
+		A universal movement method.
 
-	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]))
+		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_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:
-			return
+		to=X is translated to down=X, absolute=True
 
-		if narg is not None:
-			absolute = narg
+		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%
+		"""
+		direction = Direction(kw)
+		if 'left' in direction:
+			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(self.env.cwd),
+					current=self.env.cwd.pointer,
+					pagesize=self.ui.browser.hei)
+			self.env.cwd.move(to=newpos)
 
 	def history_go(self, relative):
 		"""Move back and forth in the history"""
@@ -172,16 +203,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()
 
 	# --------------------------
@@ -262,7 +293,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 +369,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 +386,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()
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..136fe11d 100644
--- a/ranger/defaults/keys.py
+++ b/ranger/defaults/keys.py
@@ -53,26 +53,32 @@ def _vimlike_aliases(map):
 	alias(KEY_HOME, 'gg')
 	alias(KEY_END, 'G')
 
+
+def _emacs_aliases(map):
+	alias = map.alias
+	alias(KEY_LEFT, ctrl('b'))
+	alias(KEY_RIGHT, ctrl('f'))
+	alias(KEY_HOME, ctrl('a'))
+	alias(KEY_END, ctrl('e'))
+	alias(KEY_DC, ctrl('d'))
+	alias(DEL, ctrl('h'))
+
+
 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))
+	_basic_movement(map)
 
-	map(KEY_HOME, fm.move_pointer(absolute=0))
-	map(KEY_END, fm.move_pointer(absolute=-1))
+	map.alias(KEY_LEFT, KEY_BACKSPACE, DEL)
+	map.alias(KEY_RIGHT, KEY_ENTER, ctrl('j'))
 
-	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))
+	map('%', fm.move(to=50, percentage=True))
+	map(KEY_NPAGE, ctrl('f'), fm.move(down=1, pages=True))
+	map(KEY_PPAGE, ctrl('b'), fm.move(up=1, pages=True))
+	map(ctrl('d'), 'J', fm.move(down=0.5, pages=True))
+	map(ctrl('u'), 'K', fm.move(up=0.5, pages=True))
 
 	def move_parent(n):
 		def fnc(arg):
@@ -233,27 +239,24 @@ def initialize_commands(map):
 def initialize_console_commands(map):
 	"""Initialize the commands for the console widget only"""
 
+	_basic_movement(map)
+	_emacs_aliases(map)
+
 	# -------------------------------------------------------- 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))
+	map(KEY_HOME, wdg.move(right=0, absolute=True))
+	map(KEY_END, wdg.move(right=-1, absolute=True))
 
 	# ----------------------------------------- deleting / pasting text
-	map(ctrl('d'), KEY_DC, wdg.delete(0))
-	map(ctrl('h'), KEY_BACKSPACE, DEL, wdg.delete(-1))
+	map(KEY_DC, wdg.delete(0))
+	map(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())
@@ -293,19 +296,20 @@ def initialize_embedded_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))
+	map(KEY_LEFT, wdg.move(left=4))
+	map(KEY_RIGHT, wdg.move(right=4))
+	map(KEY_NPAGE, ctrl('f'), wdg.move(down=1, pages=True))
+	map(KEY_PPAGE, ctrl('b'), wdg.move(up=1, pages=True))
+	map(ctrl('d'), wdg.move(down=0.5, pages=True))
+	map(ctrl('u'), wdg.move(up=0.5, pages=True))
+	map(' ', wdg.move(down=0.8, pages=True))
 
 	# ---------------------------------------------------------- others
 	map('E', fm.edit_file())
@@ -324,7 +328,9 @@ def _system_functions(map):
 
 
 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))
+	map(KEY_DOWN, wdg.move(down=1))
+	map(KEY_UP, wdg.move(up=1))
+	map(KEY_RIGHT, wdg.move(right=1))
+	map(KEY_LEFT, wdg.move(left=1))
+	map(KEY_HOME, wdg.move(to=0))
+	map(KEY_END, wdg.move(to=-1))
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..0b3f55f7
--- /dev/null
+++ b/ranger/ext/direction.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/>.
+
+"""
+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))
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/fsobject/directory.py b/ranger/fsobject/directory.py
index 29f0042c..3574f329 100644
--- a/ranger/fsobject/directory.py
+++ b/ranger/fsobject/directory.py
@@ -215,7 +215,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/ui.py b/ranger/gui/ui.py
index cdaf6cde..6d9c78ad 100644
--- a/ranger/gui/ui.py
+++ b/ranger/gui/ui.py
@@ -159,7 +159,7 @@ class UI(DisplayableContainer):
 				self.hint(cmd.show_obj.hint)
 		elif hasattr(cmd, 'execute'):
 			try:
-				cmd.execute_wrap(self)
+				cmd.execute_wrap(self.fm)
 			except Exception as error:
 				self.fm.notify(error)
 			self.env.key_clear()
diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py
index c0b60a2e..61c74e63 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
 
@@ -372,11 +372,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/console.py b/ranger/gui/widgets/console.py
index 8480ea42..677be2a3 100644
--- a/ranger/gui/widgets/console.py
+++ b/ranger/gui/widgets/console.py
@@ -30,6 +30,7 @@ from ranger import log, relpath_conf
 from ranger.core.runner import ALLOWED_FLAGS
 from ranger.ext.shell_escape import shell_quote
 from ranger.ext.get_executables import get_executables
+from ranger.ext.direction import Direction
 from ranger.container import CommandList, History
 from ranger.container.history import HistoryEmptyException
 import ranger
@@ -216,14 +217,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
@@ -262,7 +265,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):
diff --git a/ranger/gui/widgets/pager.py b/ranger/gui/widgets/pager.py
index 2fc8ecda..91383a18 100644
--- a/ranger/gui/widgets/pager.py
+++ b/ranger/gui/widgets/pager.py
@@ -19,7 +19,7 @@ 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
 
 BAR_REGEXP = re.compile(r'\|\d+\?\|')
 QUOTES_REGEXP = re.compile(r'"[^"]+?"')
@@ -50,6 +50,10 @@ class Pager(Widget):
 
 		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
 		self.markup = None
@@ -116,50 +120,26 @@ 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:
@@ -207,10 +187,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 +218,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/taskview.py b/ranger/gui/widgets/taskview.py
index 6e86465c..6d025048 100644
--- a/ranger/gui/widgets/taskview.py
+++ b/ranger/gui/widgets/taskview.py
@@ -90,11 +90,11 @@ 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:
diff --git a/test/tc_direction.py b/test/tc_direction.py
new file mode 100644
index 00000000..18f9eb4c
--- /dev/null
+++ b/test/tc_direction.py
@@ -0,0 +1,81 @@
+# 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))
+
+if __name__ == '__main__':
+	unittest.main()
+