summary refs log tree commit diff stats
path: root/ranger
diff options
context:
space:
mode:
authorhut <hut@lavabit.com>2010-04-07 15:50:08 +0200
committerhut <hut@lavabit.com>2010-04-07 15:50:08 +0200
commit8d89c6f3a7cf6b0c8abc2ff68ecc7201ac08e872 (patch)
tree610dd3acfdac10ebe1d766a779897207ea6dfb33 /ranger
parent55435343b142c424619e3072475ca8b3366d109c (diff)
parentf45f9734d2ff9fd6b68ff6c879d5b07b0e5c7d02 (diff)
downloadranger-8d89c6f3a7cf6b0c8abc2ff68ecc7201ac08e872.tar.gz
Merge branch 'devel' into newkey
Conflicts:
	ranger/core/actions.py
	ranger/defaults/keys.py
	ranger/ext/direction.py
	ranger/gui/ui.py
	ranger/gui/widgets/browserview.py
	ranger/gui/widgets/pager.py
Diffstat (limited to 'ranger')
-rw-r--r--ranger/__init__.py5
-rw-r--r--ranger/__main__.py38
-rw-r--r--ranger/api/options.py1
-rw-r--r--ranger/colorschemes/default.py13
-rw-r--r--ranger/core/actions.py619
-rw-r--r--ranger/core/environment.py46
-rw-r--r--ranger/core/fm.py34
-rw-r--r--ranger/defaults/apps.py26
-rw-r--r--ranger/defaults/commands.py30
-rw-r--r--ranger/defaults/keys.py141
-rw-r--r--ranger/defaults/options.py38
-rw-r--r--ranger/ext/direction.py202
-rw-r--r--ranger/ext/move.py23
-rw-r--r--ranger/ext/signal_dispatcher.py103
-rw-r--r--ranger/fsobject/directory.py70
-rw-r--r--ranger/fsobject/file.py22
-rw-r--r--ranger/fsobject/fsobject.py8
-rw-r--r--ranger/fsobject/loader.py2
-rw-r--r--ranger/gui/colorscheme.py71
-rw-r--r--ranger/gui/context.py4
-rw-r--r--ranger/gui/defaultui.py7
-rw-r--r--ranger/gui/displayable.py8
-rw-r--r--ranger/gui/mouse_event.py9
-rw-r--r--ranger/gui/ui.py12
-rw-r--r--ranger/gui/widgets/browsercolumn.py90
-rw-r--r--ranger/gui/widgets/browserview.py100
-rw-r--r--ranger/gui/widgets/console.py77
-rw-r--r--ranger/gui/widgets/pager.py82
-rw-r--r--ranger/gui/widgets/statusbar.py13
-rw-r--r--ranger/gui/widgets/titlebar.py57
-rw-r--r--ranger/help/movement.py23
-rw-r--r--ranger/shared/settings.py176
32 files changed, 1341 insertions, 809 deletions
diff --git a/ranger/__init__.py b/ranger/__init__.py
index e2a4983d..f46a1e76 100644
--- a/ranger/__init__.py
+++ b/ranger/__init__.py
@@ -34,7 +34,7 @@ USAGE = '%prog [options] [path/filename]'
 DEFAULT_CONFDIR = '~/.ranger'
 RANGERDIR = os.path.dirname(__file__)
 LOGFILE = '/tmp/errorlog'
-arg = OpenStruct(cd_after_exit=False,
+arg = OpenStruct(
 		debug=False, clean=False, confdir=DEFAULT_CONFDIR,
 		mode=0, flags='', targets=[])
 
@@ -64,6 +64,3 @@ def relpath_conf(*paths):
 def relpath(*paths):
 	"""returns the path relative to rangers library directory"""
 	return os.path.join(RANGERDIR, *paths)
-
-
-from ranger.__main__ import main
diff --git a/ranger/__main__.py b/ranger/__main__.py
index bd01bf4a..674ad8f6 100644
--- a/ranger/__main__.py
+++ b/ranger/__main__.py
@@ -29,38 +29,23 @@ def parse_arguments():
 
 	parser = OptionParser(usage=USAGE, version='ranger ' + __version__)
 
-	# Instead of using this directly, use the embedded
-	# shell script by running ranger with:
-	# source /path/to/ranger /path/to/ranger
-	parser.add_option('--cd-after-exit',
-			action='store_true',
-			help=SUPPRESS_HELP)
-
 	parser.add_option('-d', '--debug', action='store_true',
 			help="activate debug mode")
-
 	parser.add_option('-c', '--clean', action='store_true',
 			help="don't touch/require any config files. ")
-
-	parser.add_option('-r', '--confdir', dest='confdir', type='string',
-			default=DEFAULT_CONFDIR,
+	parser.add_option('-r', '--confdir', type='string',
+			metavar='dir', default=DEFAULT_CONFDIR,
 			help="the configuration directory. (%default)")
-
-	parser.add_option('-m', '--mode', type='int', dest='mode', default=0,
+	parser.add_option('-m', '--mode', type='int', default=0, metavar='n',
 			help="if a filename is supplied, run it with this mode")
-
-	parser.add_option('-f', '--flags', type='string', dest='flags', default='',
+	parser.add_option('-f', '--flags', type='string', default='',
+			metavar='string',
 			help="if a filename is supplied, run it with these flags.")
 
 	options, positional = parser.parse_args()
-
 	arg = OpenStruct(options.__dict__, targets=positional)
-
 	arg.confdir = os.path.expanduser(arg.confdir)
 
-	if arg.cd_after_exit:
-		sys.stderr = sys.__stdout__
-
 	if not arg.clean:
 		try:
 			os.makedirs(arg.confdir)
@@ -71,9 +56,7 @@ def parse_arguments():
 				print("To run ranger without the need for configuration files")
 				print("use the --clean option.")
 				raise SystemExit()
-
 		sys.path.append(arg.confdir)
-
 	return arg
 
 def main():
@@ -100,9 +83,10 @@ def main():
 	if getdefaultlocale()[1] not in ('utf8', 'UTF-8'):
 		for locale in ('en_US.utf8', 'en_US.UTF-8'):
 			try: setlocale(LC_ALL, locale)
-			except: pass  #sometimes there is none available though...
-	else:
-		setlocale(LC_ALL, '')
+			except: pass
+			else: break
+		else: setlocale(LC_ALL, '')
+	else: setlocale(LC_ALL, '')
 
 	arg = parse_arguments()
 	ranger.arg = arg
@@ -132,7 +116,6 @@ def main():
 	try:
 		my_ui = UI()
 		my_fm = FM(ui=my_ui)
-		my_fm.stderr_to_out = arg.cd_after_exit
 
 		# Run the file manager
 		my_fm.initialize()
@@ -142,9 +125,6 @@ def main():
 		# Finish, clean up
 		if 'my_ui' in vars():
 			my_ui.destroy()
-		if arg.cd_after_exit:
-			try: sys.__stderr__.write(my_fm.env.cwd.path)
-			except: pass
 
 if __name__ == '__main__':
 	top_dir = os.path.dirname(sys.path[0])
diff --git a/ranger/api/options.py b/ranger/api/options.py
index 7ead8c90..4748823d 100644
--- a/ranger/api/options.py
+++ b/ranger/api/options.py
@@ -16,6 +16,7 @@
 import re
 from re import compile as regexp
 from ranger import colorschemes as allschemes
+from ranger.gui import color
 
 class AttrToString(object):
 	"""
diff --git a/ranger/colorschemes/default.py b/ranger/colorschemes/default.py
index d1a7e820..24f8ab91 100644
--- a/ranger/colorschemes/default.py
+++ b/ranger/colorschemes/default.py
@@ -21,7 +21,7 @@ class Default(ColorScheme):
 		fg, bg, attr = default_colors
 
 		if context.reset:
-			pass
+			return default_colors
 
 		elif context.in_browser:
 			if context.selected:
@@ -40,11 +40,17 @@ class Default(ColorScheme):
 			if context.container:
 				fg = red
 			if context.directory:
+				attr |= bold
 				fg = blue
 			elif context.executable and not \
-					any((context.media, context.container)):
+					any((context.media, context.container,
+						context.fifo, context.socket)):
 				attr |= bold
 				fg = green
+			if context.socket:
+				fg = magenta
+			if context.fifo:
+				fg = yellow
 			if context.link:
 				fg = context.good and cyan or magenta
 			if context.tag_marker and not context.selected:
@@ -66,6 +72,9 @@ class Default(ColorScheme):
 				fg = context.bad and red or green
 			elif context.directory:
 				fg = blue
+			elif context.tab:
+				if context.good:
+					bg = green
 			elif context.link:
 				fg = cyan
 
diff --git a/ranger/core/actions.py b/ranger/core/actions.py
index e55d65b1..6910871b 100644
--- a/ranger/core/actions.py
+++ b/ranger/core/actions.py
@@ -18,15 +18,288 @@ import shutil
 from inspect import cleandoc
 
 import ranger
-from ranger.shared import EnvironmentAware, SettingsAware
+from ranger.ext.direction import Direction
+from ranger.shared import FileManagerAware, EnvironmentAware, SettingsAware
 from ranger import fsobject
 from ranger.gui.widgets import console_mode as cmode
 from ranger.fsobject import File
 
-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
+	# --------------------------
+
+	def exit(self):
+		"""Exit the program"""
+		raise SystemExit()
+
+	def reset(self):
+		"""Reset the filemanager, clearing the directory buffer"""
+		old_path = self.env.cwd.path
+		self.env.garbage_collect(-1)
+		self.enter_dir(old_path)
+
+	def reload_cwd(self):
+		try:
+			cwd = self.env.cwd
+		except:
+			pass
+		cwd.unload()
+		cwd.load_content()
+
+	def notify(self, text, duration=4, bad=False):
+		if isinstance(text, Exception):
+			if ranger.arg.debug:
+				raise
+			bad = True
+		text = str(text)
+		self.log.appendleft(text)
+		if hasattr(self.ui, 'notify'):
+			self.ui.notify(text, duration=duration, bad=bad)
+
+	def redraw_window(self):
+		"""Redraw the window"""
+		self.ui.redraw_window()
+
+	def open_console(self, mode=':', string=''):
+		"""Open the console if the current UI supports that"""
+		if hasattr(self.ui, 'open_console'):
+			self.ui.open_console(mode, string)
+
+	def execute_file(self, files, **kw):
+		"""Execute a file.
+		app is the name of a method in Applications, without the "app_"
+		flags is a string consisting of runner.ALLOWED_FLAGS
+		mode is a positive integer.
+		Both flags and mode specify how the program is run."""
+
+		if isinstance(files, set):
+			files = list(files)
+		elif type(files) not in (list, tuple):
+			files = [files]
+
+		return self.run(files=list(files), **kw)
+
+	# --------------------------
+	# -- Moving Around
+	# --------------------------
+
+	def move(self, narg=None, **kw):
+		"""
+		A universal movement method.
+
+		Accepts these parameters:
+		(int) down, (int) up, (int) left, (int) right, (int) to,
+		(bool) absolute, (bool) relative, (bool) pages,
+		(bool) percentage
+
+		to=X is translated to down=X, absolute=True
+
+		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)
+
+		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(absolute=newpos)
+
+	def history_go(self, relative):
+		"""Move back and forth in the history"""
+		self.env.history_go(relative)
+
+	def scroll(self, relative):
+		"""Scroll down by <relative> lines"""
+		if hasattr(self.ui, 'scroll'):
+			self.ui.scroll(relative)
+			self.env.cf = self.env.cwd.pointed_obj
+
+	def enter_dir(self, path, remember=False, history=True):
+		"""Enter the directory at the given path"""
+		if remember:
+			cwd = self.env.cwd
+			result = self.env.enter_dir(path, history=history)
+			self.bookmarks.remember(cwd)
+			return result
+		return self.env.enter_dir(path, history=history)
+
+	def cd(self, path, remember=True):
+		"""enter the directory at the given path, remember=True"""
+		self.enter_dir(path, remember=remember)
+
+	def traverse(self):
+		cf = self.env.cf
+		cwd = self.env.cwd
+		if cf is not None and cf.is_directory:
+			self.enter_dir(cf.path)
+		elif cwd.pointer >= len(cwd) - 1:
+			while True:
+				self.move(left=1)
+				cwd = self.env.cwd
+				if cwd.pointer < len(cwd) - 1:
+					break
+				if cwd.path == '/':
+					break
+			self.move(down=1)
+			self.traverse()
+		else:
+			self.move(down=1)
+			self.traverse()
+
+	# --------------------------
+	# -- Shortcuts / Wrappers
+	# --------------------------
+
+	def execute_command(self, cmd, **kw):
+		return self.run(cmd, **kw)
+
+	def edit_file(self, file=None):
+		"""Calls execute_file with the current file and app='editor'"""
+		if file is None:
+			file = self.env.cf
+		elif isinstance(file, str):
+			file = File(os.path.expanduser(file))
+		if file is None:
+			return
+		self.execute_file(file, app = 'editor')
+
+	def toggle_boolean_option(self, string):
+		"""Toggle a boolean option named <string>"""
+		if isinstance(self.env.settings[string], bool):
+			self.env.settings[string] ^= True
+
+	def set_option(self, optname, value):
+		"""Set the value of an option named <optname>"""
+		self.env.settings[optname] = value
+
+	def sort(self, func=None, reverse=None):
+		if reverse is not None:
+			self.env.settings['sort_reverse'] = bool(reverse)
+
+		if func is not None:
+			self.env.settings['sort'] = str(func)
+
+	def set_filter(self, fltr):
+		try:
+			self.env.cwd.filter = fltr
+		except:
+			pass
+
+	def mark(self, all=False, toggle=False, val=None, movedown=None, narg=1):
+		"""
+		A wrapper for the directory.mark_xyz functions.
+
+		Arguments:
+		all - change all files of the current directory at once?
+		toggle - toggle the marked-status?
+		val - mark or unmark?
+		"""
+
+		if self.env.cwd is None:
+			return
+
+		cwd = self.env.cwd
+
+		if not cwd.accessible:
+			return
+
+		if movedown is None:
+			movedown = not all
+
+		if val is None and toggle is False:
+			return
+
+		if all:
+			if toggle:
+				cwd.toggle_all_marks()
+			else:
+				cwd.mark_all(val)
+		else:
+			for i in range(cwd.pointer, min(cwd.pointer + narg, len(cwd))):
+				item = cwd.files[i]
+				if item is not None:
+					if toggle:
+						cwd.toggle_mark(item)
+					else:
+						cwd.mark_item(item, val)
+
+		if movedown:
+			self.move(down=narg)
+
+		if hasattr(self.ui, 'redraw_main_column'):
+			self.ui.redraw_main_column()
+		if hasattr(self.ui, 'status'):
+			self.ui.status.need_redraw = True
+
+	# --------------------------
+	# -- Searching
+	# --------------------------
+
 	def search(self, order=None, forward=True):
 		original_order = order
 		if self.search_forward:
@@ -74,26 +347,11 @@ class Actions(EnvironmentAware, SettingsAware):
 			self.search_method = order
 			self.search_forward = forward
 
-	def resize(self):
-		"""Update the size of the UI"""
-		self.ui.update_size()
-
-	def exit(self):
-		"""Exit the program"""
-		raise SystemExit()
-
-	def enter_dir(self, path, remember=False):
-		"""Enter the directory at the given path"""
-		if remember:
-			cwd = self.env.cwd
-			result = self.env.enter_dir(path)
-			self.bookmarks.remember(cwd)
-			return result
-		return self.env.enter_dir(path)
-
-	def cd(self, path, remember=True):
-		"""enter the directory at the given path, remember=True"""
-		self.enter_dir(path, remember)
+	# --------------------------
+	# -- Tags
+	# --------------------------
+	# Tags are saved in ~/.ranger/tagged and simply mark if a
+	# file is important to you in any context.
 
 	def tag_toggle(self, movedown=None):
 		try:
@@ -107,7 +365,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()
@@ -124,11 +382,16 @@ 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()
 
+	# --------------------------
+	# -- Bookmarks
+	# --------------------------
+	# Using ranger.container.bookmarks.
+
 	def enter_bookmark(self, key):
 		"""Enter the bookmark with the name <key>"""
 		try:
@@ -148,35 +411,10 @@ class Actions(EnvironmentAware, SettingsAware):
 		"""Delete the bookmark with the name <key>"""
 		self.bookmarks.delete(key)
 
-	def move_left(self, narg=None):
-		"""Enter the parent directory"""
-		if narg is None:
-			narg = 1
-		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 history_go(self, relative):
-		"""Move back and forth in the history"""
-		self.env.history_go(relative)
-
-	def handle_mouse(self):
-		"""Handle mouse-buttons if one was pressed"""
-		self.ui.handle_mouse()
+	# --------------------------
+	# -- Pager
+	# --------------------------
+	# These commands open the built-in pager and set specific sources.
 
 	def display_command_help(self, console_widget):
 		if not hasattr(self.ui, 'open_pager'):
@@ -239,229 +477,59 @@ class Actions(EnvironmentAware, SettingsAware):
 			pager = self.ui.open_embedded_pager()
 			pager.set_source(f)
 
-	def execute_file(self, files, **kw):
-		"""Execute a file.
-		app is the name of a method in Applications, without the "app_"
-		flags is a string consisting of runner.ALLOWED_FLAGS
-		mode is a positive integer.
-		Both flags and mode specify how the program is run."""
-
-		if isinstance(files, set):
-			files = list(files)
-		elif type(files) not in (list, tuple):
-			files = [files]
-
-		return self.run(files=list(files), **kw)
-
-	def execute_command(self, cmd, **kw):
-		return self.run(cmd, **kw)
-
-	def edit_file(self, file=None):
-		"""Calls execute_file with the current file and app='editor'"""
-		if file is None:
-			file = self.env.cf
-		elif isinstance(file, str):
-			file = File(os.path.expanduser(file))
-		if file is None:
-			return
-		self.execute_file(file, app = 'editor')
-
-	def open_console(self, mode=':', string=''):
-		"""Open the console if the current UI supports that"""
-		if hasattr(self.ui, 'open_console'):
-			self.ui.open_console(mode, string)
-
-	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, 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)
+	# --------------------------
+	# -- Tabs
+	# --------------------------
+	# This implementation of tabs is very simple and keeps track of
+	# directory paths only.
+
+	def tab_open(self, name, path=None):
+		do_emit_signal = name != self.current_tab
+		self.current_tab = name
+		if path or (name in self.tabs):
+			self.enter_dir(path or self.tabs[name])
 		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]))
-
-	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
-
-		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=absolute)
-
-	def scroll(self, relative):
-		"""Scroll down by <relative> lines"""
-		if hasattr(self.ui, 'scroll'):
-			self.ui.scroll(relative)
-			self.env.cf = self.env.cwd.pointed_obj
-
-	def redraw_window(self):
-		"""Redraw the window"""
-		self.ui.redraw_window()
-
-	def reset(self):
-		"""Reset the filemanager, clearing the directory buffer"""
-		old_path = self.env.cwd.path
-		self.env.directories = {}
-		self.enter_dir(old_path)
-
-	def toggle_boolean_option(self, string):
-		"""Toggle a boolean option named <string>"""
-		if isinstance(self.env.settings[string], bool):
-			self.env.settings[string] ^= True
-
-	def sort(self, func=None, reverse=None):
-		if reverse is not None:
-			self.env.settings['reverse'] = bool(reverse)
-
-		if func is not None:
-			self.env.settings['sort'] = str(func)
-
-	def force_load_preview(self):
-		cf = self.env.cf
-		if hasattr(cf, 'unload') and hasattr(cf, 'load_content'):
-			cf.unload()
-			cf.load_content()
-
-	def reload_cwd(self):
-		try:
-			cwd = self.env.cwd
-		except:
-			pass
-		cwd.unload()
-		cwd.load_content()
-
-	def traverse(self):
-		cf = self.env.cf
-		cwd = self.env.cwd
-		if cf is not None and cf.is_directory:
-			self.enter_dir(cf.path)
-		elif cwd.pointer >= len(cwd) - 1:
-			while True:
-				self.enter_dir('..')
-				cwd = self.env.cwd
-				if cwd.pointer < len(cwd) - 1:
-					break
-				if cwd.path == '/':
-					break
-			self.move_pointer(1)
-			self.traverse()
-		else:
-			self.move_pointer(1)
-			self.traverse()
-
-	def set_filter(self, fltr):
-		try:
-			self.env.cwd.filter = fltr
-		except:
-			pass
-
-	def notify(self, text, duration=4, bad=False):
-		if isinstance(text, Exception):
-			if ranger.arg.debug:
-				raise
-			bad = True
-		text = str(text)
-		self.log.appendleft(text)
-		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.
-
-		Arguments:
-		all - change all files of the current directory at once?
-		toggle - toggle the marked-status?
-		val - mark or unmark?
-		"""
-
-		if self.env.cwd is None:
-			return
-
-		cwd = self.env.cwd
-
-		if not cwd.accessible:
-			return
-
-		if movedown is None:
-			movedown = not all
-
-		if val is None and toggle is False:
-			return
-
-		if all:
-			if toggle:
-				cwd.toggle_all_marks()
-			else:
-				cwd.mark_all(val)
-		else:
-			for i in range(cwd.pointer, min(cwd.pointer + narg, len(cwd))):
-				item = cwd.files[i]
-				if item is not None:
-					if toggle:
-						cwd.toggle_mark(item)
-					else:
-						cwd.mark_item(item, val)
-
-		if movedown:
-			self.move_pointer(relative=narg)
-
-		if hasattr(self.ui, 'redraw_main_column'):
-			self.ui.redraw_main_column()
-		if hasattr(self.ui, 'status'):
-			self.ui.status.need_redraw = True
-
-	# ------------------------------------ filesystem operations
+			self._update_current_tab()
+		if do_emit_signal:
+			self.signal_emit('tab.change')
+
+	def tab_close(self, name=None):
+		if name is None:
+			name = self.current_tab
+		if name == self.current_tab:
+			direction = -1 if name == self._get_tab_list()[-1] else 1
+			previous = self.current_tab
+			self.tab_move(direction)
+			if previous == self.current_tab:
+				return  # can't close last tab
+		if name in self.tabs:
+			del self.tabs[name]
+
+	def tab_move(self, offset):
+		assert isinstance(offset, int)
+		tablist = self._get_tab_list()
+		current_index = tablist.index(self.current_tab)
+		newtab = tablist[(current_index + offset) % len(tablist)]
+		if newtab != self.current_tab:
+			self.tab_open(newtab)
+
+	def tab_new(self):
+		for i in range(10):
+			i = (i + 1) % 10
+			if not i in self.tabs:
+				self.tab_open(i)
+				break
+
+	def _get_tab_list(self):
+		assert len(self.tabs) > 0, "There must be >=1 tabs at all times"
+		return sorted(self.tabs)
+
+	def _update_current_tab(self):
+		self.tabs[self.current_tab] = self.env.cwd.path
+
+	# --------------------------
+	# -- File System Operations
+	# --------------------------
 
 	def copy(self):
 		"""Copy the selected items"""
@@ -567,7 +635,6 @@ class Actions(EnvironmentAware, SettingsAware):
 		except OSError as err:
 			self.notify(err)
 
-
 	def rename(self, src, dest):
 		if hasattr(src, 'path'):
 			src = src.path
diff --git a/ranger/core/environment.py b/ranger/core/environment.py
index 4301d237..0b38c475 100644
--- a/ranger/core/environment.py
+++ b/ranger/core/environment.py
@@ -21,15 +21,15 @@ from os.path import abspath, normpath, join, expanduser, isdir
 
 from ranger.fsobject.directory import Directory, NoDirectoryGiven
 from ranger.container import KeyBuffer, History
+from ranger.ext.signal_dispatcher import SignalDispatcher
 from ranger.shared import SettingsAware
 
-class Environment(SettingsAware):
+class Environment(SettingsAware, SignalDispatcher):
 	"""A collection of data which is relevant for more than
 	one class.
 	"""
 
 	cwd = None  # current directory
-	cf = None  # current file
 	copy = None
 	cmd = None
 	cut = None
@@ -42,7 +42,9 @@ class Environment(SettingsAware):
 	keybuffer = None
 
 	def __init__(self, path):
+		SignalDispatcher.__init__(self)
 		self.path = abspath(expanduser(path))
+		self._cf = None
 		self.pathway = ()
 		self.directories = {}
 		self.keybuffer = KeyBuffer(None, None)
@@ -59,6 +61,22 @@ class Environment(SettingsAware):
 		from ranger.shared import EnvironmentAware
 		EnvironmentAware.env = self
 
+		self.signal_bind('move', self._set_cf_from_signal, priority=0.1,
+				weak=True)
+
+	def _set_cf_from_signal(self, signal):
+		self._cf = signal.new
+
+	def _set_cf(self, value):
+		if value is not self._cf:
+			previous = self._cf
+			self.signal_emit('move', previous=previous, new=value)
+
+	def _get_cf(self):
+		return self._cf
+
+	cf = property(_get_cf, _set_cf)
+
 	def key_append(self, key):
 		"""Append a key to the keybuffer"""
 
@@ -100,14 +118,14 @@ class Environment(SettingsAware):
 			except KeyError:
 				return directory
 
-	def garbage_collect(self):
+	def garbage_collect(self, age):
 		"""Delete unused directory objects"""
-		from ranger.fsobject.fsobject import FileSystemObject
-		for key in tuple(self.directories.keys()):
+		for key in tuple(self.directories):
 			value = self.directories[key]
-			if isinstance(value, FileSystemObject):
-				if value.is_older_than(1200):
-					del self.directories[key]
+			if value.is_older_than(age): # and not value in self.pathway:
+				del self.directories[key]
+				if value.is_directory:
+					value.files = None
 
 	def get_selection(self):
 		if self.cwd:
@@ -154,6 +172,8 @@ class Environment(SettingsAware):
 		if path is None: return
 		path = str(path)
 
+		previous = self.cwd
+
 		# get the absolute path
 		path = normpath(join(self.path, expanduser(path)))
 
@@ -165,9 +185,12 @@ class Environment(SettingsAware):
 		except NoDirectoryGiven:
 			return False
 
+		try:
+			os.chdir(path)
+		except:
+			return True
 		self.path = path
 		self.cwd = new_cwd
-		os.chdir(path)
 
 		self.cwd.load_content_if_outdated()
 
@@ -186,11 +209,14 @@ class Environment(SettingsAware):
 		self.assign_cursor_positions_for_subdirs()
 
 		# set the current file.
-		self.cwd.directories_first = self.settings.directories_first
+		self.cwd.sort_directories_first = self.settings.sort_directories_first
+		self.cwd.sort_reverse = self.settings.sort_reverse
 		self.cwd.sort_if_outdated()
 		self.cf = self.cwd.pointed_obj
 
 		if history:
 			self.history.add(new_cwd)
 
+		self.signal_emit('cd', previous=previous, new=self.cwd)
+
 		return True
diff --git a/ranger/core/fm.py b/ranger/core/fm.py
index 994447b0..25e66407 100644
--- a/ranger/core/fm.py
+++ b/ranger/core/fm.py
@@ -19,6 +19,9 @@ 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
 
 import ranger
 from ranger.core.actions import Actions
@@ -26,23 +29,27 @@ from ranger.container import Bookmarks
 from ranger.core.runner import Runner
 from ranger import relpath_conf
 from ranger.ext.get_executables import get_executables
+from ranger.ext.signal_dispatcher import SignalDispatcher
 from ranger import __version__
 from ranger.fsobject import Loader
 
 CTRL_C = 3
 TICKS_BEFORE_COLLECTING_GARBAGE = 100
+TIME_BEFORE_FILE_BECOMES_GARBAGE = 1200
 
-class FM(Actions):
+class FM(Actions, SignalDispatcher):
 	input_blocked = False
 	input_blocked_until = 0
-	stderr_to_out = False
 	def __init__(self, ui=None, bookmarks=None, tags=None):
 		"""Initialize FM."""
 		Actions.__init__(self)
+		SignalDispatcher.__init__(self)
 		self.ui = ui
 		self.log = deque(maxlen=20)
 		self.bookmarks = bookmarks
 		self.tags = tags
+		self.tabs = {}
+		self.current_tab = 1
 		self.loader = Loader()
 		self._executables = None
 		self.apps = self.settings.apps.CustomApplications()
@@ -55,6 +62,10 @@ class FM(Actions):
 		from ranger.shared import FileManagerAware
 		FileManagerAware.fm = self
 
+		self.log.append('Ranger {0} started! Process ID is {1}.' \
+				.format(__version__, os.getpid()))
+		self.log.append('Running on Python ' + sys.version.replace('\n',''))
+
 	@property
 	def executables(self):
 		if self._executables is None:
@@ -88,6 +99,8 @@ class FM(Actions):
 			self.ui = DefaultUI()
 			self.ui.initialize()
 
+		self.env.signal_bind('cd', self._update_current_tab)
+
 	def block_input(self, sec=0):
 		self.input_blocked = sec != 0
 		self.input_blocked_until = time() + sec
@@ -131,16 +144,21 @@ class FM(Actions):
 				key = ui.get_next_key()
 
 				if key > 0:
-					if self.input_blocked and \
-							time() > self.input_blocked_until:
-						self.input_blocked = False
-					if not self.input_blocked:
-						ui.handle_key(key)
+					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)
 
 				gc_tick += 1
 				if gc_tick > TICKS_BEFORE_COLLECTING_GARBAGE:
 					gc_tick = 0
-					env.garbage_collect()
+					env.garbage_collect(TIME_BEFORE_FILE_BECOMES_GARBAGE)
 
 		except KeyboardInterrupt:
 			# this only happens in --debug mode. By default, interrupts
diff --git a/ranger/defaults/apps.py b/ranger/defaults/apps.py
index 347b9ce2..9543badb 100644
--- a/ranger/defaults/apps.py
+++ b/ranger/defaults/apps.py
@@ -59,6 +59,7 @@ class CustomApplications(Applications):
 
 		if f.extension is not None:
 			if f.extension in ('pdf'):
+				c.flags += 'd'
 				return self.either(c, 'evince', 'zathura', 'apvlv')
 			if f.extension in ('html', 'htm', 'xhtml', 'swf'):
 				return self.either(c, 'firefox', 'opera', 'elinks')
@@ -114,7 +115,7 @@ class CustomApplications(Applications):
 	@depends_on('mplayer')
 	def app_mplayer(self, c):
 		if c.mode is 1:
-			return tup('mplayer', *c)
+			return tup('mplayer', '-fs', *c)
 
 		elif c.mode is 2:
 			args = "mplayer -fs -sid 0 -vfm ffmpeg -lavdopts " \
@@ -126,7 +127,7 @@ class CustomApplications(Applications):
 			return tup('mplayer', '-mixer', 'software', *c)
 
 		else:
-			return tup('mplayer', '-fs', *c)
+			return tup('mplayer', *c)
 
 	@depends_on("eog")
 	def app_eye_of_gnome(self, c):
@@ -151,15 +152,18 @@ class CustomApplications(Applications):
 		if len(c.files) > 1:
 			return tup('feh', *c)
 
-		from collections import deque
+		try:
+			from collections import deque
 
-		directory = self.fm.env.get_directory(c.file.dirname)
-		images = [f.path for f in directory.files if f.image]
-		position = images.index(c.file.path)
-		deq = deque(images)
-		deq.rotate(-position)
+			directory = self.fm.env.get_directory(c.file.dirname)
+			images = [f.path for f in directory.files if f.image]
+			position = images.index(c.file.path)
+			deq = deque(images)
+			deq.rotate(-position)
 
-		return tup('feh', *deq)
+			return tup('feh', *deq)
+		except:
+			return tup('feh', *c)
 
 	@depends_on("gimp")
 	def app_gimp(self, c):
@@ -231,6 +235,6 @@ class CustomApplications(Applications):
 	@depends_on('totem')
 	def app_totem(self, c):
 		if c.mode is 0:
-			return tup("totem", "--fullscreen", *c)
-		if c.mode is 1:
 			return tup("totem", *c)
+		if c.mode is 1:
+			return tup("totem", "--fullscreen", *c)
diff --git a/ranger/defaults/commands.py b/ranger/defaults/commands.py
index b1518013..f653168f 100644
--- a/ranger/defaults/commands.py
+++ b/ranger/defaults/commands.py
@@ -199,9 +199,10 @@ class find(Command):
 		search = parse(self.line).rest(1)
 		search = re.escape(search)
 		self.fm.env.last_search = re.compile(search, re.IGNORECASE)
+		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):
@@ -239,11 +240,25 @@ class quit(Command):
 	"""
 	:quit
 
+	Closes the current tab.  If there is only one tab, quit the program.
+	"""
+
+	def execute(self):
+		if len(self.fm.tabs) <= 1:
+			self.fm.exit()
+		self.fm.tab_close()
+
+
+class quit_now(Command):
+	"""
+	:quit!
+
 	Quits the program immediately.
 	"""
+	name = 'quit!'
 
 	def execute(self):
-		raise SystemExit
+		self.fm.exit()
 
 
 class delete(Command):
@@ -277,8 +292,11 @@ class delete(Command):
 			# user did not confirm deletion
 			return
 
-		if self.fm.env.cwd.marked_items \
-		or (self.fm.env.cf.is_directory and not self.fm.env.cf.empty()):
+		cwd = self.fm.env.cwd
+		cf = self.fm.env.cf
+
+		if cwd.marked_items or (cf.is_directory and not cf.islink \
+				and len(os.listdir(cf.path)) > 0):
 			# better ask for a confirmation, when attempting to
 			# delete multiple files or a non-empty directory.
 			return self.fm.open_console(self.mode, delete.WARNING)
@@ -495,5 +513,7 @@ def get_command(name, abbrev=True):
 def command_generator(start):
 	return (cmd + ' ' for cmd in by_name if cmd.startswith(start))
 
-alias(e=edit)  # to make :e unambiguous.
+alias(e=edit, q=quit)  # for unambiguity
+alias(**{'q!':quit_now})
+alias(qall=quit_now)
 
diff --git a/ranger/defaults/keys.py b/ranger/defaults/keys.py
index f48c7012..a236ad9f 100644
--- a/ranger/defaults/keys.py
+++ b/ranger/defaults/keys.py
@@ -53,28 +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))
-	map(KEY_HOME, fm.move_pointer(absolute=0))
-	map(KEY_END, fm.move_pointer(absolute=-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, fm.move_pointer_by_pages(1))
-	map(KEY_PPAGE, 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))
 
 	map(']', fm.traverse())
 	map('[', fm.history_go(-1))
@@ -107,14 +111,16 @@ def initialize_commands(map):
 	map('du', fm.execute_command('du --max-depth=1 -h | less'))
 
 	# -------------------------------------------------- toggle options
-	map('b', hint="show_//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('bP', fm.toggle_boolean_option('preview_directories'))
-	map('bi', fm.toggle_boolean_option('flushinput'))
-	map('bd', fm.toggle_boolean_option('directories_first'))
-	map('bc', fm.toggle_boolean_option('collapse_preview'))
+	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")
@@ -133,7 +139,7 @@ def initialize_commands(map):
 			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))
+			arg.fm.sort(reverse=not arg.fm.settings.sort_reverse))
 
 	# ----------------------------------------------- console shortcuts
 	@map("A")
@@ -146,6 +152,8 @@ def initialize_commands(map):
 	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('~'))
@@ -162,24 +170,32 @@ def initialize_commands(map):
 	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(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(TAB, fm.search(order='tag'))
+	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')
+	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", draw_bookmarks=True)
+	map("`", "'", "m", "um", draw_bookmarks=True)
 
 	# ---------------------------------------------------- change views
 	map('i', fm.display_file())
@@ -204,10 +220,18 @@ def initialize_commands(map):
 
 	# ------------------------------------------------ system functions
 	_system_functions(map)
-	map('ZZ', fm.exit())
+	map('ZZ', 'ZQ', fm.exit())
 	map(ctrl('R'), fm.reset())
 	map('R', fm.reload_cwd())
-	map(ctrl('C'), fm.exit())
+	@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))
@@ -220,33 +244,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())
 
-	# ----------------------------------------------------- typing keys
-	def type_key(arg):
-		arg.wdg.type_key(arg.keys)
-
-	for i in range(ord(' '), ord('~')+1):
-		map(i, type_key)
-
 	# ------------------------------------------------ system functions
-	_system_functions(map)
-
 	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())
@@ -286,42 +301,44 @@ 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, wdg.move(relative=1, pages=True))
-	map(KEY_PPAGE, 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())
 	map('?', fm.display_help())
 
 	# --------------------------------------------- less-like shortcuts
-	map.alias(KEY_NPAGE, 'd')
-	map.alias(KEY_PPAGE, 'u')
+	map.alias(KEY_NPAGE, 'f')
+	map.alias(KEY_PPAGE, 'b')
+	map.alias(ctrl('d'), 'd')
+	map.alias(ctrl('u'), 'u')
 
 
 def _system_functions(map):
-	# Each commandlist should have this bindings
-	map(KEY_RESIZE, fm.resize())
-	map(KEY_MOUSE, fm.handle_mouse())
 	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))
+	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))
 
 
 
@@ -339,7 +356,7 @@ def base_directions():
 	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('%<any>', dir=Direction(down=1, percentage=True, absolute=True))
 	map('<space>', dir=Direction(down=1, pages=True))
 	map('<CR>', dir=Direction(down=1))
 
diff --git a/ranger/defaults/options.py b/ranger/defaults/options.py
index a7090285..6b2a0dc9 100644
--- a/ranger/defaults/options.py
+++ b/ranger/defaults/options.py
@@ -33,7 +33,8 @@ of the values stay the same.
 
 from ranger.api.options import *
 
-# Which files are hidden if show_hidden is False?
+# Which files should be hidden?  Toggle this by typing `zh' or
+# changing the setting `show_hidden'
 hidden_filter = regexp(
 	r'lost\+found|^\.|~$|\.(:?pyc|pyo|bak|swp)$')
 show_hidden = False
@@ -50,8 +51,19 @@ preview_directories = True
 max_filesize_for_preview = 300 * 1024  # 300kb
 collapse_preview = True
 
+# Save the console history on exit?
+save_console_history = True
+
 # Draw borders around columns?
 draw_borders = False
+draw_bookmark_borders = True
+
+# How many columns are there, and what are their relative widths?
+column_ratios = (1, 1, 4, 3)
+
+# Display the file size in the main column or status bar?
+display_size_in_main_column = True
+display_size_in_status_bar = False
 
 # Set a title for the window?
 update_title = True
@@ -80,6 +92,26 @@ show_cursor = False
 
 # One of: size, basename, mtime, type
 sort = 'basename'
-reverse = False
-directories_first = True
+sort_reverse = False
+sort_case_insensitive = False
+sort_directories_first = True
+
+
+# Apply an overlay function to the colorscheme.  It will be called with
+# 4 arguments: the context and the 3 values (fg, bg, attr) returned by
+# the original use() function of your colorscheme.  The return value
+# must be a 3-tuple of (fg, bg, attr).
+# Note: Here, the colors/attributes aren't directly imported into
+# the namespace but have to be accessed with color.xyz.
+def colorscheme_overlay(context, fg, bg, attr):
+	if context.directory and attr & color.bold and \
+			not any((context.marked, context.selected)):
+		attr ^= color.bold  # I don't like bold directories!
+
+	if context.main_column and context.selected:
+		fg, bg = color.red, color.default  # To highlight the main column!
+
+	return fg, bg, attr
 
+# The above function was just an example, let's set it back to None
+colorscheme_overlay = None
diff --git a/ranger/ext/direction.py b/ranger/ext/direction.py
index 30eb87ce..417f3add 100644
--- a/ranger/ext/direction.py
+++ b/ranger/ext/direction.py
@@ -13,109 +13,123 @@
 # 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']
+"""
+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:
-			self.right = right
+			dict.__init__(self, keywords)
+		if 'to' in self:
+			self['down'] = self['to']
+			self['absolute'] = True
 
-		if 'relative' in keywords:
-			self.absolute = not relative
-		else:
-			self.absolute = absolute
+	def copy(self):
+		return Direction(**self)
 
-		if 'default' in keywords:
-			self.default = keywords['default']
-		else:
-			self.default = NoDefault
+	def _get_bool(self, first, second, fallback=None):
+		try: return self[first]
+		except:
+			try: return not self[second]
+			except: return fallback
 
-		self.original_down = self.down
-		self.original_right = self.right
+	def _get_direction(self, first, second, fallback=0):
+		try: return self[first]
+		except:
+			try: return -self[second]
+			except: return fallback
 
-		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
+		return -Direction.down(self)
 
-	@property
-	def relative(self):
-		return not self.absolute
+	def down(self):
+		return Direction._get_direction(self, 'down', 'up')
 
-	def down_or_default(self, default):
-		if self.has_been_modified:
-			return self.down
-		return default
+	def right(self):
+		return Direction._get_direction(self, 'right', 'left')
 
-	def steps_down(self, page_length=10):
-		if self.pages:
-			return self.down * page_length
-		else:
-			return self.down
+	def absolute(self):
+		return Direction._get_bool(self, 'absolute', 'relative')
 
-	def steps_right(self, page_length=10):
-		if self.pages:
-			return self.right * page_length
-		else:
-			return self.right
+	def left(self):
+		return -Direction.right(self)
 
-	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")
+	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:
-			s.append(" relative")
-		if self.pages:
-			s.append(" pages")
-		if self.percent:
-			s.append(" percent")
-		s.append('>')
-		return ''.join(s)
+			pos += current
+		return int(max(min(pos, maximum + offset), 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/ext/signal_dispatcher.py b/ranger/ext/signal_dispatcher.py
new file mode 100644
index 00000000..c1630c0c
--- /dev/null
+++ b/ranger/ext/signal_dispatcher.py
@@ -0,0 +1,103 @@
+# 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 weakref
+from types import MethodType
+
+class Signal(dict):
+	stopped = False
+	def __init__(self, **keywords):
+		dict.__init__(self, keywords)
+		self.__dict__ = self
+
+	def stop(self):
+		self.stopped = True
+
+
+class SignalHandler(object):
+	active = True
+	def __init__(self, signal_name, function, priority, pass_signal):
+		self.priority = max(0, min(1, priority))
+		self.signal_name = signal_name
+		self.function = function
+		self.pass_signal = pass_signal
+
+
+class SignalDispatcher(object):
+	def __init__(self):
+		self._signals = dict()
+
+	signal_clear = __init__
+
+	def signal_bind(self, signal_name, function, priority=0.5, weak=False):
+		assert isinstance(signal_name, str)
+		try:
+			handlers = self._signals[signal_name]
+		except:
+			handlers = self._signals[signal_name] = []
+		nargs = function.__code__.co_argcount
+
+		try:
+			instance = function.__self__
+		except:
+			if weak:
+				function = weakref.proxy(function)
+		else:
+			nargs -= 1
+			if weak:
+				function = (function.__func__, weakref.proxy(function.__self__))
+		handler = SignalHandler(signal_name, function, priority, nargs > 0)
+		handlers.append(handler)
+		handlers.sort(key=lambda handler: -handler.priority)
+		return handler
+
+	def signal_unbind(self, signal_handler):
+		try:
+			handlers = self._signals[signal_handler.signal_name]
+		except:
+			pass
+		else:
+			try:
+				handlers.remove(signal_handler)
+			except:
+				pass
+
+	def signal_emit(self, signal_name, **kw):
+		assert isinstance(signal_name, str)
+		try:
+			handlers = self._signals[signal_name]
+		except:
+			return
+		if not handlers:
+			return
+
+		signal = Signal(origin=self, name=signal_name, **kw)
+
+		# propagate
+		for handler in tuple(handlers):
+			if handler.active:
+				try:
+					if isinstance(handler.function, tuple):
+						fnc = MethodType(*handler.function)
+					else:
+						fnc = handler.function
+					if handler.pass_signal:
+						fnc(signal)
+					else:
+						fnc()
+					if signal.stopped:
+						return
+				except ReferenceError:
+					handlers.remove(handler)
diff --git a/ranger/fsobject/directory.py b/ranger/fsobject/directory.py
index 8bb8a78a..79e32bff 100644
--- a/ranger/fsobject/directory.py
+++ b/ranger/fsobject/directory.py
@@ -17,7 +17,6 @@ import os
 from collections import deque
 from time import time
 
-from ranger import log
 from ranger.fsobject import BAD_INFO, File, FileSystemObject
 from ranger.shared import SettingsAware
 from ranger.ext.accumulator import Accumulator
@@ -27,9 +26,13 @@ def sort_by_basename(path):
 	"""returns path.basename (for sorting)"""
 	return path.basename
 
+def sort_by_basename_icase(path):
+	"""returns case-insensitive path.basename (for sorting)"""
+	return path.basename_lower
+
 def sort_by_directory(path):
 	"""returns 0 if path is a directory, otherwise 1 (for sorting)"""
-	return 1 - int( isinstance( path, Directory ) )
+	return 1 - path.is_directory
 
 class NoDirectoryGiven(Exception):
 	pass
@@ -54,12 +57,8 @@ class Directory(FileSystemObject, Accumulator, SettingsAware):
 	last_update_time = -1
 	load_content_mtime = -1
 
-	old_show_hidden = None
-	old_directories_first = None
-	old_reverse = None
-	old_sort = None
-	old_filter = None
-	old_hidden_filter = None
+	order_outdated = False
+	content_outdated = False
 
 	sort_dict = {
 		'basename': sort_by_basename,
@@ -79,13 +78,20 @@ class Directory(FileSystemObject, Accumulator, SettingsAware):
 
 		self.marked_items = list()
 
-		# to find out if something has changed:
-		self.old_show_hidden = self.settings.show_hidden
-		self.old_directories_first = self.settings.directories_first
-		self.old_sort = self.settings.sort
-		self.old_filter = self.filter
-		self.old_hidden_filter = self.settings.hidden_filter
-		self.old_reverse = self.settings.reverse
+		for opt in ('sort_directories_first', 'sort', 'sort_reverse',
+				'sort_case_insensitive'):
+			self.settings.signal_bind('setopt.' + opt,
+					self.request_resort, weak=True)
+
+		for opt in ('filter', 'hidden_filter', 'show_hidden'):
+			self.settings.signal_bind('setopt.' + opt,
+				self.request_reload, weak=True)
+
+	def request_resort(self):
+		self.order_outdated = True
+
+	def request_reload(self):
+		self.content_outdated = True
 
 	def get_list(self):
 		return self.files
@@ -203,7 +209,6 @@ class Directory(FileSystemObject, Accumulator, SettingsAware):
 					else:
 						self.mark_item(item, False)
 
-				self.old_directories_first = None
 				self.sort()
 
 				if len(self.files) > 0:
@@ -236,8 +241,12 @@ class Directory(FileSystemObject, Accumulator, SettingsAware):
 		if not self.loading:
 			self.load_once()
 
+			if not self.accessible:
+				self.content_loaded = True
+				return
+
 			if schedule is None:
-				schedule = self.size > 30
+				schedule = True   # was: self.size > 30
 
 			if self.load_generator is None:
 				self.load_generator = self.load_bit_by_bit()
@@ -265,12 +274,17 @@ class Directory(FileSystemObject, Accumulator, SettingsAware):
 			sort_func = self.sort_dict[self.settings.sort]
 		except:
 			sort_func = sort_by_basename
+
+		if self.settings.sort_case_insensitive and \
+				sort_func == sort_by_basename:
+			sort_func = sort_by_basename_icase
+
 		self.files.sort(key = sort_func)
 
-		if self.settings.reverse:
+		if self.settings.sort_reverse:
 			self.files.reverse()
 
-		if self.settings.directories_first:
+		if self.settings.sort_directories_first:
 			self.files.sort(key = sort_by_directory)
 
 		if self.pointer is not None:
@@ -278,15 +292,10 @@ class Directory(FileSystemObject, Accumulator, SettingsAware):
 		else:
 			self.correct_pointer()
 
-		self.old_directories_first = self.settings.directories_first
-		self.old_sort = self.settings.sort
-		self.old_reverse = self.settings.reverse
-
 	def sort_if_outdated(self):
 		"""Sort the containing files if they are outdated"""
-		if self.old_directories_first != self.settings.directories_first \
-				or self.old_sort != self.settings.sort \
-				or self.old_reverse != self.settings.reverse:
+		if self.order_outdated:
+			self.order_outdated = False
 			self.sort()
 			return True
 		return False
@@ -361,12 +370,8 @@ class Directory(FileSystemObject, Accumulator, SettingsAware):
 
 		if self.load_content_once(*a, **k): return True
 
-		if self.old_show_hidden != self.settings.show_hidden or \
-				self.old_filter != self.filter or \
-				self.old_hidden_filter != self.settings.hidden_filter:
-			self.old_filter = self.filter
-			self.old_hidden_filter = self.settings.hidden_filter
-			self.old_show_hidden = self.settings.show_hidden
+		if self.content_outdated:
+			self.content_outdated = False
 			self.load_content(*a, **k)
 			return True
 
@@ -374,6 +379,7 @@ class Directory(FileSystemObject, Accumulator, SettingsAware):
 			real_mtime = os.stat(self.path).st_mtime
 		except OSError:
 			real_mtime = None
+			return False
 		if self.stat:
 			cached_mtime = self.load_content_mtime
 		else:
diff --git a/ranger/fsobject/file.py b/ranger/fsobject/file.py
index 86621095..aa44016e 100644
--- a/ranger/fsobject/file.py
+++ b/ranger/fsobject/file.py
@@ -13,6 +13,28 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+control_characters = set(chr(n) for n in set(range(0, 9)) | set(range(14,32)))
+N_FIRST_BYTES = 20
+
 from .fsobject import FileSystemObject as SuperClass
 class File(SuperClass):
 	is_file = True
+
+	@property
+	def firstbytes(self):
+		try:
+			return self._firstbytes
+		except:
+			try:
+				f = open(self.path, 'r')
+				self._firstbytes = f.read(N_FIRST_BYTES)
+				f.close()
+				return self._firstbytes
+			except:
+				pass
+
+	def is_binary(self):
+		if self.firstbytes and control_characters & set(self.firstbytes):
+			return True
+		return False
+
diff --git a/ranger/fsobject/fsobject.py b/ranger/fsobject/fsobject.py
index 4278c3e8..1ab3addd 100644
--- a/ranger/fsobject/fsobject.py
+++ b/ranger/fsobject/fsobject.py
@@ -17,6 +17,7 @@ CONTAINER_EXTENSIONS = 'rar zip tar gz bz bz2 tgz 7z iso cab'.split()
 DOCUMENT_EXTENSIONS = 'pdf doc ppt odt'.split()
 DOCUMENT_BASENAMES = 'README TODO LICENSE COPYING INSTALL'.split()
 
+import time
 from . import T_FILE, T_DIRECTORY, T_UNKNOWN, T_NONEXISTANT, BAD_INFO
 from ranger.shared import MimeTypeAware, FileManagerAware
 from ranger.ext.shell_escape import shell_escape
@@ -81,6 +82,9 @@ class FileSystemObject(MimeTypeAware, FileManagerAware):
 		self.set_mimetype()
 		self.use()
 
+	def __repr__(self):
+		return "<{0} {1}>".format(self.__class__.__name__, self.path)
+
 	@property
 	def shell_escaped_basename(self):
 		if self._shell_escaped_basename is None:
@@ -109,12 +113,12 @@ class FileSystemObject(MimeTypeAware, FileManagerAware):
 
 	def use(self):
 		"""mark the filesystem-object as used at the current time"""
-		import time
 		self.last_used = time.time()
 
 	def is_older_than(self, seconds):
 		"""returns whether this object wasn't use()d in the last n seconds"""
-		import time
+		if seconds < 0:
+			return True
 		return self.last_used + seconds < time.time()
 
 	def set_mimetype(self):
diff --git a/ranger/fsobject/loader.py b/ranger/fsobject/loader.py
index b47dd9c9..4f4424e4 100644
--- a/ranger/fsobject/loader.py
+++ b/ranger/fsobject/loader.py
@@ -77,7 +77,7 @@ class Loader(FileManagerAware):
 
 	def remove(self, item=None, index=None):
 		if item is not None and index is None:
-			for test, i in zip(self.queue, range(len(self.queue))):
+			for i, test in enumerate(self.queue):
 				if test == item:
 					index = i 
 					break
diff --git a/ranger/gui/colorscheme.py b/ranger/gui/colorscheme.py
index 199a5523..dffacffb 100644
--- a/ranger/gui/colorscheme.py
+++ b/ranger/gui/colorscheme.py
@@ -41,11 +41,20 @@ If your colorscheme-file contains more than one colorscheme, specify it with:
 colorscheme = colorschemes.filename.classname
 """
 
+import os
 from curses import color_pair
+from inspect import isclass, ismodule
+
+import ranger
 from ranger.gui.color import get_color
 from ranger.gui.context import Context
+from ranger.shared.settings import SettingsAware
+
+# ColorScheme is not SettingsAware but it will gain access
+# to the settings during the initialization.  We can't import
+# SettingsAware here because of circular imports.
 
-class ColorScheme(object):
+class ColorScheme(SettingsAware):
 	"""
 	This is the class that colorschemes must inherit from.
 
@@ -73,6 +82,14 @@ class ColorScheme(object):
 
 			# add custom error messages for broken colorschemes
 			color = self.use(context)
+			if self.settings.colorscheme_overlay:
+				result = self.settings.colorscheme_overlay(context, *color)
+				assert isinstance(result, (tuple, list)), \
+						"Your colorscheme overlay doesn't return a tuple!"
+				assert all(isinstance(val, int) for val in result), \
+						"Your colorscheme overlay doesn't return a tuple"\
+						" containing 3 integers!"
+				color = result
 			self.cache[keys] = color
 			return color
 
@@ -102,3 +119,55 @@ class ColorScheme(object):
 			attr |= 2097152
 			fg = 4
 		return fg, -1, attr
+
+def _colorscheme_name_to_class(signal):
+	# Find the colorscheme.  First look for it at ~/.ranger/colorschemes,
+	# then at RANGERDIR/colorschemes.  If the file contains a class
+	# named Scheme, it is used.  Otherwise, an arbitrary other class
+	# is picked.
+	if isinstance(signal.value, ColorScheme): return
+
+	scheme_name = signal.value
+	usecustom = not ranger.arg.clean
+
+	def exists(colorscheme):
+		return os.path.exists(colorscheme + '.py')
+
+	def is_scheme(x):
+		return isclass(x) and issubclass(x, ColorScheme)
+
+	# create ~/.ranger/colorschemes/__init__.py if it doesn't exist
+	if usecustom:
+		if os.path.exists(ranger.relpath_conf('colorschemes')):
+			initpy = ranger.relpath_conf('colorschemes', '__init__.py')
+			if not os.path.exists(initpy):
+				open(initpy, 'a').close()
+
+	if usecustom and \
+			exists(ranger.relpath_conf('colorschemes', scheme_name)):
+		scheme_supermodule = 'colorschemes'
+	elif exists(ranger.relpath('colorschemes', scheme_name)):
+		scheme_supermodule = 'ranger.colorschemes'
+	else:
+		scheme_supermodule = None  # found no matching file.
+
+	if scheme_supermodule is None:
+		# XXX: dont print while curses is running
+		print("ERROR: colorscheme not found, fall back to builtin scheme")
+		if ranger.arg.debug:
+			raise Exception("Cannot locate colorscheme!")
+		signal.value = ColorScheme()
+	else:
+		scheme_module = getattr(__import__(scheme_supermodule,
+				globals(), locals(), [scheme_name], 0), scheme_name)
+		assert ismodule(scheme_module)
+		if hasattr(scheme_module, 'Scheme') \
+				and is_scheme(scheme_module.Scheme):
+			signal.value = scheme_module.Scheme()
+		else:
+			for name, var in scheme_module.__dict__.items():
+				if var != ColorScheme and is_scheme(var):
+					signal.value = var()
+					break
+			else:
+				raise Exception("The module contains no valid colorscheme!")
diff --git a/ranger/gui/context.py b/ranger/gui/context.py
index d31124ca..4ea50714 100644
--- a/ranger/gui/context.py
+++ b/ranger/gui/context.py
@@ -17,7 +17,7 @@ CONTEXT_KEYS = ['reset', 'error',
 		'in_browser', 'in_statusbar', 'in_titlebar', 'in_console',
 		'in_pager', 'in_taskview',
 		'directory', 'file', 'hostname',
-		'executable', 'media', 'link',
+		'executable', 'media', 'link', 'fifo', 'socket',
 		'video', 'audio', 'image', 'media', 'document', 'container',
 		'selected', 'empty', 'main_column', 'message', 'background',
 		'good', 'bad',
@@ -26,7 +26,7 @@ CONTEXT_KEYS = ['reset', 'error',
 		'marked', 'tagged', 'tag_marker',
 		'help_markup',
 		'seperator', 'key', 'special', 'border',
-		'title', 'text', 'highlight', 'bars', 'quotes',
+		'title', 'text', 'highlight', 'bars', 'quotes', 'tab',
 		'keybuffer']
 
 class Context(object):
diff --git a/ranger/gui/defaultui.py b/ranger/gui/defaultui.py
index e6a365de..08e0b204 100644
--- a/ranger/gui/defaultui.py
+++ b/ranger/gui/defaultui.py
@@ -15,8 +15,6 @@
 
 from ranger.gui.ui import UI
 
-RATIO = ( 3, 3, 12, 9 )
-
 class DefaultUI(UI):
 	def setup(self):
 		"""Build up the UI by initializing widgets."""
@@ -32,9 +30,10 @@ class DefaultUI(UI):
 		self.add_child(self.titlebar)
 
 		# Create the browser view
-		self.browser = BrowserView(self.win, RATIO)
+		self.browser = BrowserView(self.win, self.settings.column_ratios)
+		self.settings.signal_bind('setopt.column_ratios',
+				self.browser.change_ratios)
 		self.add_child(self.browser)
-		self.main_column = self.browser.main_column
 
 		# Create the process manager
 		self.taskview = TaskView(self.win)
diff --git a/ranger/gui/displayable.py b/ranger/gui/displayable.py
index ceefb3f1..a7a0945d 100644
--- a/ranger/gui/displayable.py
+++ b/ranger/gui/displayable.py
@@ -72,6 +72,7 @@ class Displayable(EnvironmentAware, FileManagerAware, CursesShortcuts):
 		self.hei = 0
 		self.paryx = (0, 0)
 		self.parent = None
+		self.fresh = True
 
 		self._old_visible = self.visible
 
@@ -154,7 +155,7 @@ class Displayable(EnvironmentAware, FileManagerAware, CursesShortcuts):
 
 	def resize(self, y, x, hei=None, wid=None):
 		"""Resize the widget"""
-		do_move = False
+		do_move = self.fresh
 		try:
 			maxy, maxx = self.env.termsize
 		except TypeError:
@@ -212,6 +213,7 @@ class Displayable(EnvironmentAware, FileManagerAware, CursesShortcuts):
 			except:
 				pass
 
+			self.fresh = False
 			self.paryx = self.win.getparyx()
 			self.y, self.x = self.paryx
 			if self.parent:
@@ -291,7 +293,7 @@ class DisplayableContainer(Displayable):
 			return True
 
 		for displayable in self.container:
-			if event in displayable:
+			if displayable.visible and event in displayable:
 				if displayable.click(event):
 					return True
 
@@ -314,7 +316,7 @@ class DisplayableContainer(Displayable):
 	def remove_child(self, obj):
 		"""Remove the object from the container."""
 		try:
-			container.remove(obj)
+			self.container.remove(obj)
 		except ValueError:
 			pass
 		else:
diff --git a/ranger/gui/mouse_event.py b/ranger/gui/mouse_event.py
index d588fd8e..f3955825 100644
--- a/ranger/gui/mouse_event.py
+++ b/ranger/gui/mouse_event.py
@@ -41,6 +41,15 @@ class MouseEvent(object):
 		except:
 			return False
 
+	def mouse_wheel_direction(self):
+		if self.bstate & curses.BUTTON4_PRESSED:
+			return -1
+		elif self.bstate & curses.BUTTON2_PRESSED \
+				or self.bstate > curses.ALL_MOUSE_EVENTS:
+			return 1
+		else:
+			return 0
+
 	def ctrl(self):
 		return self.bstate & curses.BUTTON_CTRL
 
diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py
index 2d86736c..c7c2090a 100644
--- a/ranger/gui/ui.py
+++ b/ranger/gui/ui.py
@@ -84,8 +84,6 @@ class UI(DisplayableContainer):
 
 	def suspend(self):
 		"""Turn off curses"""
-		# from ranger import log
-		# log("suspending ui!")
 		self.win.keypad(0)
 		curses.nocbreak()
 		curses.echo()
@@ -111,8 +109,8 @@ class UI(DisplayableContainer):
 
 	def destroy(self):
 		"""Destroy all widgets and turn off curses"""
-		DisplayableContainer.destroy(self)
 		self.suspend()
+		DisplayableContainer.destroy(self)
 
 	def handle_mouse(self):
 		"""Handles mouse input"""
@@ -121,10 +119,6 @@ class UI(DisplayableContainer):
 		except _curses.error:
 			return
 
-		# from ranger import log
-		# log('{0:0>28b} ({0})'.format(event.bstate))
-		# log('y: {0}  x: {1}'.format(event.y, event.x))
-
 		DisplayableContainer.click(self, event)
 
 	def handle_key(self, key):
@@ -153,7 +147,7 @@ class UI(DisplayableContainer):
 
 		if cmd.function:
 			try:
-				cmd.function(CommandArgs.from_widget(self))
+				cmd.function(CommandArgs.from_widget(self.fm))
 			except Exception as error:
 				self.fm.notify(error)
 			if kbuf.done:
@@ -197,7 +191,7 @@ class UI(DisplayableContainer):
 		self.env.termsize = self.win.getmaxyx()
 
 	def draw(self):
-		"""Erase the window, then draw all objects in the container"""
+		"""Draw all objects in the container"""
 		self.win.touchwin()
 		DisplayableContainer.draw(self)
 		if self._draw_title and self.settings.update_title:
diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py
index 238a4803..0d46ee06 100644
--- a/ranger/gui/widgets/browsercolumn.py
+++ b/ranger/gui/widgets/browsercolumn.py
@@ -15,6 +15,7 @@
 
 """The BrowserColumn widget displays the contents of a directory or file."""
 import re
+import stat
 from time import time
 
 from . import Widget
@@ -41,6 +42,15 @@ PREVIEW_BLACKLIST = re.compile(r"""
 		$
 """, re.VERBOSE | re.IGNORECASE)
 
+PREVIEW_WHITELIST = re.compile(r"""
+		\.(
+			txt | py | c
+		)
+		# ignore filetype-independent suffixes:
+			(\.part|\.bak|~)?
+		$
+""", re.VERBOSE | re.IGNORECASE)
+
 class BrowserColumn(Pager):
 	main_column = False
 	display_infostring = False
@@ -65,6 +75,12 @@ class BrowserColumn(Pager):
 		Widget.__init__(self, win)
 		self.level = level
 
+		self.settings.signal_bind('setopt.display_size_in_main_column',
+				self.request_redraw, weak=True)
+
+	def request_redraw(self):
+		self.need_redraw = True
+
 	def resize(self, y, x, hei, wid):
 		Widget.resize(self, y, x, hei, wid)
 
@@ -86,7 +102,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]
@@ -96,7 +112,7 @@ class BrowserColumn(Pager):
 
 		else:
 			if self.level > 0:
-				self.fm.move_right()
+				self.fm.move(right=0)
 
 		return True
 
@@ -124,6 +140,9 @@ class BrowserColumn(Pager):
 			self.need_redraw = True
 			self.old_dir = self.target
 
+		if self.target:  # don't garbage collect this directory please
+			self.target.use()
+
 		if self.target and self.target.is_directory \
 				and (self.level <= 0 or self.settings.preview_directories):
 			if self.target.pointed_obj != self.old_cf:
@@ -149,10 +168,24 @@ class BrowserColumn(Pager):
 			self.last_redraw_time = time()
 
 	def _preview_this_file(self, target):
+		if not (target \
+				and self.settings.preview_files \
+				and target.is_file \
+				and target.accessible \
+				and target.stat \
+				and not target.stat.st_mode & stat.S_IFIFO):
+			return False
+
 		maxsize = self.settings.max_filesize_for_preview
-		return self.settings.preview_files \
-				and not PREVIEW_BLACKLIST.search(target.basename) \
-				and (maxsize is None or maxsize >= target.size)
+		if maxsize is not None and target.size > maxsize:
+			return False
+		if PREVIEW_WHITELIST.search(target.basename):
+			return True
+		if PREVIEW_BLACKLIST.search(target.basename):
+			return False
+		if target.is_binary():
+			return False
+		return True
 
 	def _draw_file(self):
 		"""Draw a preview of the file, if the settings allow it"""
@@ -183,8 +216,6 @@ class BrowserColumn(Pager):
 
 		base_color = ['in_browser']
 
-		self.target.use()
-
 		self.win.move(0, 0)
 
 		if not self.target.content_loaded:
@@ -215,18 +246,18 @@ class BrowserColumn(Pager):
 			i = line + self.scroll_begin
 
 			try:
-				drawed = self.target.files[i]
+				drawn = self.target.files[i]
 			except IndexError:
 				break
 
-			this_color = base_color + list(drawed.mimetype_tuple)
-			text = drawed.basename
-			tagged = self.fm.tags and drawed.realpath in self.fm.tags
+			this_color = base_color + list(drawn.mimetype_tuple)
+			text = drawn.basename
+			tagged = self.fm.tags and drawn.realpath in self.fm.tags
 
 			if i == selected_i:
 				this_color.append('selected')
 
-			if drawed.marked:
+			if drawn.marked:
 				this_color.append('marked')
 				if self.main_column:
 					text = " " + text
@@ -236,19 +267,25 @@ class BrowserColumn(Pager):
 				if self.main_column:
 					text = self.tagged_marker + text
 
-			if drawed.is_directory:
+			if drawn.is_directory:
 				this_color.append('directory')
 			else:
 				this_color.append('file')
 
-			if drawed.stat is not None and drawed.stat.st_mode & stat.S_IXUSR:
-				this_color.append('executable')
+			if drawn.stat:
+				mode = drawn.stat.st_mode
+				if mode & stat.S_IXUSR:
+					this_color.append('executable')
+				if stat.S_ISFIFO(mode):
+					this_color.append('fifo')
+				if stat.S_ISSOCK(mode):
+					this_color.append('socket')
 
-			if drawed.islink:
+			if drawn.islink:
 				this_color.append('link')
-				this_color.append(drawed.exists and 'good' or 'bad')
+				this_color.append(drawn.exists and 'good' or 'bad')
 
-			string = drawed.basename
+			string = drawn.basename
 			try:
 				if self.main_column:
 					if tagged:
@@ -258,8 +295,9 @@ class BrowserColumn(Pager):
 				else:
 					self.win.addnstr(line, 0, text, self.wid)
 
-				if self.display_infostring and drawed.infostring:
-					info = drawed.infostring
+				if self.display_infostring and drawn.infostring \
+						and self.settings.display_size_in_main_column:
+					info = drawn.infostring
 					x = self.wid - 1 - len(info)
 					if x > self.x:
 						self.win.addstr(line, x, str(info) + ' ')
@@ -320,19 +358,11 @@ class BrowserColumn(Pager):
 		self.scroll_begin = self._get_scroll_begin()
 		self.target.scroll_begin = self.scroll_begin
 
-	# TODO: does not work if options.scroll_offset is high,
-	# relative > 1 and you scroll from scroll_begin = 1 to 0
 	def scroll(self, relative):
 		"""scroll by n lines"""
 		self.need_redraw = True
-		self._set_scroll_begin()
-		old_value = self.target.scroll_begin
-		self.target.scroll_begin += relative
-		self._set_scroll_begin()
-
-		if self.target.scroll_begin == old_value:
-			self.target.move(relative = relative)
-			self.target.scroll_begin += relative
+		self.target.move(relative=relative)
+		self.target.scroll_begin += 3 * relative
 
 	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 8d6dc611..1995b714 100644
--- a/ranger/gui/widgets/browserview.py
+++ b/ranger/gui/widgets/browserview.py
@@ -15,6 +15,7 @@
 
 """The BrowserView manages a set of BrowserColumns."""
 import curses
+from ranger.ext.signal_dispatcher import Signal
 from . import Widget
 from .browsercolumn import BrowserColumn
 from .pager import Pager
@@ -30,13 +31,31 @@ class BrowserView(Widget, DisplayableContainer):
 
 	def __init__(self, win, ratios, preview = True):
 		DisplayableContainer.__init__(self, win)
-		self.ratios = ratios
 		self.preview = preview
-		self.old_cf = self.env.cf
-		self.old_prevfile = None
-		self.old_prevdir = None
+		self.columns = []
+
+		self.pager = Pager(self.win, embedded=True)
+		self.pager.visible = False
+		self.add_child(self.pager)
+
+		self.change_ratios(ratios, resize=False)
+
+		for option in ('preview_directories', 'preview_files'):
+			self.settings.signal_bind('setopt.' + option,
+					self._request_clear_if_has_borders, weak=True)
+
+		self.fm.env.signal_bind('move', self.request_clear)
+		self.settings.signal_bind('setopt.column_ratios', self.request_clear)
+
+	def change_ratios(self, ratios, resize=True):
+		if isinstance(ratios, Signal):
+			ratios = ratios.value
+
+		for column in self.columns:
+			column.destroy()
+			self.remove_child(column)
+		self.columns = []
 
-		# normalize ratios:
 		ratio_sum = float(sum(ratios))
 		self.ratios = tuple(x / ratio_sum for x in ratios)
 
@@ -46,42 +65,38 @@ class BrowserView(Widget, DisplayableContainer):
 					(self.ratios[-1] * 0.1))
 
 		offset = 1 - len(ratios)
-		if preview: offset += 1
+		if self.preview: offset += 1
 
 		for level in range(len(ratios)):
 			fl = BrowserColumn(self.win, level + offset)
 			self.add_child(fl)
+			self.columns.append(fl)
 
 		try:
-			self.main_column = self.container[preview and -2 or -1]
+			self.main_column = self.columns[self.preview and -2 or -1]
 		except IndexError:
 			self.main_column = None
 		else:
 			self.main_column.display_infostring = True
 			self.main_column.main_column = True
 
-		self.pager = Pager(self.win, embedded=True)
-		self.pager.visible = False
-		self.add_child(self.pager)
+		self.resize(self.y, self.x, self.hei, self.wid)
+
+	def _request_clear_if_has_borders(self):
+		if self.settings.draw_borders:
+			self.request_clear()
+
+	def request_clear(self):
+		self.need_clear = True
 
 	def draw(self):
 		if self.draw_bookmarks:
 			self._draw_bookmarks()
 		else:
-			if self.old_cf != self.env.cf:
-				self.need_clear = True
-			if self.settings.draw_borders:
-				if self.old_prevdir != self.settings.preview_directories:
-					self.need_clear = True
-				if self.old_prevfile != self.settings.preview_files:
-					self.need_clear = True
 			if self.need_clear:
 				self.win.erase()
 				self.need_redraw = True
 				self.need_clear = False
-				self.old_cf = self.env.cf
-				self.old_prevfile = self.settings.preview_files
-				self.old_prevdir = self.settings.preview_directories
 			DisplayableContainer.draw(self)
 			if self.settings.draw_borders:
 				self._draw_borders()
@@ -102,13 +117,14 @@ class BrowserView(Widget, DisplayableContainer):
 				pass
 
 	def _draw_bookmarks(self):
+		self.color_reset()
 		self.need_clear = True
 
 		sorted_bookmarks = sorted(item for item in self.fm.bookmarks \
 				if '/.' not in item[1].path)
 
 		def generator():
-			return zip(range(self.hei), sorted_bookmarks)
+			return zip(range(self.hei-1), sorted_bookmarks)
 
 		try:
 			maxlen = max(len(item[1].path) for i, item in generator())
@@ -116,10 +132,19 @@ class BrowserView(Widget, DisplayableContainer):
 			return
 		maxlen = min(maxlen + 5, self.wid)
 
+		whitespace = " " * maxlen
 		for line, items in generator():
 			key, mark = items
 			string = " " + key + ": " + mark.path
-			self.addnstr(line, 0, string.ljust(maxlen), self.wid)
+			self.addstr(line, 0, whitespace)
+			self.addnstr(line, 0, string, self.wid)
+
+		if self.settings.draw_bookmark_borders:
+			self.win.hline(line+1, 0, curses.ACS_HLINE, maxlen)
+
+			if maxlen < self.wid:
+				self.win.vline(0, maxlen, curses.ACS_VLINE, line+1)
+				self.win.addch(line+1, maxlen, curses.ACS_LRCORNER)
 
 	def _draw_borders(self):
 		win = self.win
@@ -128,17 +153,13 @@ class BrowserView(Widget, DisplayableContainer):
 		left_start = 0
 		right_end = self.wid - 1
 
-		rows = [row for row in self.container \
-				if isinstance(row, BrowserColumn)]
-		rows.sort(key=lambda row: row.x)
-
-		for child in rows:
+		for child in self.columns:
 			if not child.has_preview():
 				left_start = child.x + child.wid
 			else:
 				break
 		if not self.pager.visible:
-			for child in reversed(rows):
+			for child in reversed(self.columns):
 				if not child.has_preview():
 					right_end = child.x - 1
 				else:
@@ -151,7 +172,7 @@ class BrowserView(Widget, DisplayableContainer):
 				right_end - left_start)
 		win.vline(1, left_start, curses.ACS_VLINE, self.hei - 2)
 
-		for child in rows:
+		for child in self.columns:
 			if not child.has_preview():
 				continue
 			if child.main_column and self.pager.visible:
@@ -203,7 +224,7 @@ class BrowserView(Widget, DisplayableContainer):
 						max(1, self.wid - left - pad))
 
 			try:
-				self.container[i].resize(pad, left, hei - pad * 2, \
+				self.columns[i].resize(pad, left, hei - pad * 2, \
 						max(1, wid - 1))
 			except KeyError:
 				pass
@@ -211,11 +232,10 @@ class BrowserView(Widget, DisplayableContainer):
 			left += wid
 
 	def click(self, event):
-		n = event.ctrl() and 1 or 3
-		if event.pressed(4):
-			self.main_column.scroll(relative = -n)
-		elif event.pressed(2) or event.key_invalid():
-			self.main_column.scroll(relative = n)
+		n = event.ctrl() and 5 or 1
+		direction = event.mouse_wheel_direction()
+		if direction:
+			self.main_column.scroll(direction)
 		else:
 			DisplayableContainer.click(self, event)
 
@@ -225,8 +245,8 @@ class BrowserView(Widget, DisplayableContainer):
 		self.need_clear = True
 		self.pager.open()
 		try:
-			self.container[-2].visible = False
-			self.container[-3].visible = False
+			self.columns[-1].visible = False
+			self.columns[-2].visible = False
 		except IndexError:
 			pass
 
@@ -236,15 +256,15 @@ class BrowserView(Widget, DisplayableContainer):
 		self.need_clear = True
 		self.pager.close()
 		try:
-			self.container[-2].visible = True
-			self.container[-3].visible = True
+			self.columns[-1].visible = True
+			self.columns[-2].visible = True
 		except IndexError:
 			pass
 
 	def poke(self):
 		DisplayableContainer.poke(self)
 		if self.settings.collapse_preview and self.preview:
-			has_preview = self.container[-2].has_preview()
+			has_preview = self.columns[-2].has_preview()
 			if self.preview_available != has_preview:
 				self.preview_available = has_preview
 				self.resize(self.y, self.x, self.hei, self.wid)
diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py
index 4dea98c7..aaa85d8e 100644
--- a/ranger/gui/widgets/console.py
+++ b/ranger/gui/widgets/console.py
@@ -25,8 +25,10 @@ from collections import deque
 from . import Widget
 from ranger.defaults import commands
 from ranger.gui.widgets.console_mode import is_valid_mode, mode_to_class
-from ranger import log
+from ranger import log, relpath_conf
 from ranger.ext.shell_escape import shell_quote
+from ranger.ext.direction import Direction
+import ranger
 
 DEFAULT_HISTORY = 0
 SEARCH_HISTORY = 1
@@ -52,17 +54,38 @@ class Console(Widget):
 	histories = None
 	override = None
 	allow_close = False
+	historypaths = []
 
 	def __init__(self, win):
 		from ranger.container import History
 		Widget.__init__(self, win)
 		self.keymap = self.settings.keys.console_keys
 		self.clear()
-		self.histories = [None] * 4
-		self.histories[DEFAULT_HISTORY] = History()
-		self.histories[SEARCH_HISTORY] = History()
-		self.histories[QUICKOPEN_HISTORY] = History()
-		self.histories[OPEN_HISTORY] = History()
+		self.histories = []
+		# load histories from files
+		if not ranger.arg.clean:
+			self.historypaths = [relpath_conf(x) for x in \
+				('history', 'history_search', 'history_qopen', 'history_open')]
+			for i, path in enumerate(self.historypaths):
+				hist = History(self.settings.max_history_size)
+				self.histories.append(hist)
+				if ranger.arg.clean: continue
+				try: f = open(path, 'r')
+				except: continue
+				for line in f:
+					hist.add(line[:-1])
+				f.close()
+
+	def destroy(self):
+		# save histories from files
+		if ranger.arg.clean or not self.settings.save_console_history:
+			return
+		for i, path in enumerate(self.historypaths):
+			try: f = open(path, 'w')
+			except: continue
+			for entry in self.histories[i]:
+				f.write(entry + '\n')
+			f.close()
 
 	def init(self):
 		"""override this. Called directly after class change"""
@@ -73,12 +96,16 @@ class Console(Widget):
 
 		self.win.erase()
 		self.addstr(0, 0, self.prompt)
-		self.addstr(self.line)
+		overflow = -self.wid + len(self.prompt) + len(self.line) + 1
+		if overflow > 0: 
+			self.addstr(self.line[overflow:])
+		else:
+			self.addstr(self.line)
 
 	def finalize(self):
 		try:
 			self.fm.ui.win.move(self.y,
-					self.x + self.pos + len(self.prompt))
+					self.x + min(self.wid-1, self.pos + len(self.prompt)))
 		except:
 			pass
 
@@ -135,9 +162,15 @@ class Console(Widget):
 		except KeyError:
 			# An unclean hack to allow unicode input.
 			# This whole part should be replaced.
-			self.type_key(chr(keytuple[0]))
-			self.env.key_clear()
-			return
+			try:
+				chrkey = chr(keytuple[0])
+			except:
+				pass
+			else:
+				self.type_key(chrkey)
+			finally:
+				self.env.key_clear()
+				return
 
 		if cmd == self.commandlist.dummy_object:
 			return
@@ -181,14 +214,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
@@ -227,7 +262,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):
@@ -388,6 +423,8 @@ class OpenConsole(ConsoleWithTab):
 
 	def execute(self):
 		command, flags = self._parse()
+		if not command and 'p' in flags:
+			command = 'cat %f'
 		if command:
 			if _CustomTemplate.delimiter in command:
 				command = self._substitute_metachars(command)
diff --git a/ranger/gui/widgets/pager.py b/ranger/gui/widgets/pager.py
index c5ed8af1..e915f790 100644
--- a/ranger/gui/widgets/pager.py
+++ b/ranger/gui/widgets/pager.py
@@ -18,8 +18,7 @@ The pager displays text and allows you to scroll inside it.
 """
 import re
 from . import Widget
-from ranger.ext.move import move_between
-from ranger import log
+from ranger.ext.direction import Direction
 
 BAR_REGEXP = re.compile(r'\|\d+\?\|')
 QUOTES_REGEXP = re.compile(r'"[^"]+?"')
@@ -46,6 +45,10 @@ class Pager(Widget):
 		else:
 			self.keymap = self.settings.keys.pager_keys
 
+	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
@@ -112,50 +115,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:
@@ -201,13 +180,13 @@ class Pager(Widget):
 
 	def click(self, event):
 		n = event.ctrl() and 1 or 3
-		if event.pressed(4):
-			self.move(relative = -n)
-		elif event.pressed(2) or event.key_invalid():
-			self.move(relative = n)
+		direction = event.mouse_wheel_direction()
+		if direction:
+			self.move(relative=direction)
 		return True
 
 	def _get_line(self, n, attempt_to_read=True):
+		assert isinstance(n, int), n
 		try:
 			return self.lines[n]
 		except (KeyError, IndexError):
@@ -234,3 +213,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 6f52f8ef..75fbbe89 100644
--- a/ranger/gui/widgets/statusbar.py
+++ b/ranger/gui/widgets/statusbar.py
@@ -47,6 +47,11 @@ class StatusBar(Widget):
 	def __init__(self, win, column=None):
 		Widget.__init__(self, win)
 		self.column = column
+		self.settings.signal_bind('setopt.display_size_in_status_bar',
+				self.request_redraw, weak=True)
+	
+	def request_redraw(self):
+		self.need_redraw = True
 
 	def notify(self, text, duration=4, bad=False):
 		self.msg = Message(text, duration, bad)
@@ -157,12 +162,16 @@ class StatusBar(Widget):
 		left.add(self._get_owner(target), 'owner')
 		left.add_space()
 		left.add(self._get_group(target), 'group')
-		left.add_space()
 
 		if target.islink:
 			how = target.exists and 'good' or 'bad'
-			left.add('-> ' + target.readlink, 'link', how)
+			left.add(' -> ' + target.readlink, 'link', how)
 		else:
+			if self.settings.display_size_in_status_bar and target.infostring:
+				left.add(target.infostring)
+
+			left.add_space()
+
 			left.add(strftime(self.timeformat,
 					localtime(target.stat.st_mtime)), 'mtime')
 
diff --git a/ranger/gui/widgets/titlebar.py b/ranger/gui/widgets/titlebar.py
index e1be8e97..62740e2d 100644
--- a/ranger/gui/widgets/titlebar.py
+++ b/ranger/gui/widgets/titlebar.py
@@ -30,18 +30,65 @@ class TitleBar(Widget):
 	old_wid = None
 	result = None
 	throbber = ' '
+	need_redraw = False
+	tab_width = 0
+
+	def __init__(self, *args, **keywords):
+		Widget.__init__(self, *args, **keywords)
+		self.fm.signal_bind('tab.change', self.request_redraw, weak=True)
+
+	def request_redraw(self):
+		self.need_redraw = True
 
 	def draw(self):
-		if self.env.cf != self.old_cf or\
+		if self.need_redraw or \
+				self.env.cf != self.old_cf or\
 				str(self.env.keybuffer) != str(self.old_keybuffer) or\
 				self.wid != self.old_wid:
+			self.need_redraw = False
 			self.old_wid = self.wid
 			self.old_cf = self.env.cf
 			self._calc_bar()
 		self._print_result(self.result)
 		if self.wid > 2:
 			self.color('in_titlebar', 'throbber')
-			self.win.addnstr(self.y, self.wid - 2, self.throbber, 1)
+			self.win.addnstr(self.y, self.wid - 2 - self.tab_width,
+					self.throbber, 1)
+
+	def click(self, event):
+		"""Handle a MouseEvent"""
+		direction = event.mouse_wheel_direction()
+		if direction:
+			self.fm.tab_move(direction)
+			self.need_redraw = True
+			return True
+
+		if not event.pressed(1) or not self.result:
+			return False
+
+		pos = self.wid - 1
+		for tabname in reversed(self.fm._get_tab_list()):
+			pos -= len(str(tabname)) + 1
+			if event.x > pos:
+				self.fm.tab_open(tabname)
+				self.need_redraw = True
+				return True
+
+		pos = 0
+		for i, part in enumerate(self.result):
+			pos += len(part.string)
+			if event.x < pos:
+				if i < 2:
+					self.fm.enter_dir("~")
+				elif i == 2:
+					self.fm.enter_dir("/")
+				else:
+					try:
+						self.fm.env.enter_dir(self.env.pathway[(i-3)/2])
+					except:
+						pass
+				return True
+		return False
 
 	def _calc_bar(self):
 		bar = Bar('in_titlebar')
@@ -80,6 +127,12 @@ class TitleBar(Widget):
 		self.old_keybuffer = kb
 		bar.addright(kb, 'keybuffer', fixedsize=True)
 		bar.addright('  ', 'space', fixedsize=True)
+		self.tab_width = 0
+		if len(self.fm.tabs) > 1:
+			for tabname in self.fm._get_tab_list():
+				self.tab_width += len(str(tabname)) + 1
+				clr = 'good' if tabname == self.fm.current_tab else 'bad'
+				bar.addright(' '+str(tabname), 'tab', clr, fixedsize=True)
 
 	def _print_result(self, result):
 		import _curses
diff --git a/ranger/help/movement.py b/ranger/help/movement.py
index a0407838..4ea2b0c3 100644
--- a/ranger/help/movement.py
+++ b/ranger/help/movement.py
@@ -21,7 +21,8 @@
 1.3. Searching
 1.4. Cycling
 1.5. Bookmarks
-1.6. Mouse usage
+1.6. Tabs
+1.7. Mouse usage
 
 
 ==============================================================================
@@ -72,7 +73,7 @@ These keys work like in vim:
 	^R	clear the cache and reload the view
 	^L	redraw the window
 	:	open the console |3?|
-	b	toggle options
+	z	toggle options
 
 	i	inspect the content of the file
 	E	edit the file
@@ -102,7 +103,7 @@ visible files. Pressing "n" will move you to the next occurance,
 "N" to the previous one.
 
 You can search for more than just strings:
-	TAB	search tagged files
+	ct	search tagged files
 	cc	cycle through all files by their ctime (last modification)
 	cm	cycle by mime type, connecting similar files
 	cs	cycle by size, large items first
@@ -134,7 +135,21 @@ Note: The ' key is equivalent to `.
 
 
 ==============================================================================
-1.6. Mouse usage
+1.6. Tabs
+
+Tabs are used to work in different directories in the same Ranger instance.
+In Ranger, tabs are very simple though and only store the directory path.
+
+	gt	Go to the next tab. (also TAB)
+	gT	Go to the previous tab. (also Shift+TAB)
+	gn, ^N	Create a new tab
+	g<N>	Open a tab. N has to be a number from 0 to 9.
+		If the tab doesn't exist yet, it will be created.
+	gc, ^W	Close the current tab.  The last tab cannot be closed.
+
+
+==============================================================================
+1.7. Mouse usage
 
 The mouse can be used to quickly enter directories which you point at,
 or to scroll around with the mouse wheel. The implementation of the mouse
diff --git a/ranger/shared/settings.py b/ranger/shared/settings.py
index cdddd623..a4a58e6e 100644
--- a/ranger/shared/settings.py
+++ b/ranger/shared/settings.py
@@ -13,22 +13,25 @@
 # 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 os
-import types
-from inspect import isclass, ismodule
 import ranger
+from ranger.ext.signal_dispatcher import SignalDispatcher
 from ranger.ext.openstruct import OpenStruct
-from ranger.gui.colorscheme import ColorScheme
 
 ALLOWED_SETTINGS = {
 	'show_hidden': bool,
 	'show_cursor': bool,
 	'autosave_bookmarks': bool,
+	'save_console_history': bool,
 	'collapse_preview': bool,
+	'column_ratios': (tuple, list, set),
+	'display_size_in_main_column': bool,
+	'display_size_in_status_bar': bool,
 	'draw_borders': bool,
+	'draw_bookmark_borders': bool,
 	'sort': str,
-	'reverse': bool,
-	'directories_first': bool,
+	'sort_reverse': bool,
+	'sort_case_insensitive': bool,
+	'sort_directories_first': bool,
 	'update_title': bool,
 	'shorten_title': int,  # Note: False is an instance of int
 	'max_filesize_for_preview': (int, type(None)),
@@ -38,26 +41,88 @@ ALLOWED_SETTINGS = {
 	'preview_directories': bool,
 	'flushinput': bool,
 	'colorscheme': str,
+	'colorscheme_overlay': (type(None), type(lambda:0)),
 	'hidden_filter': lambda x: isinstance(x, str) or hasattr(x, 'match'),
 }
 
+
+COMPAT_MAP = {
+	'sort_reverse': 'reverse',
+	'sort_directories_first': 'directories_first',
+}
+
+
+class SettingObject(SignalDispatcher):
+	def __init__(self):
+		SignalDispatcher.__init__(self)
+		self.__dict__['_settings'] = dict()
+		self.__dict__['_setting_sources'] = list()
+
+	def __setattr__(self, name, value):
+		if name[0] == '_':
+			self.__dict__[name] = value
+		else:
+			assert name in self._settings, "No such setting: {0}!".format(name)
+			assert self._check_type(name, value)
+			kws = dict(setting=name, value=value,
+					previous=self._settings[name])
+			self.signal_bind('setopt.'+name,
+					self._raw_set_with_signal, priority=0.2)
+			self.signal_emit('setopt', **kws)
+			self.signal_emit('setopt.'+name, **kws)
+
+	def __getattr__(self, name):
+		assert name in ALLOWED_SETTINGS or name in self._settings, \
+				"No such setting: {0}!".format(name)
+		try:
+			return self._settings[name]
+		except:
+			for struct in self._setting_sources:
+				try: value = getattr(struct, name)
+				except: pass
+				else: break
+			else:
+				raise Exception("The option `{0}' was not defined" \
+						" in the defaults!".format(name))
+			assert self._check_type(name, value)
+			self._raw_set(name, value)
+			self.__setattr__(name, value)
+			return self._settings[name]
+
+	def _check_type(self, name, value):
+		from inspect import isfunction
+		typ = ALLOWED_SETTINGS[name]
+		if isfunction(typ):
+			assert typ(value), \
+				"The option `" + name + "' has an incorrect type!"
+		else:
+			assert isinstance(value, typ), \
+				"The option `" + name + "' has an incorrect type!"\
+				" Got " + str(type(value)) + ", expected " + str(typ) + "!"
+		return True
+
+	__getitem__ = __getattr__
+	__setitem__ = __setattr__
+
+	def _raw_set(self, name, value):
+		self._settings[name] = value
+
+	def _raw_set_with_signal(self, signal):
+		self._settings[signal.setting] = signal.value
+
+
 # -- globalize the settings --
 class SettingsAware(object):
 	settings = OpenStruct()
 
 	@staticmethod
 	def _setup():
-		settings = OpenStruct()
+		settings = SettingObject()
 
-		from ranger.defaults import options
-		for setting in ALLOWED_SETTINGS:
-			try:
-				settings[setting] = getattr(options, setting)
-			except AttributeError:
-				raise Exception("The option `{0}' was not defined" \
-						" in the defaults!".format(setting))
+		from ranger.gui.colorscheme import _colorscheme_name_to_class
+		settings.signal_bind('setopt.colorscheme',
+				_colorscheme_name_to_class, priority=1)
 
-		import sys
 		if not ranger.arg.clean:
 			# overwrite single default options with custom options
 			try:
@@ -65,83 +130,34 @@ class SettingsAware(object):
 			except ImportError:
 				pass
 			else:
-				for setting in ALLOWED_SETTINGS:
+				settings._setting_sources.append(my_options)
+
+				# For backward compatibility:
+				for new, old in COMPAT_MAP.items():
 					try:
-						settings[setting] = getattr(my_options, setting)
+						setattr(my_options, new, getattr(my_options, old))
+						print("Warning: the option `{0}'"\
+								" was renamed to `{1}'\nPlease update"\
+								" your configuration file soon." \
+								.format(old, new))
 					except AttributeError:
 						pass
 
-		assert check_option_types(settings)
-
-		# Find the colorscheme.  First look for it at ~/.ranger/colorschemes,
-		# then at RANGERDIR/colorschemes.  If the file contains a class
-		# named Scheme, it is used.  Otherwise, an arbitrary other class
-		# is picked.
-
-		scheme_name = settings.colorscheme
-
-		def exists(colorscheme):
-			return os.path.exists(colorscheme + '.py')
-
-		def is_scheme(x):
-			return isclass(x) and issubclass(x, ColorScheme)
-
-		# create ~/.ranger/colorschemes/__init__.py if it doesn't exist
-		if os.path.exists(ranger.relpath_conf('colorschemes')):
-			initpy = ranger.relpath_conf('colorschemes', '__init__.py')
-			if not os.path.exists(initpy):
-				open(initpy, 'a').close()
-
-		if exists(ranger.relpath_conf('colorschemes', scheme_name)):
-			scheme_supermodule = 'colorschemes'
-		elif exists(ranger.relpath('colorschemes', scheme_name)):
-			scheme_supermodule = 'ranger.colorschemes'
-		else:
-			scheme_supermodule = None  # found no matching file.
-
-		if scheme_supermodule is None:
-			print("ERROR: colorscheme not found, fall back to builtin scheme")
-			if ranger.arg.debug:
-				raise Exception("Cannot locate colorscheme!")
-			settings.colorscheme = ColorScheme()
-		else:
-			scheme_module = getattr(__import__(scheme_supermodule,
-					globals(), locals(), [scheme_name], 0), scheme_name)
-			assert ismodule(scheme_module)
-			if hasattr(scheme_module, 'Scheme') \
-					and is_scheme(scheme_module.Scheme):
-				settings.colorscheme = scheme_module.Scheme()
-			else:
-				for name, var in scheme_module.__dict__.items():
-					if var != ColorScheme and is_scheme(var):
-						settings.colorscheme = var()
-						break
-				else:
-					raise Exception("The module contains no " \
-							"valid colorscheme!")
+		from ranger.defaults import options as default_options
+		settings._setting_sources.append(default_options)
+		assert all(hasattr(default_options, setting) \
+				for setting in ALLOWED_SETTINGS), \
+				"Ensure that all options are defined in the defaults!"
 
 		try:
 			import apps
 		except ImportError:
 			from ranger.defaults import apps
-		settings.apps = apps
+		settings._raw_set('apps', apps)
 		try:
 			import keys
 		except ImportError:
 			from ranger.defaults import keys
-		settings.keys = keys
+		settings._raw_set('keys', keys)
 
 		SettingsAware.settings = settings
-
-def check_option_types(opt):
-	import inspect
-	for name, typ in ALLOWED_SETTINGS.items():
-		optvalue = getattr(opt, name)
-		if inspect.isfunction(typ):
-			assert typ(optvalue), \
-				"The option `" + name + "' has an incorrect type!"
-		else:
-			assert isinstance(optvalue, typ), \
-				"The option `" + name + "' has an incorrect type!"\
-				" Got " + str(type(optvalue)) + ", expected " + str(typ) + "!"
-	return True