summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--Makefile16
-rw-r--r--README6
-rw-r--r--doc/HACKING6
-rw-r--r--doc/ranger.16
-rw-r--r--doc/ranger.pod6
-rw-r--r--examples/README2
-rw-r--r--examples/bash_automatic_cd.sh19
-rw-r--r--examples/bash_subshell_notice.sh5
-rwxr-xr-xexamples/rifle_sxiv.sh26
-rw-r--r--examples/vim_file_chooser.vim15
-rwxr-xr-xranger.py14
-rw-r--r--ranger/__init__.py41
-rw-r--r--ranger/api/apps.py180
-rw-r--r--ranger/api/commands.py36
-rw-r--r--ranger/api/options.py15
-rw-r--r--ranger/colorschemes/default.py20
-rw-r--r--ranger/colorschemes/default88.py14
-rw-r--r--ranger/colorschemes/jungle.py14
-rw-r--r--ranger/colorschemes/snow.py14
-rw-r--r--ranger/config/__init__.py (renamed from ranger/defaults/__init__.py)0
-rw-r--r--ranger/config/commands.py (renamed from ranger/defaults/commands.py)86
-rw-r--r--ranger/config/options.py (renamed from ranger/defaults/options.py)5
-rw-r--r--ranger/config/rc.conf (renamed from ranger/defaults/rc.conf)26
-rw-r--r--ranger/config/rifle.conf195
-rw-r--r--ranger/container/bookmarks.py14
-rw-r--r--ranger/container/history.py24
-rw-r--r--ranger/container/settingobject.py14
-rw-r--r--ranger/container/tags.py14
-rw-r--r--ranger/core/actions.py334
-rw-r--r--ranger/core/environment.py300
-rw-r--r--ranger/core/fm.py153
-rw-r--r--ranger/core/helper.py211
-rw-r--r--ranger/core/loader.py54
-rw-r--r--ranger/core/main.py191
-rw-r--r--ranger/core/runner.py31
-rw-r--r--ranger/core/shared.py16
-rw-r--r--ranger/core/tab.py159
l---------ranger/data/ranger1
-rw-r--r--ranger/defaults/apps.py325
-rw-r--r--ranger/ext/accumulator.py14
-rw-r--r--ranger/ext/cached_function.py14
-rw-r--r--ranger/ext/curses_interrupt_handler.py14
-rw-r--r--ranger/ext/direction.py14
-rw-r--r--ranger/ext/get_executables.py20
-rw-r--r--ranger/ext/human_readable.py14
-rw-r--r--ranger/ext/iter_tools.py14
-rw-r--r--ranger/ext/keybinding_parser.py14
-rw-r--r--ranger/ext/mount_path.py14
-rw-r--r--ranger/ext/next_available_filename.py14
-rw-r--r--ranger/ext/openstruct.py14
-rw-r--r--ranger/ext/relative_symlink.py14
-rwxr-xr-xranger/ext/rifle.py420
-rw-r--r--ranger/ext/shell_escape.py14
-rw-r--r--ranger/ext/signals.py14
-rw-r--r--ranger/ext/spawn.py14
-rw-r--r--ranger/ext/widestring.py14
-rw-r--r--ranger/fsobject/directory.py37
-rw-r--r--ranger/fsobject/file.py14
-rw-r--r--ranger/fsobject/fsobject.py27
-rw-r--r--ranger/gui/ansi.py14
-rw-r--r--ranger/gui/bar.py14
-rw-r--r--ranger/gui/color.py14
-rw-r--r--ranger/gui/colorscheme.py16
-rw-r--r--ranger/gui/context.py16
-rw-r--r--ranger/gui/curses_shortcuts.py14
-rw-r--r--ranger/gui/displayable.py18
-rw-r--r--ranger/gui/mouse_event.py14
-rw-r--r--ranger/gui/ui.py44
-rw-r--r--ranger/gui/widgets/browsercolumn.py125
-rw-r--r--ranger/gui/widgets/browserview.py40
-rw-r--r--ranger/gui/widgets/console.py22
-rw-r--r--ranger/gui/widgets/pager.py16
-rw-r--r--ranger/gui/widgets/statusbar.py43
-rw-r--r--ranger/gui/widgets/taskview.py26
-rw-r--r--ranger/gui/widgets/titlebar.py42
l---------scripts/ranger1
l---------scripts/rifle1
-rwxr-xr-xsetup.py21
78 files changed, 1813 insertions, 2024 deletions
diff --git a/Makefile b/Makefile
index f13de38d..20cf6f25 100644
--- a/Makefile
+++ b/Makefile
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 NAME = ranger
 VERSION = $(shell grep -m 1 -o '[0-9][0-9.]\+' README)
@@ -72,7 +60,7 @@ doc: cleandoc
 test:
 	@for FILE in $(shell grep -IHm 1 doctest -r ranger | cut -d: -f1); do \
 		echo "Testing $$FILE..."; \
-		PYTHONPATH=".:"$$PYTHONPATH ${PYTHON} $$FILE; \
+		RANGER_DOCTEST=1 PYTHONPATH=".:"$$PYTHONPATH ${PYTHON} $$FILE; \
 	done
 
 man:
diff --git a/README b/README
index d1f9f513..0b0cf9a7 100644
--- a/README
+++ b/README
@@ -7,8 +7,8 @@ open your files with.
 
 This file describes ranger and how to get it to run.  For instructions on the
 usage, please read the man page.  See doc/HACKING for development specific
-information.  For configuration, check the files in ranger/defaults/.  They
-are usually installed to /usr/lib/python*/site-packages/ranger/defaults/
+information.  For configuration, check the files in ranger/config/.  They
+are usually installed to /usr/lib/python*/site-packages/ranger/config/
 and can be obtained with ranger's --copy-config option.
 
 A note to packagers:  Versions meant for packaging are listed in the changelog
@@ -92,5 +92,5 @@ directory.
 
 Ranger can automatically copy default configuration files to ~/.config/ranger
 if you run it with the switch --copy-config. (see ranger --help for a
-description of that switch.)  Also check ranger/defaults/ for the default
+description of that switch.)  Also check ranger/config/ for the default
 configuration.
diff --git a/doc/HACKING b/doc/HACKING
index f6d5d064..89452398 100644
--- a/doc/HACKING
+++ b/doc/HACKING
@@ -46,7 +46,7 @@ In ranger/fsobject/file.py
 the constant PREVIEW_BLACKLIST
 
 * Adding options:
-In ranger/defaults/options.py
+In ranger/config/options.py
 add the default value, like: my_option = True
 In ranger/container/settingobject.py
 add the name of your option to the constant ALLOWED_SETTINGS
@@ -58,12 +58,12 @@ assuming <self> is a "SettingsAware" object.
 Copy ranger/colorschemes/default.py to ranger/colorschemes/myscheme.py
 and modify it according to your needs.  Alternatively, mimic the jungle
 colorscheme.  It subclasses the default scheme and just modifies a few things.
-In ranger/defaults/options.py (or ~/.config/ranger/options.py), change
+In ranger/config/options.py (or ~/.config/ranger/options.py), change
     colorscheme = 'default'
 to: colorscheme = 'myscheme'
 
 * Change the file type => application associations:
-In ranger/defaults/apps.py
+In ranger/config/apps.py
 modify the method app_default.
 The variable "f" is a filesystem-object with attributes like mimetype,
 extension, etc.  For a full list, check ranger/fsobject/fsobject.py
diff --git a/doc/ranger.1 b/doc/ranger.1
index e5a79160..cfdbd455 100644
--- a/doc/ranger.1
+++ b/doc/ranger.1
@@ -146,8 +146,8 @@ open your files with.
 .PP
 This manual mainly contains information on the usage of ranger.  Refer to the
 \&\fI\s-1README\s0\fR for install instructions and to \fIdoc/HACKING\fR for development
-specific information.  For configuration, see the files in \fIranger/defaults\fR.
-They are usually installed to \fI/usr/lib/python*/site\-packages/ranger/defaults\fR
+specific information.  For configuration, see the files in \fIranger/config\fR.
+They are usually installed to \fI/usr/lib/python*/site\-packages/ranger/config\fR
 and can be obtained with ranger's \-\-copy\-config option.
 .PP
 Inside ranger, you can press \fI1?\fR for a list of key bindings, \fI2?\fR for a list
@@ -321,7 +321,7 @@ to extract an archive.  See the \fIapps.py\fR configuration file for all program
 and modes.
 .SH "KEY BINDINGS"
 .IX Header "KEY BINDINGS"
-Key bindings are defined in the file \fIranger/defaults/rc.conf\fR.  Check this
+Key bindings are defined in the file \fIranger/config/rc.conf\fR.  Check this
 file for a list of all key bindings.  You can copy it to your local
 configuration directory with the \-\-copy\-config=rc option.
 .PP
diff --git a/doc/ranger.pod b/doc/ranger.pod
index 9f8b4f04..2a3a345b 100644
--- a/doc/ranger.pod
+++ b/doc/ranger.pod
@@ -24,8 +24,8 @@ open your files with.
 
 This manual mainly contains information on the usage of ranger.  Refer to the
 F<README> for install instructions and to F<doc/HACKING> for development
-specific information.  For configuration, see the files in F<ranger/defaults>.
-They are usually installed to F</usr/lib/python*/site-packages/ranger/defaults>
+specific information.  For configuration, see the files in F<ranger/config>.
+They are usually installed to F</usr/lib/python*/site-packages/ranger/config>
 and can be obtained with ranger's --copy-config option.
 
 Inside ranger, you can press I<1?> for a list of key bindings, I<2?> for a list
@@ -229,7 +229,7 @@ and modes.
 
 =head1 KEY BINDINGS
 
-Key bindings are defined in the file F<ranger/defaults/rc.conf>.  Check this
+Key bindings are defined in the file F<ranger/config/rc.conf>.  Check this
 file for a list of all key bindings.  You can copy it to your local
 configuration directory with the --copy-config=rc option.
 
diff --git a/examples/README b/examples/README
new file mode 100644
index 00000000..60f29ac6
--- /dev/null
+++ b/examples/README
@@ -0,0 +1,2 @@
+Thes files in this directory contain applications or extensions of ranger which
+are put here for your inspiration and as references.
diff --git a/examples/bash_automatic_cd.sh b/examples/bash_automatic_cd.sh
new file mode 100644
index 00000000..a63280d1
--- /dev/null
+++ b/examples/bash_automatic_cd.sh
@@ -0,0 +1,19 @@
+# Automatically change the directory in bash after closing ranger
+#
+# This is a bash function for .bashrc to automatically change the directory to
+# the last visited one after ranger quits.
+# To undo the effect of this function, you can type "cd -" to return to the
+# original directory.
+
+function ranger-cd {
+    tempfile='/tmp/chosendir'
+    /usr/bin/ranger --choosedir="$tempfile" "${@:-$(pwd)}"
+    test -f "$tempfile" &&
+    if [ "$(cat -- "$tempfile")" != "$(echo -n `pwd`)" ]; then
+        cd -- "$(cat "$tempfile")"
+    fi
+    rm -f -- "$tempfile"
+}
+
+# This binds Ctrl-O to ranger-cd:
+bind '"\C-o":"ranger-cd\C-m"'
diff --git a/examples/bash_subshell_notice.sh b/examples/bash_subshell_notice.sh
new file mode 100644
index 00000000..b6b03d74
--- /dev/null
+++ b/examples/bash_subshell_notice.sh
@@ -0,0 +1,5 @@
+# Change the prompt when you open a shell from inside ranger
+#
+# Add this line to your .bashrc for it to work.
+
+[ -n "$RANGER_LEVEL" ] && PS1="$PS1"'(in ranger) '
diff --git a/examples/rifle_sxiv.sh b/examples/rifle_sxiv.sh
new file mode 100755
index 00000000..efa935b2
--- /dev/null
+++ b/examples/rifle_sxiv.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+#
+# This script searches image files in a directory, opens them all with sxiv
+# and sets the first argument to the first image displayed by sxiv.
+#
+# This is supposed to be used in rifle.conf as a workaround for the fact that
+# sxiv takes no file name arguments for the first image, just the number.
+# Copy this file somewhere into your $PATH and add this at the top of rifle.conf:
+#
+#   mime ^image, has sxiv, X, flag f = path/to/this/script -- "$@"
+#
+
+[ "$1" == '--' ] && shift
+target="$(realpath -s "$1")"
+function listfiles {
+    find -L "$(dirname "$target")" -maxdepth 1 -type f -iregex \
+      '.*\(jpe?g\|bmp\|png\|gif\)$' -print0 | sort -z
+}
+
+count="$(listfiles | grep -m 1 -Zznx "$target" | cut -d: -f1)"
+
+if [ -n "$count" ]; then
+    listfiles | xargs -0 sxiv -n "$count" --
+else
+    sxiv -- "$@" # fallback
+fi
diff --git a/examples/vim_file_chooser.vim b/examples/vim_file_chooser.vim
new file mode 100644
index 00000000..a44ab2b7
--- /dev/null
+++ b/examples/vim_file_chooser.vim
@@ -0,0 +1,15 @@
+" Add ranger as a file chooser in vim
+" 
+" If you add this function and the key binding to the .vimrc, ranger can be
+" started using the keybinding ",r".  Once you select a file by pressing
+" enter, ranger will quit again and vim will open the selected file.
+
+fun! RangerChooser()
+    exec "silent !ranger --choosefile=/tmp/chosenfile " . expand("%:p:h")
+    if filereadable('/tmp/chosenfile')
+        exec 'edit ' . system('cat /tmp/chosenfile')
+        call system('rm /tmp/chosenfile')
+    endif
+    redraw!
+endfun
+map ,r :call RangerChooser()<CR>
diff --git a/ranger.py b/ranger.py
index c763a8d9..efd81f4d 100755
--- a/ranger.py
+++ b/ranger.py
@@ -1,19 +1,7 @@
 #!/usr/bin/python -O
 # ranger - a vim-inspired file manager for the console  (coding: utf-8)
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 # =====================
 # This embedded bash script can be executed by sourcing this file.
diff --git a/ranger/__init__.py b/ranger/__init__.py
index fa6800b3..ea94f53c 100644
--- a/ranger/__init__.py
+++ b/ranger/__init__.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 """
 A console file manager with VI key bindings.
@@ -33,13 +21,38 @@ __email__ = 'romanz@lavabit.com'
 RANGERDIR = os.path.dirname(__file__)
 TICKS_BEFORE_COLLECTING_GARBAGE = 100
 TIME_BEFORE_FILE_BECOMES_GARBAGE = 1200
+MAX_RESTORABLE_TABS = 3
 MACRO_DELIMITER = '%'
+DEFAULT_PAGER = 'less'
 LOGFILE = '/tmp/ranger_errorlog'
 USAGE = '%prog [options] [path/filename]'
-STABLE = True
+VERSION = 'ranger-git based on %s' % __version__
 
 # If the environment variable XDG_CONFIG_HOME is non-empty, CONFDIR is ignored
 # and the configuration directory will be $XDG_CONFIG_HOME/ranger instead.
 CONFDIR = '~/.config/ranger'
 
+# Debugging functions.  These will be activated when run with --debug.
+# Example usage in the code:
+# import ranger; ranger.log("hello world")
+def log(*objects, **keywords):
+	"""
+	Writes objects to a logfile (for the purpose of debugging only.)
+	Has the same arguments as print() in python3.
+	"""
+	from ranger import arg
+	if LOGFILE is None or not arg.debug or arg.clean: return
+	start = 'start' in keywords and keywords['start'] or 'ranger:'
+	sep   =   'sep' in keywords and keywords['sep']   or ' '
+	_file =  'file' in keywords and keywords['file']  or open(LOGFILE, 'a')
+	end   =   'end' in keywords and keywords['end']   or '\n'
+	_file.write(sep.join(map(str, (start, ) + objects)) + end)
+
+
+def log_traceback():
+	from ranger import arg
+	if LOGFILE is None or not arg.debug or arg.clean: return
+	import traceback
+	traceback.print_stack(file=open(LOGFILE, 'a'))
+
 from ranger.core.main import main
diff --git a/ranger/api/apps.py b/ranger/api/apps.py
deleted file mode 100644
index 56f8afd2..00000000
--- a/ranger/api/apps.py
+++ /dev/null
@@ -1,180 +0,0 @@
-# Copyright (C) 2009, 2010, 2011  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 os, sys, re
-from ranger.api import *
-from ranger.ext.iter_tools import flatten
-from ranger.ext.get_executables import get_executables
-from ranger.core.runner import Context
-from ranger.core.shared import FileManagerAware
-
-
-class Applications(FileManagerAware):
-	"""
-	This class contains definitions on how to run programs and should
-	be extended in ranger.apps
-
-	The user can decide what program to run, and if he uses eg. 'vim', the
-	function app_vim() will be called.  However, usually the user
-	simply wants to "start" the file without specific instructions.
-	In such a case, app_default() is called, where you should examine
-	the context and decide which program to use.
-
-	All app functions have a name starting with app_ and return a string
-	containing the whole command or a tuple containing a list of the
-	arguments. They are supplied with one argument, which is the
-	AppContext instance.
-
-	You should define at least app_default, app_pager and app_editor since
-	internal functions depend on those.  Here are sample implementations:
-
-	def app_default(self, context):
-		if context.file.media:
-			if context.file.video:
-				# detach videos from the filemanager
-				context.flags += 'd'
-			return self.app_mplayer(context)
-		else:
-			return self.app_editor(context)
-
-	def app_pager(self, context):
-		return 'less', context
-
-	def app_editor(self, context):
-		return ('vim', context)
-	"""
-
-	def _meets_dependencies(self, fnc):
-		try:
-			deps = fnc.dependencies
-		except AttributeError:
-			return True
-
-		for dep in deps:
-			if dep == 'X':
-				if 'DISPLAY' not in os.environ or not os.environ['DISPLAY']:
-					return False
-				continue
-			if hasattr(dep, 'dependencies') \
-			and not self._meets_dependencies(dep):
-				return False
-			if dep not in get_executables():
-				return False
-
-		return True
-
-	def either(self, context, *args):
-		for app in args:
-			try:
-				application_handler = getattr(self, 'app_' + app)
-			except AttributeError:
-				if app in get_executables():
-					return _generic_app(app, context)
-				continue
-			if self._meets_dependencies(application_handler):
-				return application_handler(context)
-
-	def app_self(self, context):
-		"""Run the file itself"""
-		return "./" + context.file.basename
-
-	def get(self, app):
-		"""Looks for an application, returns app_default if it doesn't exist"""
-		try:
-			return getattr(self, 'app_' + app)
-		except AttributeError:
-			return self.app_default
-
-	def apply(self, app, context):
-		if not app:
-			app = 'default'
-		try:
-			handler = getattr(self, 'app_' + app)
-		except AttributeError:
-			if app in get_executables():
-				return [app] + list(context)
-			handler = self.app_default
-		arguments = handler(context)
-		# flatten
-		if isinstance(arguments, str):
-			return arguments
-		if arguments is None:
-			return None
-		result = []
-		for obj in arguments:
-			if isinstance(obj, (tuple, list, Context)):
-				result.extend(obj)
-			else:
-				result.append(obj)
-		return result
-
-	def has(self, app):
-		"""Returns whether an application is defined"""
-		return hasattr(self, 'app_' + app)
-
-	def all(self):
-		"""Returns a list with all application functions"""
-		result = set()
-		# go through all the classes in the mro (method resolution order)
-		# so subclasses will return the apps of their superclasses.
-		for cls in self.__class__.__mro__:
-			result |= set(m[4:] for m in cls.__dict__ if m.startswith('app_'))
-		return sorted(result)
-
-	@classmethod
-	def generic(cls, *args, **keywords):
-		flags = 'flags' in keywords and keywords['flags'] or ""
-		deps = 'deps' in keywords and keywords['deps'] or ()
-		for name in args:
-			assert isinstance(name, str)
-			if not hasattr(cls, "app_" + name):
-				fnc = _generic_wrapper(name, flags=flags)
-				fnc = depends_on(*deps)(fnc)
-				setattr(cls, "app_" + name, fnc)
-
-
-def tup(*args):
-	"""
-	This helper function creates a tuple out of the arguments.
-
-	('a', ) + tuple(some_iterator)
-	is equivalent to:
-	tup('a', *some_iterator)
-	"""
-	return args
-
-
-def depends_on(*args):
-	args = tuple(flatten(args))
-	def decorator(fnc):
-		try:
-			fnc.dependencies += args
-		except:
-			fnc.dependencies  = args
-		return fnc
-	return decorator
-
-
-def _generic_app(name, context, flags=''):
-	"""Use this function when no other information is given"""
-	context.flags += flags
-	return name, context
-
-
-def _generic_wrapper(name, flags=''):
-	"""Wraps _generic_app into a method for Applications"""
-	assert isinstance(name, str)
-	return depends_on(name)(lambda self, context:
-			_generic_app(name, context, flags))
diff --git a/ranger/api/commands.py b/ranger/api/commands.py
index a2501c7f..aaadde5d 100644
--- a/ranger/api/commands.py
+++ b/ranger/api/commands.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 # TODO: Add an optional "!" to all commands and set a flag if it's there
 
@@ -35,9 +23,15 @@ class CommandContainer(object):
 	def __getitem__(self, key):
 		return self.commands[key]
 
-	def alias(self, new, old):
+	def alias(self, name, full_command):
 		try:
-			self.commands[new] = self.commands[old]
+			cmd = type(name, (AliasCommand, ), dict())
+			cmd._based_function = name
+			cmd._function_name = name
+			cmd._object_name = name
+			cmd._line = full_command
+			self.commands[name] = cmd
+
 		except:
 			pass
 
@@ -185,7 +179,7 @@ class Command(FileManagerAware):
 	def _tab_only_directories(self):
 		from os.path import dirname, basename, expanduser, join
 
-		cwd = self.fm.env.cwd.path
+		cwd = self.fm.thisdir.path
 
 		rel_dest = self.rest(1)
 
@@ -231,7 +225,7 @@ class Command(FileManagerAware):
 	def _tab_directory_content(self):
 		from os.path import dirname, basename, expanduser, join
 
-		cwd = self.fm.env.cwd.path
+		cwd = self.fm.thisdir.path
 
 		rel_dest = self.rest(1)
 
@@ -329,3 +323,11 @@ class FunctionCommand(Command):
 				self.fm.notify("Bad arguments for %s.%s: %s, %s" %
 						(self._object_name, self._function_name,
 							repr(args), repr(keywords)), bad=True)
+
+class AliasCommand(Command):
+	_based_function = None
+	_object_name = ""
+	_function_name = "unknown"
+	_line = ""
+	def execute(self):
+		self.fm.execute_console(self._line)
diff --git a/ranger/api/options.py b/ranger/api/options.py
index e2558ffb..38e5f760 100644
--- a/ranger/api/options.py
+++ b/ranger/api/options.py
@@ -1,18 +1,7 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
+# THIS WHOLE FILE IS OBSOLETE AND EXISTS FOR BACKWARDS COMPATIBILITIY
 import re
 from re import compile as regexp
 from ranger.api import *
diff --git a/ranger/colorschemes/default.py b/ranger/colorschemes/default.py
index fb46dd43..aed6812a 100644
--- a/ranger/colorschemes/default.py
+++ b/ranger/colorschemes/default.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 from ranger.gui.colorscheme import ColorScheme
 from ranger.gui.color import *
@@ -114,4 +102,10 @@ class Default(ColorScheme):
 			if context.selected:
 				attr |= reverse
 
+			if context.loaded:
+				if context.selected:
+					fg = green
+				else:
+					bg = green
+
 		return fg, bg, attr
diff --git a/ranger/colorschemes/default88.py b/ranger/colorschemes/default88.py
index 8bf33807..2a2d4825 100644
--- a/ranger/colorschemes/default88.py
+++ b/ranger/colorschemes/default88.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 """
 The default colorscheme, using 88 colors.
diff --git a/ranger/colorschemes/jungle.py b/ranger/colorschemes/jungle.py
index ea2e0d94..4f2b30ba 100644
--- a/ranger/colorschemes/jungle.py
+++ b/ranger/colorschemes/jungle.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 from ranger.gui.color import *
 from ranger.colorschemes.default import Default
diff --git a/ranger/colorschemes/snow.py b/ranger/colorschemes/snow.py
index a8125ee6..c5940cc8 100644
--- a/ranger/colorschemes/snow.py
+++ b/ranger/colorschemes/snow.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 from ranger.gui.colorscheme import ColorScheme
 from ranger.gui.color import *
diff --git a/ranger/defaults/__init__.py b/ranger/config/__init__.py
index 71df3cb3..71df3cb3 100644
--- a/ranger/defaults/__init__.py
+++ b/ranger/config/__init__.py
diff --git a/ranger/defaults/commands.py b/ranger/config/commands.py
index a89dd0f7..2eff68e6 100644
--- a/ranger/defaults/commands.py
+++ b/ranger/config/commands.py
@@ -54,16 +54,16 @@
 # self.fm.notify(string): Print the given string on the screen.
 # self.fm.notify(string, bad=True): Print the given string in RED.
 # self.fm.reload_cwd(): Reload the current working directory.
-# self.fm.env.cwd: The current working directory. (A File object.)
-# self.fm.env.cf: The current file. (A File object too.)
-# self.fm.env.cwd.get_selection(): A list of all selected files.
+# self.fm.thisdir: The current working directory. (A File object.)
+# self.fm.thisfile: The current file. (A File object too.)
+# self.fm.thistab.get_selection(): A list of all selected files.
 # self.fm.execute_console(string): Execute the string as a ranger command.
 # self.fm.open_console(string): Open the console with the given string
 #      already typed in for you.
 # self.fm.move(direction): Moves the cursor in the given direction, which
 #      can be something like down=3, up=5, right=1, left=1, to=6, ...
 #
-# File objects (for example self.fm.env.cf) have these useful attributes and
+# File objects (for example self.fm.thisfile) have these useful attributes and
 # methods:
 #
 # cf.path: The path to the file.
@@ -86,11 +86,15 @@ class alias(Command):
 
 	Copies the oldcommand as newcommand.
 	"""
+
+	context = 'browser'
+	resolve_macros = False
+	
 	def execute(self):
 		if not self.arg(1) or not self.arg(2):
 			self.fm.notify('Syntax: alias <newcommand> <oldcommand>', bad=True)
 		else:
-			self.fm.commands.alias(self.arg(1), self.arg(2))
+			self.fm.commands.alias(self.arg(1), self.rest(2))
 
 class cd(Command):
 	"""
@@ -122,7 +126,7 @@ class cd(Command):
 	def tab(self):
 		from os.path import dirname, basename, expanduser, join
 
-		cwd = self.fm.env.cwd.path
+		cwd = self.fm.thisdir.path
 		rel_dest = self.rest(1)
 
 		bookmarks = [v.path for v in self.fm.bookmarks.dct.values()
@@ -218,7 +222,7 @@ class shell(Command):
 			return (start + program + ' ' for program \
 					in get_executables() if program.startswith(command))
 		if position_of_last_space == len(command) - 1:
-			selection = self.fm.env.get_selection()
+			selection = self.fm.thistab.get_selection()
 			if len(selection) == 1:
 				return self.line + selection[0].shell_escaped_basename + ' '
 			else:
@@ -226,14 +230,14 @@ class shell(Command):
 		else:
 			before_word, start_of_word = self.line.rsplit(' ', 1)
 			return (before_word + ' ' + file.shell_escaped_basename \
-					for file in self.fm.env.cwd.files \
+					for file in self.fm.thisdir.files \
 					if file.shell_escaped_basename.startswith(start_of_word))
 
 class open_with(Command):
 	def execute(self):
 		app, flags, mode = self._get_app_flags_mode(self.rest(1))
 		self.fm.execute_file(
-				files = [f for f in self.fm.env.cwd.get_selection()],
+				files = [f for f in self.fm.thistab.get_selection()],
 				app = app,
 				flags = flags,
 				mode = mode)
@@ -311,18 +315,8 @@ class open_with(Command):
 
 		return app, flags, int(mode)
 
-	def tab(self):
-		data = self.rest(1)
-		if ' ' not in data:
-			all_apps = self.fm.apps.all()
-			if all_apps:
-				return (self.firstpart + app for app in all_apps if app.startswith(data))
-
-		return None
-
 	def _is_app(self, arg):
-		return self.fm.apps.has(arg) or \
-			(not self._is_flags(arg) and arg in get_executables())
+		return not self._is_flags(arg) and not arg.isdigit()
 
 	def _is_flags(self, arg):
 		return all(x in ALLOWED_FLAGS for x in arg)
@@ -352,7 +346,7 @@ class find(Command):
 
 	def quick(self):
 		self.count = 0
-		cwd = self.fm.env.cwd
+		cwd = self.fm.thisdir
 		arg = self.rest(1)
 		if not arg:
 			return False
@@ -375,7 +369,7 @@ class find(Command):
 				self.count += 1
 				if self.count == 1:
 					cwd.move(to=(cwd.pointer + i) % len(cwd.files))
-					self.fm.env.cf = cwd.pointed_obj
+					self.fm.thisfile = cwd.pointed_obj
 			if self.count > 1:
 				return False
 			i += 1
@@ -496,8 +490,8 @@ class delete(Command):
 			# user did not confirm deletion
 			return
 
-		cwd = self.fm.env.cwd
-		cf = self.fm.env.cf
+		cwd = self.fm.thisdir
+		cf = self.fm.thisfile
 
 		if cwd.marked_items or (cf.is_directory and not cf.is_link \
 				and len(os.listdir(cf.path)) > 0):
@@ -519,7 +513,7 @@ class mark(Command):
 
 	def execute(self):
 		import re
-		cwd = self.fm.env.cwd
+		cwd = self.fm.thisdir
 		input = self.rest(1)
 		searchflags = re.UNICODE
 		if input.lower() == input: # "smartcase"
@@ -565,7 +559,7 @@ class load_copy_buffer(Command):
 		except:
 			return self.fm.notify("Cannot open %s" % \
 					(fname or self.copy_buffer_filename), bad=True)
-		self.fm.env.copy = set(File(g) \
+		self.fm.copy_buffer = set(File(g) \
 			for g in f.read().split("\n") if exists(g))
 		f.close()
 		self.fm.ui.redraw_main_column()
@@ -586,7 +580,7 @@ class save_copy_buffer(Command):
 		except:
 			return self.fm.notify("Cannot open %s" % \
 					(fname or self.copy_buffer_filename), bad=True)
-		f.write("\n".join(f.path for f in self.fm.env.copy))
+		f.write("\n".join(f.path for f in self.fm.copy_buffer))
 		f.close()
 
 
@@ -610,7 +604,7 @@ class mkdir(Command):
 		from os.path import join, expanduser, lexists
 		from os import mkdir
 
-		dirname = join(self.fm.env.cwd.path, expanduser(self.rest(1)))
+		dirname = join(self.fm.thisdir.path, expanduser(self.rest(1)))
 		if not lexists(dirname):
 			mkdir(dirname)
 		else:
@@ -627,7 +621,7 @@ class touch(Command):
 	def execute(self):
 		from os.path import join, expanduser, lexists
 
-		fname = join(self.fm.env.cwd.path, expanduser(self.rest(1)))
+		fname = join(self.fm.thisdir.path, expanduser(self.rest(1)))
 		if not lexists(fname):
 			open(fname, 'a').close()
 		else:
@@ -643,7 +637,7 @@ class edit(Command):
 
 	def execute(self):
 		if not self.arg(1):
-			self.fm.edit_file(self.fm.env.cf.path)
+			self.fm.edit_file(self.fm.thisfile.path)
 		else:
 			self.fm.edit_file(self.rest(1))
 
@@ -661,7 +655,7 @@ class eval_(Command):
 
 	Examples:
 	:eval fm
-	:eval len(fm.env.directories)
+	:eval len(fm.directories)
 	:eval p("Hello World!")
 	"""
 	name = 'eval'
@@ -708,16 +702,16 @@ class rename(Command):
 		if not new_name:
 			return self.fm.notify('Syntax: rename <newname>', bad=True)
 
-		if new_name == self.fm.env.cf.basename:
+		if new_name == self.fm.thisfile.basename:
 			return
 
 		if access(new_name, os.F_OK):
 			return self.fm.notify("Can't rename: file already exists!", bad=True)
 
-		self.fm.rename(self.fm.env.cf, new_name)
+		self.fm.rename(self.fm.thisfile, new_name)
 		f = File(new_name)
-		self.fm.env.cwd.pointed_obj = f
-		self.fm.env.cf = f
+		self.fm.thisdir.pointed_obj = f
+		self.fm.thisfile = f
 
 	def tab(self):
 		return self._tab_directory_content()
@@ -749,7 +743,7 @@ class chmod(Command):
 			self.fm.notify("Need an octal number between 0 and 777!", bad=True)
 			return
 
-		for file in self.fm.env.get_selection():
+		for file in self.fm.thistab.get_selection():
 			try:
 				os.chmod(file.path, mode)
 			except Exception as ex:
@@ -758,7 +752,7 @@ class chmod(Command):
 		try:
 			# reloading directory.  maybe its better to reload the selected
 			# files only.
-			self.fm.env.cwd.load_content()
+			self.fm.thisdir.load_content()
 		except:
 			pass
 
@@ -782,7 +776,7 @@ class bulkrename(Command):
 		py3 = sys.version > "3"
 
 		# Create and edit the file list
-		filenames = [f.basename for f in self.fm.env.get_selection()]
+		filenames = [f.basename for f in self.fm.thistab.get_selection()]
 		listfile = tempfile.NamedTemporaryFile()
 
 		if py3:
@@ -829,7 +823,7 @@ class relink(Command):
 		from ranger.fsobject import File
 
 		new_path = self.rest(1)
-		cf = self.fm.env.cf
+		cf = self.fm.thisfile
 
 		if not new_path:
 			return self.fm.notify('Syntax: relink <newpath>', bad=True)
@@ -847,12 +841,12 @@ class relink(Command):
 			self.fm.notify(err)
 
 		self.fm.reset()
-		self.fm.env.cwd.pointed_obj = cf
-		self.fm.env.cf = cf
+		self.fm.thisdir.pointed_obj = cf
+		self.fm.thisfile = cf
 
 	def tab(self):
 		if not self.rest(1):
-			return self.line+os.readlink(self.fm.env.cf.path)
+			return self.line+os.readlink(self.fm.thisfile.path)
 		else:
 			return self._tab_directory_content()
 
@@ -887,7 +881,7 @@ class copymap(Command):
 			return self.notify("Not enough arguments", bad=True)
 
 		for arg in self.args[2:]:
-			self.fm.env.keymaps.copy(self.context, self.arg(1), arg)
+			self.fm.ui.keymaps.copy(self.context, self.arg(1), arg)
 
 
 class copypmap(copymap):
@@ -923,7 +917,7 @@ class unmap(Command):
 
 	def execute(self):
 		for arg in self.args[1:]:
-			self.fm.env.keymaps.unbind(self.context, arg)
+			self.fm.ui.keymaps.unbind(self.context, arg)
 
 
 class cunmap(unmap):
@@ -964,7 +958,7 @@ class map_(Command):
 	resolve_macros = False
 
 	def execute(self):
-		self.fm.env.keymaps.bind(self.context, self.arg(1), self.rest(2))
+		self.fm.ui.keymaps.bind(self.context, self.arg(1), self.rest(2))
 
 
 class cmap(map_):
@@ -1015,5 +1009,5 @@ class grep(Command):
 		if self.rest(1):
 			action = ['grep', '--color=always', '--line-number']
 			action.extend(['-e', self.rest(1), '-r'])
-			action.extend(f.path for f in self.fm.env.get_selection())
+			action.extend(f.path for f in self.fm.thistab.get_selection())
 			self.fm.execute_command(action, flags='p')
diff --git a/ranger/defaults/options.py b/ranger/config/options.py
index 5e30f042..681feabc 100644
--- a/ranger/defaults/options.py
+++ b/ranger/config/options.py
@@ -14,8 +14,6 @@
 # But make sure you update your configs when you update ranger.
 # ===================================================================
 
-from ranger.api.options import *
-
 # Load the deault rc.conf file?  If you've copied it to your configuration
 # direcory, then you should deactivate this option.
 load_default_rc = True
@@ -25,8 +23,7 @@ column_ratios = (1, 3, 4)
 
 # Which files should be hidden?  Toggle this by typing `zh' or
 # changing the setting `show_hidden'
-hidden_filter = regexp(
-	r'^\.|\.(?:pyc|pyo|bak|swp)$|^lost\+found$|^__(py)?cache__$')
+hidden_filter = r'^\.|\.(?:pyc|pyo|bak|swp)$|^lost\+found$|^__(py)?cache__$'
 show_hidden = False
 
 # Which script is used to generate file previews?
diff --git a/ranger/defaults/rc.conf b/ranger/config/rc.conf
index 77ffa5c3..1b344465 100644
--- a/ranger/defaults/rc.conf
+++ b/ranger/config/rc.conf
@@ -53,7 +53,7 @@ map !  console shell
 map @  console -p6 shell  %%s
 map #  console shell -p 
 map s  console shell 
-map r  console open_with 
+map r  chain draw_possible_programs; console open_with 
 map f  console find 
 map cd console cd 
 
@@ -139,8 +139,8 @@ map yn shell -d echo -n %f    | xsel -i
 map =  chmod
 
 map cw console rename 
-map A  eval fm.open_console('rename ' + fm.env.cf.basename)
-map I  eval fm.open_console('rename ' + fm.env.cf.basename, position=7)
+map A  eval fm.open_console('rename ' + fm.thisfile.basename)
+map I  eval fm.open_console('rename ' + fm.thisfile.basename, position=7)
 
 map pp paste
 map po paste overwrite=True
@@ -191,6 +191,7 @@ map gt        tab_move 1
 map gT        tab_move -1
 map gn        tab_new ~
 map gc        tab_close
+map uq        tab_restore
 map <a-1>     tab_open 1
 map <a-2>     tab_open 2
 map <a-3>     tab_open 3
@@ -244,13 +245,18 @@ map um<any> unset_bookmark %any
 map m<bg>   draw_bookmarks
 copymap m<bg>  um<bg> `<bg> '<bg>
 
-# Beware. I haven't figured out how to make these keybindings pretty yet:
-
-# map +ow shell -d chmod o+w (one mapping for each combination)
-eval import itertools; [cmd("map +%s%s shell -d chmod %s+%s %%s" % (mode+mode)) for mode in itertools.product("ugoa", "rwxXst")]
-
-# map -ow shell -d chmod o+w (one mapping for each combination)
-eval import itertools; [cmd("map -%s%s shell -d chmod %s-%s %%s" % (mode+mode)) for mode in itertools.product("ugoa", "rwxXst")]
+# Generate all the chmod bindings with some python help:
+eval for arg in "rwxXst": cmd("map +u{0} shell -d chmod u+{0} %s".format(arg))
+eval for arg in "rwxXst": cmd("map +g{0} shell -d chmod g+{0} %s".format(arg))
+eval for arg in "rwxXst": cmd("map +o{0} shell -d chmod o+{0} %s".format(arg))
+eval for arg in "rwxXst": cmd("map +a{0} shell -d chmod a+{0} %s".format(arg))
+eval for arg in "rwxXst": cmd("map +{0}  shell -d chmod u+{0} %s".format(arg))
+
+eval for arg in "rwxXst": cmd("map -u{0} shell -d chmod u-{0} %s".format(arg))
+eval for arg in "rwxXst": cmd("map -g{0} shell -d chmod g-{0} %s".format(arg))
+eval for arg in "rwxXst": cmd("map -o{0} shell -d chmod o-{0} %s".format(arg))
+eval for arg in "rwxXst": cmd("map -a{0} shell -d chmod a-{0} %s".format(arg))
+eval for arg in "rwxXst": cmd("map -{0}  shell -d chmod u-{0} %s".format(arg))
 
 # ===================================================================
 # == Define keys for the console
diff --git a/ranger/config/rifle.conf b/ranger/config/rifle.conf
new file mode 100644
index 00000000..e4528875
--- /dev/null
+++ b/ranger/config/rifle.conf
@@ -0,0 +1,195 @@
+# vim: ft=cfg
+#
+# This is the configuration file of "rifle", ranger's file executor/opener.
+# Each line consists of conditions and a command.  For each line the conditions
+# are checked and if they are met, the respective command is run.
+#
+# Syntax:
+#   <condition1> , <condition2> , ... = command
+#
+# The command can contain these environment variables:
+#   $1-$9 | The n-th selected file
+#   $@    | All selected files
+#
+# If you use the special command "ask", rifle will ask you what program to run.
+#
+# Prefixing a condition with "!" will negate its result.
+# These conditions are currently supported:
+#   match <regexp> | The regexp matches $1
+#   ext <regexp>   | The regexp matches the extension of $1
+#   mime <regexp>  | The regexp matches the mime type of $1
+#   name <regexp>  | The regexp matches the basename of $1
+#   path <regexp>  | The regexp matches the absolute path of $1
+#   has <program>  | The program is installed (i.e. located in $PATH)
+#   file           | $1 is a file
+#   directory      | $1 is a directory
+#   number <n>     | change the number of this command to n
+#   terminal       | stdin, stderr and stdout are connected to a terminal
+#   X              | $DISPLAY is not empty (i.e. Xorg runs)
+#
+# There are also pseudo-conditions which have a "side effect":
+#   flag <flags>  | Change how the program is run. See below.
+#   label <label> | Assign a label or name to the command so it can
+#                 | be started with :open_with <label> in ranger
+#                 | or `rifle -p <label>` in the standalone executable.
+#   else          | Always true.
+#
+# Flags are single characters which slightly transform the command:
+#   f | Fork the program, make it run in the background.
+#     |   New command = setsid $command >& /dev/null &
+#   r | Execute the command with root permissions
+#     |   New command = sudo $command
+#   t | Run the program in a new terminal.  If $TERMCMD is not defined,
+#     | rifle will attempt to extract it from $TERM.
+#     |   New command = $TERMCMD -e $command
+
+#-------------------------------------------
+# Websites
+#-------------------------------------------
+# Rarely installed browsers get higher priority; It is assumed that if you
+# install a rare browser, you probably use it.  Firefox/konqueror/w3m on the
+# other hand are often only installed as fallback browsers.
+ext x?html?, has surf,           X, flag f = surf -- file://"$1"
+ext x?html?, has vimprobable,    X, flag f = vimprobable -- "$@"
+ext x?html?, has vimprobable2,   X, flag f = vimprobable2 -- "$@"
+ext x?html?, has jumanji,        X, flag f = jumanji -- "$@"
+ext x?html?, has luakit,         X, flag f = luakit -- "$@"
+ext x?html?, has uzbl,           X, flag f = uzbl -- "$@"
+ext x?html?, has uzbl-browser,   X, flag f = uzbl-browser -- "$@"
+ext x?html?, has uzbl-core,      X, flag f = uzbl-core -- "$@"
+ext x?html?, has midori,         X, flag f = midori -- "$@"
+ext x?html?, has opera,          X, flag f = opera -- "$@"
+ext x?html?, has firefox,        X, flag f = firefox -- "$@"
+ext x?html?, has seamonkey,      X, flag f = seamonkey -- "$@"
+ext x?html?, has iceweasel,      X, flag f = iceweasel -- "$@"
+ext x?html?, has epiphany,       X, flag f = epiphany -- "$@"
+ext x?html?, has konqueror,      X, flag f = konqueror -- "$@"
+ext x?html?, has elinks,          terminal = elinks "$@"
+ext x?html?, has links2,          terminal = links2 "$@"
+ext x?html?, has links,           terminal = links "$@"
+ext x?html?, has lynx,            terminal = lynx -- "$@"
+ext x?html?, has w3m,             terminal = w3m "$@"
+
+#-------------------------------------------
+# Misc
+#-------------------------------------------
+# Define the "editor" for text files as first action
+mime ^text,  label editor = "$EDITOR" -- "$@"
+mime ^text,  label pager  = "$PAGER" -- "$@"
+!mime ^text, label editor, ext xml|csv|tex|py|pl|rb|sh|php = "$EDITOR" -- "$@"
+!mime ^text, label editor, ext xml|csv|tex|py|pl|rb|sh|php = "$PAGER" -- "$@"
+
+ext 1                         = man "$1"
+ext s[wmf]c, has zsnes, X     = zsnes "$1"
+ext nes, has fceux, X         = fceux "$1"
+ext exe                       = wine "$1"
+name ^[mM]akefile$            = make
+
+#--------------------------------------------
+# Code
+#-------------------------------------------
+ext py  = python -- "$1"
+ext pl  = perl -- "$1"
+ext rb  = ruby -- "$1"
+ext sh  = bash -- "$1"
+ext php = php -- "$1"
+
+#--------------------------------------------
+# Audio without X
+#-------------------------------------------
+mime ^audio|ogg$, terminal, has mplayer  = mplayer -- "$@"
+mime ^audio|ogg$, terminal, has mplayer2 = mplayer2 -- "$@"
+ext midi?,        terminal, has wildmidi = wildmidi -- "$@"
+
+#--------------------------------------------
+# Video/Audio with a GUI
+#-------------------------------------------
+mime ^video|audio, has gmplayer, X, flag f = gmplayer -- "$@"
+mime ^video|audio, has smplayer, X, flag f = smplayer "$@"
+mime ^video,       has mplayer2, X, flag f = mplayer2 -- "$@"
+mime ^video,       has mplayer2, X, flag f = mplayer2 -fs -- "$@"
+mime ^video,       has mplayer,  X, flag f = mplayer -- "$@"
+mime ^video,       has mplayer,  X, flag f = mplayer -fs -- "$@"
+mime ^video|audio, has vlc,      X, flag f = vlc -- "$@"
+mime ^video|audio, has totem,    X, flag f = totem -- "$@"
+mime ^video|audio, has totem,    X, flag f = totem --fullscreen -- "$@"
+
+# MKV videos are sometimes not recognized as video/x-matroska
+!mime ^video|audio, ext mkv, has gmplayer, X, flag f = gmplayer -- "$@"
+!mime ^video|audio, ext mkv, has smplayer, X, flag f = smplayer "$@"
+!mime ^video,       ext mkv, has mplayer2, X, flag f = mplayer2 -- "$@"
+!mime ^video,       ext mkv, has mplayer2, X, flag f = mplayer2 -fs -- "$@"
+!mime ^video,       ext mkv, has mplayer,  X, flag f = mplayer -- "$@"
+!mime ^video,       ext mkv, has mplayer,  X, flag f = mplayer -fs -- "$@"
+!mime ^video|audio, ext mkv, has vlc,      X, flag f = vlc -- "$@"
+!mime ^video|audio, ext mkv, has totem,    X, flag f = totem -- "$@"
+!mime ^video|audio, ext mkv, has totem,    X, flag f = totem --fullscreen -- "$@"
+
+#--------------------------------------------
+# Video without X:
+#-------------------------------------------
+mime ^video, terminal, !X, has mplayer2  = mplayer2 -- "$@"
+mime ^video, terminal, !X, has mplayer   = mplayer -- "$@"
+!mime ^video, ext mkv, terminal, !X, has mplayer2  = mplayer2 -- "$@"
+!mime ^video, ext mkv, terminal, !X, has mplayer   = mplayer -- "$@"
+
+#-------------------------------------------
+# Image Viewing:
+#-------------------------------------------
+mime ^image, has eog,    X, flag f = eog -- "$@"
+mime ^image, has sxiv,   X, flag f = sxiv -- "$@"
+mime ^image, has feh,    X, flag f = feh -- "$@"
+mime ^image, has mirage, X, flag f = mirage -- "$@"
+mime ^image, has gimp,   X, flag f = gimp -- "$@"
+ext xcf,                 X, flag f = gimp -- "$@"
+
+#-------------------------------------------
+# Documents
+#-------------------------------------------
+ext pdf, has llpp,     X, flag f = llpp "$@"
+ext pdf, has zathura,  X, flag f = zathura -- "$@"
+ext pdf, has mupdf,    X, flag f = mupdf -- "$@"
+ext pdf, has apvlv,    X, flag f = apvlv -- "$@"
+ext pdf, has xpdf,     X, flag f = xpdf -- "$@"
+ext pdf, has evince,   X, flag f = evince -- "$@"
+ext pdf, has okular,   X, flag f = okular -- "$@"
+ext pdf, has epdfview, X, flag f = epdfview -- "$@"
+
+ext            sxc|xlsx?|xlt|xlw|gnm|gnumeric, has gnumeric,    X, flag f = gnumeric -- "$@"
+ext            sxc|xlsx?|xlt|xlw|gnm|gnumeric, has kspread,     X, flag f = kspread -- "$@"
+ext od[dfgpst]|sxc|xlsx?|xlt|xlw|gnm|gnumeric, has libreoffice, X, flag f = libreoffice "$@"
+ext od[dfgpst]|sxc|xlsx?|xlt|xlw|gnm|gnumeric, has soffice,     X, flag f = soffice "$@"
+ext od[dfgpst]|sxc|xlsx?|xlt|xlw|gnm|gnumeric, has ooffice,     X, flag f = ooffice "$@"
+
+ext docx?, has catdoc,       terminal = catdoc -- "$@" | "$PAGER"
+ext docx?, has libreoffice, X, flag f = libreoffice -- "$@"
+ext docx?, has soffice,     X, flag f = soffice -- "$@"
+ext docx?, has ooffice,     X, flag f = ooffice -- "$@"
+
+ext djvu, has evince, X, flag f = evince -- "$@"
+
+#-------------------------------------------
+# Archives
+#-------------------------------------------
+# This requires atool
+ext 7z|ace|ar|arc|bz2?|cab|cpio|cpt|deb|dgc|dmg|gz,  has als     = als -- "$@" | "$PAGER"
+ext iso|jar|msi|pkg|rar|shar|tar|tgz|xar|xpi|xz|zip, has als     = als -- "$@" | "$PAGER"
+ext 7z|ace|ar|arc|bz2?|cab|cpio|cpt|deb|dgc|dmg|gz,  has aunpack = aunpack -- "$@"
+ext iso|jar|msi|pkg|rar|shar|tar|tgz|xar|xpi|xz|zip, has aunpack = aunpack -- "$@"
+
+# Fallback:
+ext tar|gz, has tar = tar vvtf "$@" | "$PAGER"
+ext tar|gz, has tar = tar vvxf "$@"
+
+#-------------------------------------------
+# Misc
+#-------------------------------------------
+label wallpaper, number 11, mime ^image, X = feh --bg-scale "$1"
+label wallpaper, number 12, mime ^image, X = feh --bg-tile "$1"
+label wallpaper, number 13, mime ^image, X = feh --bg-center "$1"
+label wallpaper, number 14, mime ^image, X = feh --bg-fill "$1"
+
+# Define the editor for non-text files + pager as last action
+              !mime ^text, !ext xml|csv|tex|py|pl|rb|sh|php  = ask
+label editor, !mime ^text, !ext xml|csv|tex|py|pl|rb|sh|php  = "$EDITOR" -- "$@"
+label pager,  !mime ^text, !ext xml|csv|tex|py|pl|rb|sh|php  = "$PAGER" -- "$@"
diff --git a/ranger/container/bookmarks.py b/ranger/container/bookmarks.py
index 750515c5..4d692989 100644
--- a/ranger/container/bookmarks.py
+++ b/ranger/container/bookmarks.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 import string
 import re
diff --git a/ranger/container/history.py b/ranger/container/history.py
index dd511d0e..8ba092bc 100644
--- a/ranger/container/history.py
+++ b/ranger/container/history.py
@@ -1,17 +1,7 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
+
+# TODO: rewrite to use deque instead of list
 
 class HistoryEmptyException(Exception):
 	pass
@@ -61,6 +51,14 @@ class History(object):
 		except IndexError:
 			self.add(item)
 
+	def rebase(self, other_history):
+		assert isinstance(other_history, History)
+		index_offset = len(self._history) - self._index
+		self._history[:self._index] = list(other_history._history)
+		if len(self._history) > self.maxlen:
+			self._history = self._history[-self.maxlen:]
+		self._index = len(self._history) - index_offset
+
 	def __len__(self):
 		return len(self._history)
 
diff --git a/ranger/container/settingobject.py b/ranger/container/settingobject.py
index e7ded15e..cbc56278 100644
--- a/ranger/container/settingobject.py
+++ b/ranger/container/settingobject.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 from inspect import isfunction
 from ranger.ext.signals import SignalDispatcher
diff --git a/ranger/container/tags.py b/ranger/container/tags.py
index c08abdaa..7212345e 100644
--- a/ranger/container/tags.py
+++ b/ranger/container/tags.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 from os.path import isdir, exists, dirname, abspath, realpath, expanduser
 import string
diff --git a/ranger/core/actions.py b/ranger/core/actions.py
index 4e72de77..2f76527a 100644
--- a/ranger/core/actions.py
+++ b/ranger/core/actions.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 import codecs
 import os
@@ -29,8 +17,10 @@ from ranger.ext.relative_symlink import relative_symlink
 from ranger.ext.keybinding_parser import key_to_string, construct_keybinding
 from ranger.ext.shell_escape import shell_quote
 from ranger.ext.next_available_filename import next_available_filename
+from ranger.ext.rifle import squash_flags, ASK_COMMAND
 from ranger.core.shared import FileManagerAware, EnvironmentAware, \
 		SettingsAware
+from ranger.core.tab import Tab
 from ranger.fsobject import File
 from ranger.core.loader import CommandLoader
 
@@ -41,13 +31,6 @@ class _MacroTemplate(string.Template):
 	delimiter = ranger.MACRO_DELIMITER
 
 class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
-	search_method = 'ctime'
-	mode = 'normal'  # either 'normal' or 'visual'.
-	_visual_reverse = False
-	_visual_start = None
-	_visual_start_pos = None
-	_previous_selection = None
-
 	# --------------------------
 	# -- Basic Commands
 	# --------------------------
@@ -58,9 +41,10 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 
 	def reset(self):
 		"""Reset the filemanager, clearing the directory buffer"""
-		old_path = self.env.cwd.path
+		old_path = self.thisdir.path
+		self.restorable_tabs = {}
 		self.previews = {}
-		self.env.garbage_collect(-1, self.tabs)
+		self.garbage_collect(-1)
 		self.enter_dir(old_path)
 		self.change_mode('normal')
 
@@ -68,9 +52,9 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 		if mode == self.mode:
 			return
 		if mode == 'visual':
-			self._visual_start       = self.env.cwd.pointed_obj
-			self._visual_start_pos   = self.env.cwd.pointer
-			self._previous_selection = set(self.env.cwd.marked_items)
+			self._visual_start       = self.thisdir.pointed_obj
+			self._visual_start_pos   = self.thisdir.pointer
+			self._previous_selection = set(self.thisdir.marked_items)
 			self.mark_files(val=not self._visual_reverse, movedown=False)
 		elif mode == 'normal':
 			if self.mode == 'visual':
@@ -91,7 +75,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 
 	def reload_cwd(self):
 		try:
-			cwd = self.env.cwd
+			cwd = self.thisdir
 		except:
 			pass
 		cwd.unload()
@@ -120,7 +104,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 			self.loader.remove(index=0)
 
 	def get_cumulative_size(self):
-		for f in self.env.get_selection() or ():
+		for f in self.thistab.get_selection() or ():
 			f.look_up_cumulative_size()
 		self.ui.status.request_redraw()
 		self.ui.redraw_main_column()
@@ -186,74 +170,76 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 
 		macros['rangerdir'] = ranger.RANGERDIR
 
-		if self.fm.env.cf:
-			macros['f'] = self.fm.env.cf.basename
+		if self.fm.thisfile:
+			macros['f'] = self.fm.thisfile.basename
 		else:
 			macros['f'] = MACRO_FAIL
 
-		if self.fm.env.get_selection:
-			macros['s'] = [fl.basename for fl in self.fm.env.get_selection()]
+		if self.fm.thistab.get_selection:
+			macros['s'] = [fl.basename for fl in self.fm.thistab.get_selection()]
 		else:
 			macros['s'] = MACRO_FAIL
 
-		if self.fm.env.copy:
-			macros['c'] = [fl.path for fl in self.fm.env.copy]
+		if self.fm.copy_buffer:
+			macros['c'] = [fl.path for fl in self.fm.copy_buffer]
 		else:
 			macros['c'] = MACRO_FAIL
 
-		if self.fm.env.cwd.files:
-			macros['t'] = [fl.basename for fl in self.fm.env.cwd.files
+		if self.fm.thisdir.files:
+			macros['t'] = [fl.basename for fl in self.fm.thisdir.files
 					if fl.realpath in (self.fm.tags or [])]
 		else:
 			macros['t'] = MACRO_FAIL
 
-		if self.fm.env.cwd:
-			macros['d'] = self.fm.env.cwd.path
+		if self.fm.thisdir:
+			macros['d'] = self.fm.thisdir.path
 		else:
 			macros['d'] = '.'
 
 		# define d/f/s macros for each tab
 		for i in range(1,10):
 			try:
-				tab_dir_path = self.fm.tabs[i]
+				tab = self.fm.tabs[i]
 			except:
 				continue
-			tab_dir = self.fm.env.get_directory(tab_dir_path)
+			tabdir = tab.thisdir
+			if not tabdir:
+				continue
 			i = str(i)
-			macros[i + 'd'] = tab_dir_path
-			if tab_dir.get_selection():
-				macros[i + 's'] = [fl.path for fl in tab_dir.get_selection()]
+			macros[i + 'd'] = tabdir.path
+			if tabdir.get_selection():
+				macros[i + 's'] = [fl.path for fl in tabdir.get_selection()]
 			else:
 				macros[i + 's'] = MACRO_FAIL
-			if tab_dir.pointed_obj:
-				macros[i + 'f'] = tab_dir.pointed_obj.path
+			if tabdir.pointed_obj:
+				macros[i + 'f'] = tabdir.pointed_obj.path
 			else:
 				macros[i + 'f'] = MACRO_FAIL
 
 		# define D/F/S for the next tab
 		found_current_tab = False
-		next_tab_path = None
+		next_tab = None
 		first_tab = None
-		for tab in self.fm.tabs:
+		for tabname in self.fm.tabs:
 			if not first_tab:
-				first_tab = tab
+				first_tab = tabname
 			if found_current_tab:
-				next_tab_path = self.fm.tabs[tab]
+				next_tab = self.fm.tabs[tabname]
 				break
-			if self.fm.current_tab == tab:
+			if self.fm.current_tab == tabname:
 				found_current_tab = True
-		if found_current_tab and not next_tab_path:
-			next_tab_path = self.fm.tabs[first_tab]
-		next_tab = self.fm.env.get_directory(next_tab_path)
-
-		if next_tab:
-			macros['D'] = str(next_tab.path)
-			if next_tab.pointed_obj:
-				macros['F'] = next_tab.pointed_obj.path
+		if found_current_tab and next_tab is None:
+			next_tab = self.fm.tabs[first_tab]
+		next_tab_dir = next_tab.thisdir
+
+		if next_tab_dir:
+			macros['D'] = str(next_tab_dir.path)
+			if next_tab_dir.pointed_obj:
+				macros['F'] = next_tab_dir.pointed_obj.path
 			else:
 				macros['F'] = MACRO_FAIL
-			if next_tab.get_selection():
-				macros['S'] = [fl.path for fl in next_tab.get_selection()]
+			if next_tab_dir.get_selection():
+				macros['S'] = [fl.path for fl in next_tab_dir.get_selection()]
 			else:
 				macros['S'] = MACRO_FAIL
 		else:
@@ -285,14 +271,16 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 		mode is a positive integer.
 		Both flags and mode specify how the program is run."""
 
+		mode = kw['mode'] if 'mode' in kw else 0
+
 		# ranger can act as a file chooser when running with --choosefile=...
-		if ('mode' not in kw or kw['mode'] == 0) and 'app' not in kw:
+		if mode == 0 and 'label' not in kw:
 			if ranger.arg.choosefile:
-				open(ranger.arg.choosefile, 'w').write(self.fm.env.cf.path)
+				open(ranger.arg.choosefile, 'w').write(self.fm.thisfile.path)
 
 			if ranger.arg.choosefiles:
 				open(ranger.arg.choosefiles, 'w').write("".join(
-					f.path + "\n" for f in self.fm.env.get_selection()))
+					f.path + "\n" for f in self.fm.thistab.get_selection()))
 
 			if ranger.arg.choosefile or ranger.arg.choosefiles:
 				raise SystemExit()
@@ -302,16 +290,15 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 		elif type(files) not in (list, tuple):
 			files = [files]
 
-		if 'flags' in kw:
-			from ranger.core.runner import Context
-			context = Context(files=list(files), flags=kw['flags'])
-			context.squash_flags()
-			if 'c' in context.flags:
-				files = [self.fm.env.cf]
+		flags = kw.get('flags', '')
+		if 'c' in squash_flags(flags):
+			files = [self.fm.thisfile]
 
 		self.signal_emit('execute.before', keywords=kw)
+		filenames = [f.path for f in files]
+		label = kw.get('label', kw.get('app', None))
 		try:
-			return self.run(files=list(files), **kw)
+			return self.rifle.execute(filenames, mode, label, flags, None)
 		finally:
 			self.signal_emit('execute.after')
 
@@ -335,7 +322,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 		self.move(to=2, pages=True)  # moves to page 2.
 		self.move(to=1, percentage=True)  # moves to 80%
 		"""
-		cwd = self.env.cwd
+		cwd = self.thisdir
 		direction = Direction(kw)
 		if 'left' in direction or direction.left() > 0:
 			steps = direction.left()
@@ -345,17 +332,18 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 				directory = os.path.join(*(['..'] * steps))
 			except:
 				return
-			self.env.enter_dir(directory)
+			self.thistab.enter_dir(directory)
 			self.change_mode('normal')
 		if cwd and cwd.accessible and cwd.content_loaded:
 			if '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:
+				cf = self.thisfile
+				selection = self.thistab.get_selection()
+				if not self.thistab.enter_dir(cf) and selection:
+					result = self.execute_file(selection, mode=mode)
+					if result in (False, ASK_COMMAND):
 						self.open_console('open_with ')
 			elif direction.vertical() and cwd.files:
 				newpos = direction.move(
@@ -395,35 +383,36 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 		self.change_mode('normal')
 		if narg is not None:
 			n *= narg
-		parent = self.env.at_level(-1)
+		parent = self.thistab.at_level(-1)
 		if parent is not None:
 			if parent.pointer + n < 0:
 				n = 0 - parent.pointer
 			try:
-				self.env.enter_dir(parent.files[parent.pointer+n])
+				self.thistab.enter_dir(parent.files[parent.pointer+n])
 			except IndexError:
 				pass
 
 	def select_file(self, path):
 		path = path.strip()
 		if self.enter_dir(os.path.dirname(path)):
-			self.env.cwd.move_to_obj(path)
+			self.thisdir.move_to_obj(path)
 
 	def history_go(self, relative):
 		"""Move back and forth in the history"""
-		self.env.history_go(int(relative))
+		self.thistab.history_go(int(relative))
 
+	# TODO: remove this method since it is not used?
 	def scroll(self, relative):
 		"""Scroll down by <relative> lines"""
 		if self.ui.browser and self.ui.browser.main_column:
 			self.ui.browser.main_column.scroll(relative)
-			self.env.cf = self.env.cwd.pointed_obj
+			self.thisfile = self.thisdir.pointed_obj
 
 	def enter_dir(self, path, remember=False, history=True):
 		"""Enter the directory at the given path"""
-		cwd = self.env.cwd
-		result = self.env.enter_dir(path, history=history)
-		if cwd != self.env.cwd:
+		cwd = self.thisdir
+		result = self.thistab.enter_dir(path, history=history)
+		if cwd != self.thisdir:
 			if remember:
 				self.bookmarks.remember(cwd)
 			self.change_mode('normal')
@@ -435,14 +424,14 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 
 	def traverse(self):
 		self.change_mode('normal')
-		cf = self.env.cf
-		cwd = self.env.cwd
+		cf = self.thisfile
+		cwd = self.thisdir
 		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
+				cwd = self.thisdir
 				if cwd.pointer < len(cwd) - 1:
 					break
 				if cwd.path == '/':
@@ -463,6 +452,9 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 	def taskview_move(self, narg=None, **kw):
 		self.ui.taskview.move(narg=narg, **kw)
 
+	def pause_tasks(self):
+		self.loader.pause(-1)
+
 	def pager_close(self):
 		if self.ui.pager.visible:
 			self.ui.close_pager()
@@ -479,34 +471,34 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 		return self.run(cmd, **kw)
 
 	def edit_file(self, file=None):
-		"""Calls execute_file with the current file and app='editor'"""
+		"""Calls execute_file with the current file and label='editor'"""
 		if file is None:
-			file = self.env.cf
+			file = self.thisfile
 		elif isinstance(file, str):
 			file = File(os.path.expanduser(file))
 		if file is None:
 			return
-		self.execute_file(file, app = 'editor')
+		self.execute_file(file, label='editor')
 
 	def toggle_option(self, string):
 		"""Toggle a boolean option named <string>"""
-		if isinstance(self.env.settings[string], bool):
-			self.env.settings[string] ^= True
+		if isinstance(self.settings[string], bool):
+			self.settings[string] ^= True
 
 	def set_option(self, optname, value):
 		"""Set the value of an option named <optname>"""
-		self.env.settings[optname] = value
+		self.settings[optname] = value
 
 	def sort(self, func=None, reverse=None):
 		if reverse is not None:
-			self.env.settings['sort_reverse'] = bool(reverse)
+			self.settings['sort_reverse'] = bool(reverse)
 
 		if func is not None:
-			self.env.settings['sort'] = str(func)
+			self.settings['sort'] = str(func)
 
 	def set_filter(self, fltr):
 		try:
-			self.env.cwd.filter = fltr
+			self.thisdir.filter = fltr
 		except:
 			pass
 
@@ -520,10 +512,10 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 		val - mark or unmark?
 		"""
 
-		if self.env.cwd is None:
+		if self.thisdir is None:
 			return
 
-		cwd = self.env.cwd
+		cwd = self.thisdir
 
 		if not cwd.accessible:
 			return
@@ -557,10 +549,10 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 		self.ui.status.need_redraw = True
 
 	def mark_in_direction(self, val=True, dirarg=None):
-		cwd = self.env.cwd
+		cwd = self.thisdir
 		direction = Direction(dirarg)
 		pos, selected = direction.select(lst=cwd.files, current=cwd.pointer,
-				pagesize=self.env.termsize[0])
+				pagesize=self.ui.termsize[0])
 		cwd.pointer = pos
 		cwd.correct_pointer()
 		for item in selected:
@@ -576,7 +568,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 				text = re.compile(text, re.L | re.U | re.I)
 			except:
 				return False
-		self.env.last_search = text
+		self.thistab.last_search = text
 		self.search_next(order='search', offset=offset)
 
 	def search_next(self, order=None, offset=1, forward=True):
@@ -589,7 +581,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 
 		if order in ('search', 'tag'):
 			if order == 'search':
-				arg = self.env.last_search
+				arg = self.thistab.last_search
 				if arg is None:
 					return False
 				if hasattr(arg, 'search'):
@@ -599,10 +591,10 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 			elif order == 'tag':
 				fnc = lambda x: x.realpath in self.tags
 
-			return self.env.cwd.search_fnc(fnc=fnc, offset=offset, forward=forward)
+			return self.thisdir.search_fnc(fnc=fnc, offset=offset, forward=forward)
 
 		elif order in ('size', 'mimetype', 'ctime', 'mtime', 'atime'):
-			cwd = self.env.cwd
+			cwd = self.thisdir
 			if original_order is not None or not cwd.cycle_list:
 				lst = list(cwd.files)
 				if order == 'size':
@@ -635,7 +627,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 		if not self.tags:
 			return
 		if paths is None:
-			tags = tuple(x.realpath for x in self.env.get_selection())
+			tags = tuple(x.realpath for x in self.thistab.get_selection())
 		else:
 			tags = [realpath(path) for path in paths]
 		if value is True:
@@ -668,7 +660,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 		try:
 			self.bookmarks.update_if_outdated()
 			destination = self.bookmarks[str(key)]
-			cwd = self.env.cwd
+			cwd = self.thisdir
 			if destination.path != cwd.path:
 				self.bookmarks.enter(str(key))
 				self.bookmarks.remember(cwd)
@@ -678,7 +670,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 	def set_bookmark(self, key):
 		"""Set the bookmark with the name <key> to the current directory"""
 		self.bookmarks.update_if_outdated()
-		self.bookmarks[str(key)] = self.env.cwd
+		self.bookmarks[str(key)] = self.thisdir
 
 	def unset_bookmark(self, key):
 		"""Delete the bookmark with the name <key>"""
@@ -691,6 +683,19 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 	def hide_bookmarks(self):
 		self.ui.browser.draw_bookmarks = False
 
+	def draw_possible_programs(self):
+		try:
+			target = self.thistab.get_selection()[0]
+		except:
+			self.ui.browser.draw_info = []
+			return
+		programs = self.rifle.list_commands([target.path], None)
+		programs = ['%s | %s' % program[0:2] for program in programs]
+		self.ui.browser.draw_info = programs
+
+	def hide_console_info(self):
+		self.ui.browser.draw_info = False
+
 	# --------------------------
 	# -- Pager
 	# --------------------------
@@ -734,11 +739,11 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 			pager.set_source(["Message Log:", "No messages!"])
 
 	def display_file(self):
-		if not self.env.cf or not self.env.cf.is_file:
+		if not self.thisfile or not self.thisfile.is_file:
 			return
 
 		pager = self.ui.open_embedded_pager()
-		pager.set_source(self.env.cf.get_preview_source(pager.wid, pager.hei))
+		pager.set_source(self.thisfile.get_preview_source(pager.wid, pager.hei))
 
 	# --------------------------
 	# -- Previews
@@ -802,12 +807,12 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 						f.close()
 					else:
 						data[(-1, -1)] = None
-					if self.env.cf.realpath == path:
+					if self.thisfile.realpath == path:
 						self.ui.browser.need_redraw = True
 					data['loading'] = False
 					pager = self.ui.browser.pager
-					if self.env.cf and self.env.cf.is_file:
-						pager.set_source(self.env.cf.get_preview_source(
+					if self.thisfile and self.thisfile.is_file:
+						pager.set_source(self.thisfile.get_preview_source(
 							pager.wid, pager.hei))
 				def on_destroy(signal):
 					try:
@@ -829,23 +834,35 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 	# --------------------------
 	# -- Tabs
 	# --------------------------
-	# This implementation of tabs is very simple and keeps track of
-	# directory paths only.
-
 	def tab_open(self, name, path=None):
-		tab_has_changed = name != self.current_tab
+		tab_has_changed = (name != self.current_tab)
 		self.current_tab = name
-		if path or (name in self.tabs):
-			self.enter_dir(path or self.tabs[name])
+		previous_tab = self.thistab
+		try:
+			tab = self.tabs[name]
+		except KeyError:
+			# create a new tab
+			if path:
+				tab = Tab(path)
+			else:
+				tab = Tab(self.thistab.path)
+			self.tabs[name] = tab
+			self.thistab = tab
+			tab.enter_dir(tab.path, history=True)
+			if previous_tab:
+				tab.inherit_history(previous_tab.history)
 		else:
-			self._update_current_tab()
+			self.thistab = tab
+			tab.enter_dir(tab.path, history=False)
+
 		if tab_has_changed:
 			self.change_mode('normal')
-			self.signal_emit('tab.change')
+			self.signal_emit('tab.change', old=previous_tab, new=self.thistab)
 
 	def tab_close(self, name=None):
 		if name is None:
 			name = self.current_tab
+		tab = self.tabs[name]
 		if name == self.current_tab:
 			direction = -1 if name == self._get_tab_list()[-1] else 1
 			previous = self.current_tab
@@ -854,6 +871,23 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 				return  # can't close last tab
 		if name in self.tabs:
 			del self.tabs[name]
+		self.restorable_tabs.append(tab)
+
+	def tab_restore(self):
+		# NOTE: The name of the tab is not restored.
+		previous_tab = self.thistab
+		if self.restorable_tabs:
+			tab = self.restorable_tabs.pop()
+			for name in range(1, len(self.tabs) + 2):
+				if not name in self.tabs:
+					self.current_tab = name
+					self.tabs[name] = tab
+					tab.enter_dir(tab.path, history=False)
+					self.thistab = tab
+					self.change_mode('normal')
+					self.signal_emit('tab.change', old=previous_tab,
+							new=self.thistab)
+					break
 
 	def tab_move(self, offset):
 		assert isinstance(offset, int)
@@ -873,9 +907,6 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 		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
-
 	# --------------------------
 	# -- Overview of internals
 	# --------------------------
@@ -898,14 +929,15 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 
 		for context in contexts:
 			write("Keybindings in `%s'\n" % context)
-			if context in self.env.keymaps:
-				recurse([], self.env.keymaps[context])
+			if context in self.fm.ui.keymaps:
+				recurse([], self.fm.ui.keymaps[context])
 			else:
 				write("  None\n")
 			write("\n")
 
 		temporary_file.flush()
-		self.run(app='pager', files=[File(temporary_file.name)])
+		pager = os.environ.get('PAGER', ranger.DEFAULT_PAGER)
+		self.run([pager, temporary_file.name])
 
 	def dump_commands(self):
 		temporary_file = tempfile.NamedTemporaryFile()
@@ -927,7 +959,8 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 				write("    :%s\n" % cmd.get_name())
 
 		temporary_file.flush()
-		self.run(app='pager', files=[File(temporary_file.name)])
+		pager = os.environ.get('PAGER', ranger.DEFAULT_PAGER)
+		self.run([pager, temporary_file.name])
 
 	def dump_settings(self):
 		from ranger.container.settingobject import ALLOWED_SETTINGS
@@ -939,23 +972,24 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 			write("%30s = %s\n" % (setting, getattr(self.settings, setting)))
 
 		temporary_file.flush()
-		self.run(app='pager', files=[File(temporary_file.name)])
+		pager = os.environ.get('PAGER', ranger.DEFAULT_PAGER)
+		self.run([pager, temporary_file.name])
 
 	# --------------------------
 	# -- File System Operations
 	# --------------------------
 
 	def uncut(self):
-		self.env.copy = set()
-		self.env.cut = False
+		self.copy_buffer = set()
+		self.do_cut = False
 		self.ui.browser.main_column.request_redraw()
 
 	def copy(self, mode='set', narg=None, dirarg=None):
 		"""Copy the selected items.  Modes are: 'set', 'add', 'remove'."""
 		assert mode in ('set', 'add', 'remove')
-		cwd = self.env.cwd
+		cwd = self.thisdir
 		if not narg and not dirarg:
-			selected = (f for f in self.env.get_selection() if f in cwd.files)
+			selected = (f for f in self.thistab.get_selection() if f in cwd.files)
 		else:
 			if not dirarg and narg:
 				direction = Direction(down=1)
@@ -965,25 +999,25 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 				offset = 1
 			pos, selected = direction.select(
 					override=narg, lst=cwd.files, current=cwd.pointer,
-					pagesize=self.env.termsize[0], offset=offset)
+					pagesize=self.ui.termsize[0], offset=offset)
 			cwd.pointer = pos
 			cwd.correct_pointer()
 		if mode == 'set':
-			self.env.copy = set(selected)
+			self.copy_buffer = set(selected)
 		elif mode == 'add':
-			self.env.copy.update(set(selected))
+			self.copy_buffer.update(set(selected))
 		elif mode == 'remove':
-			self.env.copy.difference_update(set(selected))
-		self.env.cut = False
+			self.copy_buffer.difference_update(set(selected))
+		self.do_cut = False
 		self.ui.browser.main_column.request_redraw()
 
 	def cut(self, mode='set', narg=None, dirarg=None):
 		self.copy(mode=mode, narg=narg, dirarg=dirarg)
-		self.env.cut = True
+		self.do_cut = True
 		self.ui.browser.main_column.request_redraw()
 
 	def paste_symlink(self, relative=False):
-		copied_files = self.env.copy
+		copied_files = self.copy_buffer
 		for f in copied_files:
 			self.notify(next_available_filename(f.basename))
 			try:
@@ -996,7 +1030,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 				self.notify(x)
 
 	def paste_hardlink(self):
-		for f in self.env.copy:
+		for f in self.copy_buffer:
 			try:
 				new_name = next_available_filename(f.basename)
 				link(f.path, join(getcwd(), new_name))
@@ -1004,7 +1038,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 				self.notify(x)
 
 	def paste_hardlinked_subtree(self):
-		for f in self.env.copy:
+		for f in self.copy_buffer:
 			try:
 				target_path = join(getcwd(), f.basename)
 				self._recurse_hardlinked_tree(f.path, target_path)
@@ -1027,16 +1061,16 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 
 	def paste(self, overwrite=False):
 		"""Paste the selected items into the current directory"""
-		copied_files = tuple(self.env.copy)
+		copied_files = tuple(self.copy_buffer)
 
 		if not copied_files:
 			return
 
 		def refresh(_):
-			cwd = self.env.get_directory(original_path)
+			cwd = self.get_directory(original_path)
 			cwd.load_content()
 
-		cwd = self.env.cwd
+		cwd = self.thisdir
 		original_path = cwd.path
 		one_file = copied_files[0]
 		if overwrite:
@@ -1046,9 +1080,9 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 			cp_flags = ['--backup=numbered', '-a', '--']
 			mv_flags = ['--backup=numbered', '--']
 
-		if self.env.cut:
-			self.env.copy.clear()
-			self.env.cut = False
+		if self.do_cut:
+			self.copy_buffer.clear()
+			self.do_cut = False
 			if len(copied_files) == 1:
 				descr = "moving: " + one_file.path
 			else:
@@ -1078,8 +1112,8 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 	def delete(self):
 		# XXX: warn when deleting mount points/unseen marked files?
 		self.notify("Deleting!")
-		selected = self.env.get_selection()
-		self.env.copy -= set(selected)
+		selected = self.thistab.get_selection()
+		self.copy_buffer -= set(selected)
 		if selected:
 			for f in selected:
 				if isdir(f.path) and not os.path.islink(f.path):
@@ -1092,11 +1126,11 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 						os.remove(f.path)
 					except OSError as err:
 						self.notify(err)
-		self.env.ensure_correct_pointer()
+		self.thistab.ensure_correct_pointer()
 
 	def mkdir(self, name):
 		try:
-			os.mkdir(os.path.join(self.env.cwd.path, name))
+			os.mkdir(os.path.join(self.thisdir.path, name))
 		except OSError as err:
 			self.notify(err)
 
diff --git a/ranger/core/environment.py b/ranger/core/environment.py
index b5ab223d..61bbb6b2 100644
--- a/ranger/core/environment.py
+++ b/ranger/core/environment.py
@@ -1,221 +1,111 @@
 # Copyright (C) 2009, 2010, 2011  Roman Zimbelmann <romanz@lavabit.com>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import curses
-import os
-import pwd
-import socket
-from os.path import abspath, normpath, join, expanduser, isdir
+# This software is distributed under the terms of the GNU GPL version 3.
+
+# THIS WHOLE FILE IS OBSOLETE AND EXISTS FOR BACKWARDS COMPATIBILITIY
 
-from ranger.fsobject import Directory
-from ranger.ext.keybinding_parser import KeyBuffer, KeyMaps
-from ranger.container.history import History
+import os
 from ranger.ext.signals import SignalDispatcher
-from ranger.core.shared import SettingsAware
-
-class Environment(SettingsAware, SignalDispatcher):
-	"""
-	A collection of data which is relevant for more than one class.
-	"""
-
-	cwd = None  # current directory
-	copy = None
-	cmd = None
-	cut = None
-	termsize = None
-	history = None
-	directories = None
-	last_search = None
-	pathway = None
-	path = None
+from ranger.core.shared import SettingsAware, FileManagerAware
 
+# COMPAT
+class Environment(SettingsAware, FileManagerAware, SignalDispatcher):
 	def __init__(self, path):
 		SignalDispatcher.__init__(self)
-		self.path = abspath(expanduser(path))
-		self._cf = None
-		self.pathway = ()
-		self.directories = {}
-		self.keybuffer = KeyBuffer()
-		self.keymaps = KeyMaps(self.keybuffer)
-		self.copy = set()
-		self.history = History(self.settings.max_history_size, unique=False)
-
-		try:
-			self.username = pwd.getpwuid(os.geteuid()).pw_name
-		except:
-			self.username = 'uid:' + str(os.geteuid())
-		self.hostname = socket.gethostname()
-		self.home_path = os.path.expanduser('~')
-
-		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 _get_copy(self): return self.fm.copy_buffer
+	def _set_copy(self, obj): self.fm.copy_buffer = obj
+	copy = property(_get_copy, _set_copy)
+
+	def _get_cut(self): return self.fm.do_cut
+	def _set_cut(self, obj): self.fm.do_cut = obj
+	cut = property(_get_cut, _set_cut)
+
+	def _get_keymaps(self): return self.fm.ui.keymaps
+	def _set_keymaps(self, obj): self.fm.ui.keymaps = obj
+	keymaps = property(_get_keymaps, _set_keymaps)
+
+	def _get_keybuffer(self): return self.fm.ui.keybuffer
+	def _set_keybuffer(self, obj): self.fm.ui.keybuffer = obj
+	keybuffer = property(_get_keybuffer, _set_keybuffer)
 
-	def _set_cf(self, value):
-		if value is not self._cf:
-			previous = self._cf
-			self.signal_emit('move', previous=previous, new=value)
+	def _get_username(self): return self.fm.username
+	def _set_username(self, obj): self.fm.username = obj
+	username = property(_get_username, _set_username)
 
-	def _get_cf(self):
-		return self._cf
+	def _get_hostname(self): return self.fm.hostname
+	def _set_hostname(self, obj): self.fm.hostname = obj
+	hostname = property(_get_hostname, _set_hostname)
 
+	def _get_home_path(self): return self.fm.home_path
+	def _set_home_path(self, obj): self.fm.home_path = obj
+	home_path = property(_get_home_path, _set_home_path)
+
+	def _get_get_directory(self): return self.fm.get_directory
+	def _set_get_directory(self, obj): self.fm.get_directory = obj
+	get_directory = property(_get_get_directory, _set_get_directory)
+
+	def _get_garbage_collect(self): return self.fm.garbage_collect
+	def _set_garbage_collect(self, obj): self.fm.garbage_collect = obj
+	garbage_collect = property(_get_garbage_collect, _set_garbage_collect)
+
+	def _get_cwd(self): return self.fm.thisdir
+	def _set_cwd(self, obj): self.fm.thisdir = obj
+	cwd = property(_get_cwd, _set_cwd)
+
+	def _get_cf(self): return self.fm.thisfile
+	def _set_cf(self, obj): self.fm.thisfile = obj
 	cf = property(_get_cf, _set_cf)
 
-	def key_append(self, key):
-		"""Append a key to the keybuffer"""
-
-		# special keys:
-		if key == curses.KEY_RESIZE:
-			self.keybuffer.clear()
-
-		self.keybuffer.add(key)
-
-	def key_clear(self):
-		"""Clear the keybuffer"""
-		self.keybuffer.clear()
-
-	def at_level(self, level):
-		"""
-		Returns the FileSystemObject at the given level.
-		level >0 => previews
-		level 0 => current file/directory
-		level <0 => parent directories
-		"""
-		if level <= 0:
-			try:
-				return self.pathway[level - 1]
-			except IndexError:
-				return None
-		else:
-			directory = self.cf
-			for i in range(level - 1):
-				if directory is None:
-					return None
-				if directory.is_directory:
-					directory = directory.pointed_obj
-				else:
-					return None
-			try:
-				return self.directories[directory.path]
-			except AttributeError:
-				return None
-			except KeyError:
-				return directory
-
-	def garbage_collect(self, age, tabs):
-		"""Delete unused directory objects"""
-		for key in tuple(self.directories):
-			value = self.directories[key]
-			if age != -1:
-				if not value.is_older_than(age) or value in self.pathway:
-					continue
-				if value in tabs.values():
-					continue
-			del self.directories[key]
-			if value.is_directory:
-				value.files = None
-		self.settings.signal_garbage_collect()
-		self.signal_garbage_collect()
-
-	def get_selection(self):
-		if self.cwd:
-			return self.cwd.get_selection()
-		return set()
-
-	def get_directory(self, path):
-		"""Get the directory object at the given path"""
-		path = abspath(path)
-		try:
-			return self.directories[path]
-		except KeyError:
-			obj = Directory(path)
-			self.directories[path] = obj
-			return obj
+	def _get_history(self): return self.fm.thistab.history
+	def _set_history(self, obj): self.fm.thistab.history = obj
+	history = property(_get_history, _set_history)
+
+	def _get_last_search(self): return self.fm.thistab.last_search
+	def _set_last_search(self, obj): self.fm.thistab.last_search = obj
+	last_search = property(_get_last_search, _set_last_search)
+
+	def _get_path(self): return self.fm.thistab.path
+	def _set_path(self, obj): self.fm.thistab.path = obj
+	path = property(_get_path, _set_path)
+
+	def _get_pathway(self): return self.fm.thistab.pathway
+	def _set_pathway(self, obj): self.fm.thistab.pathway = obj
+	pathway = property(_get_pathway, _set_pathway)
+
+	def _get_enter_dir(self): return self.fm.thistab.enter_dir
+	def _set_enter_dir(self, obj): self.fm.thistab.enter_dir = obj
+	enter_dir = property(_get_enter_dir, _set_enter_dir)
+
+	def _get_at_level(self): return self.fm.thistab.at_level
+	def _set_at_level(self, obj): self.fm.thistab.at_level = obj
+	at_level = property(_get_at_level, _set_at_level)
+
+	def _get_get_selection(self): return self.fm.thistab.get_selection
+	def _set_get_selection(self, obj): self.fm.thistab.get_selection = obj
+	get_selection = property(_get_get_selection, _set_get_selection)
+
+	def _get_assign_cursor_positions_for_subdirs(self):
+		return self.fm.thistab.assign_cursor_positions_for_subdirs
+	def _set_assign_cursor_positions_for_subdirs(self, obj):
+		self.fm.thistab.assign_cursor_positions_for_subdirs = obj
+	assign_cursor_positions_for_subdirs = property(
+			_get_assign_cursor_positions_for_subdirs,
+			_set_assign_cursor_positions_for_subdirs)
+
+	def _get_ensure_correct_pointer(self):
+		return self.fm.thistab.ensure_correct_pointer
+	def _set_ensure_correct_pointer(self, obj):
+		self.fm.thistab.ensure_correct_pointer = obj
+	ensure_correct_pointer = property(_get_ensure_correct_pointer,
+			_set_ensure_correct_pointer)
+
+	def _get_history_go(self): return self.fm.thistab.history_go
+	def _set_history_go(self, obj): self.fm.thistab.history_go = obj
+	history_go = property(_get_history_go, _set_history_go)
+
+	def _set_cf_from_signal(self, signal):
+		self.fm._cf = signal.new
 
 	def get_free_space(self, path):
 		stat = os.statvfs(path)
 		return stat.f_bavail * stat.f_bsize
-
-	def assign_cursor_positions_for_subdirs(self):
-		"""Assign correct cursor positions for subdirectories"""
-		last_path = None
-		for path in reversed(self.pathway):
-			if last_path is None:
-				last_path = path
-				continue
-
-			path.move_to_obj(last_path)
-			last_path = path
-
-	def ensure_correct_pointer(self):
-		if self.cwd:
-			self.cwd.correct_pointer()
-
-	def history_go(self, relative):
-		"""Move relative in history"""
-		if self.history:
-			self.history.move(relative).go(history=False)
-
-	def enter_dir(self, path, history = True):
-		"""Enter given path"""
-		if path is None: return
-		path = str(path)
-
-		previous = self.cwd
-
-		# get the absolute path
-		path = normpath(join(self.path, expanduser(path)))
-
-		if not isdir(path):
-			return False
-		new_cwd = self.get_directory(path)
-
-		try:
-			os.chdir(path)
-		except:
-			return True
-		self.path = path
-		self.cwd = new_cwd
-
-		self.cwd.load_content_if_outdated()
-
-		# build the pathway, a tuple of directory objects which lie
-		# on the path to the current directory.
-		if path == '/':
-			self.pathway = (self.get_directory('/'), )
-		else:
-			pathway = []
-			currentpath = '/'
-			for dir in path.split('/'):
-				currentpath = join(currentpath, dir)
-				pathway.append(self.get_directory(currentpath))
-			self.pathway = tuple(pathway)
-
-		self.assign_cursor_positions_for_subdirs()
-
-		# set the current file.
-		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 20327a71..f554ab1b 100644
--- a/ranger/core/fm.py
+++ b/ranger/core/fm.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 """
 The File Manager, putting the pieces together
@@ -20,18 +8,21 @@ The File Manager, putting the pieces together
 from time import time
 from collections import deque
 import mimetypes
-import os
+import os.path
+import pwd
+import socket
 import stat
 import sys
 
 import ranger
-from ranger import *
 from ranger.core.actions import Actions
+from ranger.core.tab import Tab
 from ranger.container.tags import Tags
 from ranger.gui.ui import UI
 from ranger.container.bookmarks import Bookmarks
 from ranger.core.runner import Runner
 from ranger.ext.get_executables import get_executables
+from ranger.ext.rifle import Rifle
 from ranger.fsobject import Directory
 from ranger.ext.signals import SignalDispatcher
 from ranger import __version__
@@ -40,19 +31,42 @@ from ranger.core.loader import Loader
 class FM(Actions, SignalDispatcher):
 	input_blocked = False
 	input_blocked_until = 0
-	def __init__(self, ui=None, bookmarks=None, tags=None):
+	mode = 'normal'  # either 'normal' or 'visual'.
+	search_method = 'ctime'
+
+	_previous_selection = None
+	_visual_reverse = False
+	_visual_start = None
+	_visual_start_pos = None
+
+	def __init__(self, ui=None, bookmarks=None, tags=None, paths=['.']):
 		"""Initialize FM."""
 		Actions.__init__(self)
 		SignalDispatcher.__init__(self)
-		self.ui = ui
+		if ui is None:
+			self.ui = UI()
+		else:
+			self.ui = ui
+		self.start_paths = paths
+		self.directories = dict()
 		self.log = deque(maxlen=20)
 		self.bookmarks = bookmarks
-		self.tags = tags
+		self.current_tab = 1
 		self.tabs = {}
+		self.tags = tags
+		self.restorable_tabs = deque([], ranger.MAX_RESTORABLE_TABS)
 		self.py3 = sys.version_info >= (3, )
 		self.previews = {}
-		self.current_tab = 1
 		self.loader = Loader()
+		self.copy_buffer = set()
+		self.do_cut = False
+
+		try:
+			self.username = pwd.getpwuid(os.geteuid()).pw_name
+		except:
+			self.username = 'uid:' + str(os.geteuid())
+		self.hostname = socket.gethostname()
+		self.home_path = os.path.expanduser('~')
 
 		self.log.append('ranger {0} started! Process ID is {1}.' \
 				.format(__version__, os.getpid()))
@@ -64,6 +78,24 @@ class FM(Actions, SignalDispatcher):
 
 	def initialize(self):
 		"""If ui/bookmarks are None, they will be initialized here."""
+
+		self.tabs = dict((n+1, Tab(path)) for n, path in
+				enumerate(self.start_paths))
+		tab_list = self._get_tab_list()
+		if tab_list:
+			self.current_tab = tab_list[0]
+			self.thistab = self.tabs[self.current_tab]
+		else:
+			self.current_tab = 1
+			self.tabs[self.current_tab] = self.thistab = Tab('.')
+
+		if not ranger.arg.clean and os.path.isfile(self.confpath('rifle.conf')):
+			rifleconf = self.confpath('rifle.conf')
+		else:
+			rifleconf = self.relpath('config/rifle.conf')
+		self.rifle = Rifle(rifleconf)
+		self.rifle.reload_config()
+
 		if self.bookmarks is None:
 			if ranger.arg.clean:
 				bookmarkfile = None
@@ -78,16 +110,18 @@ class FM(Actions, SignalDispatcher):
 		if not ranger.arg.clean and self.tags is None:
 			self.tags = Tags(self.confpath('tagged'))
 
-		if self.ui is None:
-			self.ui = UI()
-			self.ui.initialize()
+		self.ui.setup_curses()
+		self.ui.initialize()
+
+		self.rifle.hook_before_executing = lambda a, b, flags: \
+			self.ui.suspend() if 'f' not in flags else None
+		self.rifle.hook_after_executing = lambda a, b, flags: \
+			self.ui.initialize() if 'f' not in flags else None
+		self.rifle.hook_logger = self.notify
 
 		def mylogfunc(text):
 			self.notify(text, bad=True)
-		self.run = Runner(ui=self.ui, apps=self.apps,
-				logfunc=mylogfunc, fm=self)
-
-		self.env.signal_bind('cd', self._update_current_tab)
+		self.run = Runner(ui=self.ui, logfunc=mylogfunc, fm=self)
 
 		if self.settings.init_function:
 			self.settings.init_function(self)
@@ -107,6 +141,21 @@ class FM(Actions, SignalDispatcher):
 				if debug:
 					raise
 
+	def _get_thisfile(self):
+		return self.thistab.thisfile
+
+	def _set_thisfile(self, obj):
+		self.thistab.thisfile = obj
+
+	def _get_thisdir(self):
+		return self.thistab.thisdir
+
+	def _set_thisdir(self, obj):
+		self.thistab.thisdir = obj
+
+	thisfile = property(_get_thisfile, _set_thisfile)
+	thisdir  = property(_get_thisdir,  _set_thisdir)
+
 	def block_input(self, sec=0):
 		self.input_blocked = sec != 0
 		self.input_blocked_until = time() + sec
@@ -130,20 +179,20 @@ class FM(Actions, SignalDispatcher):
 					shutil.copy(self.relpath(_from), self.confpath(to))
 				except Exception as e:
 					sys.stderr.write("  ERROR: %s\n" % str(e))
-		if which == 'apps' or which == 'all':
-			copy('defaults/apps.py', 'apps.py')
+		if which == 'rifle' or which == 'all':
+			copy('config/rifle.conf', 'rifle.conf')
 		if which == 'commands' or which == 'all':
-			copy('defaults/commands.py', 'commands.py')
+			copy('config/commands.py', 'commands.py')
 		if which == 'rc' or which == 'all':
-			copy('defaults/rc.conf', 'rc.conf')
+			copy('config/rc.conf', 'rc.conf')
 		if which == 'options' or which == 'all':
-			copy('defaults/options.py', 'options.py')
+			copy('config/options.py', 'options.py')
 		if which == 'scope' or which == 'all':
 			copy('data/scope.sh', 'scope.sh')
 			os.chmod(self.confpath('scope.sh'),
 				os.stat(self.confpath('scope.sh')).st_mode | stat.S_IXUSR)
 		if which not in \
-				('all', 'apps', 'scope', 'commands', 'rc', 'options'):
+				('all', 'rifle', 'scope', 'commands', 'rc', 'options'):
 			sys.stderr.write("Unknown config file `%s'\n" % which)
 
 	def confpath(self, *paths):
@@ -157,6 +206,30 @@ class FM(Actions, SignalDispatcher):
 		"""returns the path relative to rangers library directory"""
 		return os.path.join(ranger.RANGERDIR, *paths)
 
+	def get_directory(self, path):
+		"""Get the directory object at the given path"""
+		path = os.path.abspath(path)
+		try:
+			return self.directories[path]
+		except KeyError:
+			obj = Directory(path)
+			self.directories[path] = obj
+			return obj
+
+	def garbage_collect(self, age, tabs=None):  # tabs=None is for COMPATibility
+		"""Delete unused directory objects"""
+		for key in tuple(self.directories):
+			value = self.directories[key]
+			if age != -1:
+				if not value.is_older_than(age) \
+						or any(value in tab.pathway for tab in self.tabs.values()):
+					continue
+			del self.directories[key]
+			if value.is_directory:
+				value.files = None
+		self.settings.signal_garbage_collect()
+		self.signal_garbage_collect()
+
 	def loop(self):
 		"""
 		The main loop consists of:
@@ -167,7 +240,7 @@ class FM(Actions, SignalDispatcher):
 		5. after X loops: collecting unused directory objects
 		"""
 
-		self.env.enter_dir(self.env.path)
+		self.enter_dir(self.thistab.path)
 
 		gc_tick = 0
 
@@ -175,7 +248,6 @@ class FM(Actions, SignalDispatcher):
 		ui = self.ui
 		throbber = ui.throbber
 		loader = self.loader
-		env = self.env
 		has_throbber = hasattr(ui, 'throbber')
 		zombies = self.run.zombies
 
@@ -190,7 +262,7 @@ class FM(Actions, SignalDispatcher):
 
 				ui.redraw()
 
-				ui.set_load_mode(loader.has_work())
+				ui.set_load_mode(not loader.paused and loader.has_work())
 
 				ui.handle_input()
 
@@ -200,10 +272,9 @@ class FM(Actions, SignalDispatcher):
 							zombies.remove(zombie)
 
 				gc_tick += 1
-				if gc_tick > TICKS_BEFORE_COLLECTING_GARBAGE:
+				if gc_tick > ranger.TICKS_BEFORE_COLLECTING_GARBAGE:
 					gc_tick = 0
-					env.garbage_collect(TIME_BEFORE_FILE_BECOMES_GARBAGE,
-							self.tabs)
+					self.garbage_collect(ranger.TIME_BEFORE_FILE_BECOMES_GARBAGE)
 
 		except KeyboardInterrupt:
 			# this only happens in --debug mode. By default, interrupts
@@ -211,9 +282,9 @@ class FM(Actions, SignalDispatcher):
 			raise SystemExit
 
 		finally:
-			if ranger.arg.choosedir and self.env.cwd and self.env.cwd.path:
+			if ranger.arg.choosedir and self.thisdir and self.thisdir.path:
 				# XXX: UnicodeEncodeError: 'utf-8' codec can't encode character
 				# '\udcf6' in position 42: surrogates not allowed
-				open(ranger.arg.choosedir, 'w').write(self.env.cwd.path)
-			self.bookmarks.remember(env.cwd)
+				open(ranger.arg.choosedir, 'w').write(self.thisdir.path)
+			self.bookmarks.remember(self.thisdir)
 			self.bookmarks.save()
diff --git a/ranger/core/helper.py b/ranger/core/helper.py
deleted file mode 100644
index c556b9bd..00000000
--- a/ranger/core/helper.py
+++ /dev/null
@@ -1,211 +0,0 @@
-# Copyright (C) 2009, 2010, 2011  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/>.
-
-"""Helper functions"""
-
-from errno import EEXIST
-import os.path
-import sys
-from ranger import *
-
-def parse_arguments():
-	"""Parse the program arguments"""
-	from optparse import OptionParser
-	from ranger import __version__
-	from ranger.ext.openstruct import OpenStruct
-	from os.path import expanduser
-
-	if 'XDG_CONFIG_HOME' in os.environ and os.environ['XDG_CONFIG_HOME']:
-		default_confdir = os.environ['XDG_CONFIG_HOME'] + '/ranger'
-	else:
-		default_confdir = CONFDIR
-
-	parser = OptionParser(usage=USAGE, version='ranger %s%s' \
-			% (__version__, " (stable)" if STABLE else ""))
-
-	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('--copy-config', type='string', metavar='which',
-			help="copy the default configs to the local config directory. "
-			"Possible values: all, rc, apps, commands, options, scope")
-	parser.add_option('--fail-unless-cd', action='store_true',
-			help="experimental: return the exit code 1 if ranger is" \
-					"used to run a file (with `ranger filename`)")
-	parser.add_option('-r', '--confdir', type='string',
-			metavar='dir', default=default_confdir,
-			help="the configuration directory. (%default)")
-	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', default='',
-			metavar='string',
-			help="if a filename is supplied, run it with these flags.")
-	parser.add_option('--choosefile', type='string', metavar='TARGET',
-			help="Makes ranger act like a file chooser. When opening "
-			"a file, it will quit and write the name of the selected "
-			"file to TARGET.")
-	parser.add_option('--choosefiles', type='string', metavar='TARGET',
-			help="Makes ranger act like a file chooser for multiple files "
-			"at once. When opening a file, it will quit and write the name "
-			"of all selected files to TARGET.")
-	parser.add_option('--choosedir', type='string', metavar='TARGET',
-			help="Makes ranger act like a directory chooser. When ranger quits"
-			", it will write the name of the last visited directory to TARGET")
-	parser.add_option('--list-unused-keys', action='store_true',
-			help="List common keys which are not bound to any action.")
-	parser.add_option('--selectfile', type='string', metavar='filepath',
-			help="Open ranger with supplied file selected.")
-	parser.add_option('--list-tagged-files', type='string', default=None,
-			metavar='tag',
-			help="List all files which are tagged with the given tag, default: *")
-	parser.add_option('--profile', action='store_true',
-			help="Print statistics of CPU usage on exit.")
-	parser.add_option('--cmd', action='append', type='string', metavar='COMMAND',
-			help="Execute COMMAND after the configuration has been read. "
-			"Use this option multiple times to run multiple commands.")
-
-	options, positional = parser.parse_args()
-	arg = OpenStruct(options.__dict__, targets=positional)
-	arg.confdir = expanduser(arg.confdir)
-
-	return arg
-
-
-def load_settings(fm, clean):
-	from ranger.core.actions import Actions
-	import ranger.core.shared
-	import ranger.api.commands
-	from ranger.defaults import commands
-
-	# Load default commands
-	fm.commands = ranger.api.commands.CommandContainer()
-	exclude = ['settings']
-	include = [name for name in dir(Actions) if name not in exclude]
-	fm.commands.load_commands_from_object(fm, include)
-	fm.commands.load_commands_from_module(commands)
-
-	if not clean:
-		allow_access_to_confdir(ranger.arg.confdir, True)
-
-		# Load custom commands
-		try:
-			import commands
-			fm.commands.load_commands_from_module(commands)
-		except ImportError:
-			pass
-
-		# Load apps
-		try:
-			import apps
-		except ImportError:
-			from ranger.defaults import apps
-		fm.apps = apps.CustomApplications()
-
-		# Load rc.conf
-		custom_conf = fm.confpath('rc.conf')
-		default_conf = fm.relpath('defaults', 'rc.conf')
-		load_default_rc = fm.settings.load_default_rc
-
-		if load_default_rc:
-			fm.source(default_conf)
-		if os.access(custom_conf, os.R_OK):
-			fm.source(custom_conf)
-
-		# XXX Load plugins (experimental)
-		try:
-			plugindir = fm.confpath('plugins')
-			plugins = [p[:-3] for p in os.listdir(plugindir) \
-					if p.endswith('.py') and not p.startswith('_')]
-		except:
-			pass
-		else:
-			if not os.path.exists(fm.confpath('plugins', '__init__.py')):
-				f = open(fm.confpath('plugins', '__init__.py'), 'w')
-				f.close()
-
-			ranger.fm = fm
-			for plugin in sorted(plugins):
-				try:
-					module = __import__('plugins', fromlist=[plugin])
-					fm.log.append("Loaded plugin '%s'." % module)
-				except Exception as e:
-					fm.log.append("Error in plugin '%s'" % plugin)
-					import traceback
-					for line in traceback.format_exception_only(type(e), e):
-						fm.log.append(line)
-			ranger.fm = None
-
-		allow_access_to_confdir(ranger.arg.confdir, False)
-	else:
-		from ranger.defaults import apps
-		fm.apps = apps.CustomApplications()
-		fm.source(fm.relpath('defaults', 'rc.conf'))
-
-
-def load_apps(fm, clean):
-	import ranger
-	if not clean:
-		allow_access_to_confdir(ranger.arg.confdir, True)
-		try:
-			import apps
-		except ImportError:
-			from ranger.defaults import apps
-		allow_access_to_confdir(ranger.arg.confdir, False)
-	else:
-		from ranger.defaults import apps
-	fm.apps = apps.CustomApplications()
-
-
-def allow_access_to_confdir(confdir, allow):
-	if allow:
-		try:
-			os.makedirs(confdir)
-		except OSError as err:
-			if err.errno != EEXIST:  # EEXIST means it already exists
-				print("This configuration directory could not be created:")
-				print(confdir)
-				print("To run ranger without the need for configuration")
-				print("files, use the --clean option.")
-				raise SystemExit()
-		if not confdir in sys.path:
-			sys.path[0:0] = [confdir]
-	else:
-		if sys.path[0] == confdir:
-			del sys.path[0]
-
-
-# Debugging functions.  These will be activated when run with --debug.
-# Example usage in the code:
-# import ranger; ranger.log("hello world")
-def log(*objects, **keywords):
-	"""
-	Writes objects to a logfile (for the purpose of debugging only.)
-	Has the same arguments as print() in python3.
-	"""
-	from ranger import arg
-	if LOGFILE is None or not arg.debug or arg.clean: return
-	start = 'start' in keywords and keywords['start'] or 'ranger:'
-	sep   =   'sep' in keywords and keywords['sep']   or ' '
-	_file =  'file' in keywords and keywords['file']  or open(LOGFILE, 'a')
-	end   =   'end' in keywords and keywords['end']   or '\n'
-	_file.write(sep.join(map(str, (start, ) + objects)) + end)
-
-
-def log_traceback():
-	from ranger import arg
-	if LOGFILE is None or not arg.debug or arg.clean: return
-	import traceback
-	traceback.print_stack(file=open(LOGFILE, 'a'))
diff --git a/ranger/core/loader.py b/ranger/core/loader.py
index 59d3e6c0..7bbd2abf 100644
--- a/ranger/core/loader.py
+++ b/ranger/core/loader.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 from collections import deque
 from time import time, sleep
@@ -32,6 +20,7 @@ class Loadable(object):
 	def __init__(self, gen, descr):
 		self.load_generator = gen
 		self.description = descr
+		self.percent = -1
 
 	def get_description(self):
 		return self.description
@@ -123,8 +112,8 @@ class CommandLoader(Loadable, SignalDispatcher, FileManagerAware):
 				self.process.send_signal(20)
 			except:
 				pass
-		Loadable.pause(self)
-		self.signal_emit('pause', process=self.process, loader=self)
+			Loadable.pause(self)
+			self.signal_emit('pause', process=self.process, loader=self)
 
 	def unpause(self):
 		if not self.finished and self.paused:
@@ -132,8 +121,8 @@ class CommandLoader(Loadable, SignalDispatcher, FileManagerAware):
 				self.process.send_signal(18)
 			except:
 				pass
-		Loadable.unpause(self)
-		self.signal_emit('unpause', process=self.process, loader=self)
+			Loadable.unpause(self)
+			self.signal_emit('unpause', process=self.process, loader=self)
 
 	def destroy(self):
 		self.signal_emit('destroy', process=self.process, loader=self)
@@ -154,6 +143,8 @@ def safeDecode(string):
 class Loader(FileManagerAware):
 	seconds_of_work_time = 0.03
 	throbber_chars = r'/-\|'
+	throbber_paused = '#'
+	paused = False
 
 	def __init__(self):
 		self.queue = deque()
@@ -178,6 +169,10 @@ class Loader(FileManagerAware):
 		while obj in self.queue:
 			self.queue.remove(obj)
 		self.queue.appendleft(obj)
+		if self.paused:
+			obj.pause()
+		else:
+			obj.unpause()
 
 	def move(self, _from, to):
 		try:
@@ -213,11 +208,34 @@ class Loader(FileManagerAware):
 			item.destroy()
 			del self.queue[index]
 
+	def pause(self, state):
+		"""
+		Change the pause-state to 1 (pause), 0 (no pause) or -1 (toggle)
+		"""
+		if state == -1:
+			state = not self.paused
+		elif state == self.paused:
+			return
+
+		self.paused = state
+
+		if not self.queue:
+			return
+
+		if state:
+			self.queue[0].pause()
+		else:
+			self.queue[0].unpause()
+
 	def work(self):
 		"""
 		Load items from the queue if there are any.
 		Stop after approximately self.seconds_of_work_time.
 		"""
+		if self.paused:
+			self.status = self.throbber_paused
+			return
+
 		while True:
 			# get the first item with a proper load_generator
 			try:
@@ -229,6 +247,8 @@ class Loader(FileManagerAware):
 			except IndexError:
 				return
 
+		item.unpause()
+
 		self.rotate()
 		if item != self.old_item:
 			if self.old_item:
diff --git a/ranger/core/main.py b/ranger/core/main.py
index b4629801..c3a3d76b 100644
--- a/ranger/core/main.py
+++ b/ranger/core/main.py
@@ -1,32 +1,18 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 """
 The main function responsible to initialize the FM object and stuff.
 """
 
-from ranger.core.helper import *
+import os.path
 
 def main():
 	"""initialize objects and run the filemanager"""
 	import locale
-	import os.path
 	import ranger
 	import sys
-	from ranger.core.shared import (EnvironmentAware, FileManagerAware,
-			SettingsAware)
+	from ranger.core.shared import FileManagerAware, SettingsAware
 	from ranger.core.fm import FM
 
 	if not sys.stdin.isatty():
@@ -85,29 +71,28 @@ def main():
 		elif os.path.isfile(target):
 			def print_function(string):
 				print(string)
-			from ranger.core.runner import Runner
-			from ranger.fsobject import File
+			from ranger.ext.rifle import Rifle
 			fm = FM()
-			runner = Runner(logfunc=print_function, fm=fm)
-			load_apps(runner, arg.clean)
-			runner(files=[File(target)], mode=arg.mode, flags=arg.flags)
+			if not arg.clean and os.path.isfile(fm.confpath('rifle.conf')):
+				rifleconf = fm.confpath('rifle.conf')
+			else:
+				rifleconf = fm.relpath('config/rifle.conf')
+			rifle = Rifle(rifleconf)
+			rifle.reload_config()
+			rifle.execute(targets, number=ranger.arg.mode, flags=ranger.arg.flags)
 			return 1 if arg.fail_unless_cd else 0
 
 	crash_traceback = None
 	try:
 		# Initialize objects
-		from ranger.core.environment import Environment
-		fm = FM()
+		fm = FM(paths=targets)
 		FileManagerAware.fm = fm
-		EnvironmentAware.env = Environment(target)
-		fm.tabs = dict((n+1, os.path.abspath(path)) for n, path \
-				in enumerate(targets[:9]))
 		load_settings(fm, arg.clean)
 
 		if arg.list_unused_keys:
 			from ranger.ext.keybinding_parser import (special_keys,
 					reversed_special_keys)
-			maps = fm.env.keymaps['browser']
+			maps = fm.ui.keymaps['browser']
 			for key in sorted(special_keys.values(), key=lambda x: str(x)):
 				if key not in maps:
 					print("<%s>" % reversed_special_keys[key])
@@ -116,7 +101,7 @@ def main():
 					print(chr(key))
 			return 1 if arg.fail_unless_cd else 0
 
-		if fm.env.username == 'root':
+		if fm.username == 'root':
 			fm.settings.preview_files = False
 			fm.settings.use_preview_script = False
 		if not arg.debug:
@@ -151,7 +136,7 @@ def main():
 	finally:
 		if crash_traceback:
 			try:
-				filepath = fm.env.cf.path if fm.env.cf else "None"
+				filepath = fm.thisfile.path if fm.thisfile else "None"
 			except:
 				filepath = "None"
 		try:
@@ -174,3 +159,149 @@ def main():
 			print("http://savannah.nongnu.org/bugs/?group=ranger&func=additem")
 			return 1
 		return 0
+
+
+def parse_arguments():
+	"""Parse the program arguments"""
+	from optparse import OptionParser
+	from os.path import expanduser
+	from ranger import CONFDIR, USAGE, VERSION
+	from ranger.ext.openstruct import OpenStruct
+
+	if 'XDG_CONFIG_HOME' in os.environ and os.environ['XDG_CONFIG_HOME']:
+		default_confdir = os.environ['XDG_CONFIG_HOME'] + '/ranger'
+	else:
+		default_confdir = CONFDIR
+
+	parser = OptionParser(usage=USAGE, version=VERSION)
+
+	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('--copy-config', type='string', metavar='which',
+			help="copy the default configs to the local config directory. "
+			"Possible values: all, rc, rifle, commands, options, scope")
+	parser.add_option('--fail-unless-cd', action='store_true',
+			help="experimental: return the exit code 1 if ranger is" \
+					"used to run a file (with `ranger filename`)")
+	parser.add_option('-r', '--confdir', type='string',
+			metavar='dir', default=default_confdir,
+			help="the configuration directory. (%default)")
+	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', default='',
+			metavar='string',
+			help="if a filename is supplied, run it with these flags.")
+	parser.add_option('--choosefile', type='string', metavar='TARGET',
+			help="Makes ranger act like a file chooser. When opening "
+			"a file, it will quit and write the name of the selected "
+			"file to TARGET.")
+	parser.add_option('--choosefiles', type='string', metavar='TARGET',
+			help="Makes ranger act like a file chooser for multiple files "
+			"at once. When opening a file, it will quit and write the name "
+			"of all selected files to TARGET.")
+	parser.add_option('--choosedir', type='string', metavar='TARGET',
+			help="Makes ranger act like a directory chooser. When ranger quits"
+			", it will write the name of the last visited directory to TARGET")
+	parser.add_option('--list-unused-keys', action='store_true',
+			help="List common keys which are not bound to any action.")
+	parser.add_option('--selectfile', type='string', metavar='filepath',
+			help="Open ranger with supplied file selected.")
+	parser.add_option('--list-tagged-files', type='string', default=None,
+			metavar='tag',
+			help="List all files which are tagged with the given tag, default: *")
+	parser.add_option('--profile', action='store_true',
+			help="Print statistics of CPU usage on exit.")
+	parser.add_option('--cmd', action='append', type='string', metavar='COMMAND',
+			help="Execute COMMAND after the configuration has been read. "
+			"Use this option multiple times to run multiple commands.")
+
+	options, positional = parser.parse_args()
+	arg = OpenStruct(options.__dict__, targets=positional)
+	arg.confdir = expanduser(arg.confdir)
+
+	return arg
+
+
+def load_settings(fm, clean):
+	from ranger.core.actions import Actions
+	import ranger.core.shared
+	import ranger.api.commands
+	from ranger.config import commands
+
+	# Load default commands
+	fm.commands = ranger.api.commands.CommandContainer()
+	exclude = ['settings']
+	include = [name for name in dir(Actions) if name not in exclude]
+	fm.commands.load_commands_from_object(fm, include)
+	fm.commands.load_commands_from_module(commands)
+
+	if not clean:
+		allow_access_to_confdir(ranger.arg.confdir, True)
+
+		# Load custom commands
+		try:
+			import commands
+			fm.commands.load_commands_from_module(commands)
+		except ImportError:
+			pass
+
+		# Load rc.conf
+		custom_conf = fm.confpath('rc.conf')
+		default_conf = fm.relpath('config', 'rc.conf')
+		load_default_rc = fm.settings.load_default_rc
+
+		if load_default_rc:
+			fm.source(default_conf)
+		if os.access(custom_conf, os.R_OK):
+			fm.source(custom_conf)
+
+		# XXX Load plugins (experimental)
+		try:
+			plugindir = fm.confpath('plugins')
+			plugins = [p[:-3] for p in os.listdir(plugindir) \
+					if p.endswith('.py') and not p.startswith('_')]
+		except:
+			pass
+		else:
+			if not os.path.exists(fm.confpath('plugins', '__init__.py')):
+				f = open(fm.confpath('plugins', '__init__.py'), 'w')
+				f.close()
+
+			ranger.fm = fm
+			for plugin in sorted(plugins):
+				try:
+					module = __import__('plugins', fromlist=[plugin])
+					fm.log.append("Loaded plugin '%s'." % module)
+				except Exception as e:
+					fm.log.append("Error in plugin '%s'" % plugin)
+					import traceback
+					for line in traceback.format_exception_only(type(e), e):
+						fm.log.append(line)
+			ranger.fm = None
+
+		allow_access_to_confdir(ranger.arg.confdir, False)
+	else:
+		fm.source(fm.relpath('config', 'rc.conf'))
+
+
+def allow_access_to_confdir(confdir, allow):
+	import sys
+	from errno import EEXIST
+
+	if allow:
+		try:
+			os.makedirs(confdir)
+		except OSError as err:
+			if err.errno != EEXIST:  # EEXIST means it already exists
+				print("This configuration directory could not be created:")
+				print(confdir)
+				print("To run ranger without the need for configuration")
+				print("files, use the --clean option.")
+				raise SystemExit()
+		if not confdir in sys.path:
+			sys.path[0:0] = [confdir]
+	else:
+		if sys.path[0] == confdir:
+			del sys.path[0]
diff --git a/ranger/core/runner.py b/ranger/core/runner.py
index c02ad6b3..c5decf4c 100644
--- a/ranger/core/runner.py
+++ b/ranger/core/runner.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 """
 This module is an abstract layer over subprocess.Popen
@@ -41,7 +29,9 @@ from subprocess import Popen, PIPE
 from ranger.ext.get_executables import get_executables
 
 
-ALLOWED_FLAGS = 'sdpwcrtSDPWCRT'
+# TODO: Remove unused parts of runner.py
+#ALLOWED_FLAGS = 'sdpwcrtSDPWCRT'
+ALLOWED_FLAGS = 'cfrtCFRT'
 
 
 def press_enter():
@@ -97,11 +87,10 @@ class Context(object):
 
 
 class Runner(object):
-	def __init__(self, ui=None, logfunc=None, apps=None, fm=None):
+	def __init__(self, ui=None, logfunc=None, fm=None):
 		self.ui = ui
 		self.fm = fm
 		self.logfunc = logfunc
-		self.apps = apps
 		self.zombies = set()
 
 	def _log(self, text):
@@ -140,16 +129,6 @@ class Runner(object):
 				flags=flags, wait=wait, popen_kws=popen_kws,
 				file=files and files[0] or None)
 
-		if self.apps:
-			if try_app_first and action is not None:
-				test = self.apps.apply(app, context)
-				if test:
-					action = test
-			if action is None:
-				action = self.apps.apply(app, context)
-				if action is None:
-					return self._log("No action found!")
-
 		if action is None:
 			return self._log("No way of determining the action!")
 
diff --git a/ranger/core/shared.py b/ranger/core/shared.py
index 9da79578..26023e43 100644
--- a/ranger/core/shared.py
+++ b/ranger/core/shared.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 """Shared objects contain singleton variables which can be
 inherited, essentially acting like global variables."""
@@ -83,7 +71,7 @@ class SettingsAware(Awareness):
 				settings._setting_sources.append(my_options)
 			del sys.path[0]
 
-		from ranger.defaults import options as default_options
+		from ranger.config import options as default_options
 		settings._setting_sources.append(default_options)
 		assert all(hasattr(default_options, setting) \
 				for setting in ALLOWED_SETTINGS), \
diff --git a/ranger/core/tab.py b/ranger/core/tab.py
new file mode 100644
index 00000000..6059eb6b
--- /dev/null
+++ b/ranger/core/tab.py
@@ -0,0 +1,159 @@
+# Copyright (C) 2009, 2010, 2011  Roman Zimbelmann <romanz@lavabit.com>
+# This software is distributed under the terms of the GNU GPL version 3.
+
+import os
+import sys
+from os.path import abspath, normpath, join, expanduser, isdir
+
+from ranger.container.history import History
+from ranger.core.shared import FileManagerAware, SettingsAware
+from ranger.ext.signals import SignalDispatcher
+
+class Tab(FileManagerAware, SettingsAware):
+	def __init__(self, path):
+		self.thisdir = None  # Current Working Directory
+		self._thisfile = None  # Current File
+		self.history = History(self.settings.max_history_size, unique=False)
+		self.last_search = None
+		self.pointer = 0
+		self.path = abspath(expanduser(path))
+		self.pathway = ()
+		# NOTE: in the line below, weak=True works only in python3.  In python2,
+		# weak references are not equal to the original object when tested with
+		# "==", and this breaks _set_thisfile_from_signal and _on_tab_change.
+		self.fm.signal_bind('move', self._set_thisfile_from_signal, priority=0.1,
+				weak=(sys.version > '3'))
+		self.fm.signal_bind('tab.change', self._on_tab_change,
+				weak=(sys.version > '3'))
+
+	def _set_thisfile_from_signal(self, signal):
+		if self == signal.tab:
+			self._thisfile = signal.new
+			if self == self.fm.thistab:
+				self.pointer = self.thisdir.pointer
+
+	def _on_tab_change(self, signal):
+		if self == signal.new and self.thisdir:
+			# restore the pointer whenever this tab is reopened
+			self.thisdir.pointer = self.pointer
+			self.thisdir.correct_pointer()
+
+	def _set_thisfile(self, value):
+		if value is not self._thisfile:
+			previous = self._thisfile
+			self.fm.signal_emit('move', previous=previous, new=value, tab=self)
+
+	def _get_thisfile(self):
+		return self._thisfile
+
+	thisfile = property(_get_thisfile, _set_thisfile)
+
+	def at_level(self, level):
+		"""
+		Returns the FileSystemObject at the given level.
+		level >0 => previews
+		level 0 => current file/directory
+		level <0 => parent directories
+		"""
+		if level <= 0:
+			try:
+				return self.pathway[level - 1]
+			except IndexError:
+				return None
+		else:
+			directory = self.thisfile
+			for i in range(level - 1):
+				if directory is None:
+					return None
+				if directory.is_directory:
+					directory = directory.pointed_obj
+				else:
+					return None
+			try:
+				return self.fm.directories[directory.path]
+			except AttributeError:
+				return None
+			except KeyError:
+				return directory
+
+	def get_selection(self):
+		if self.thisdir:
+			return self.thisdir.get_selection()
+		return set()
+
+	def assign_cursor_positions_for_subdirs(self):
+		"""Assign correct cursor positions for subdirectories"""
+		last_path = None
+		for path in reversed(self.pathway):
+			if last_path is None:
+				last_path = path
+				continue
+
+			path.move_to_obj(last_path)
+			last_path = path
+
+	def ensure_correct_pointer(self):
+		if self.thisdir:
+			self.thisdir.correct_pointer()
+
+	def history_go(self, relative):
+		"""Move relative in history"""
+		if self.history:
+			self.history.move(relative).go(history=False)
+
+	def inherit_history(self, other_history):
+		self.history.rebase(other_history)
+
+	def enter_dir(self, path, history = True):
+		"""Enter given path"""
+		# TODO: Ensure that there is always a self.thisdir
+		if path is None: return
+		path = str(path)
+
+		previous = self.thisdir
+
+		# get the absolute path
+		path = normpath(join(self.path, expanduser(path)))
+
+		if not isdir(path):
+			return False
+		new_thisdir = self.fm.get_directory(path)
+
+		try:
+			os.chdir(path)
+		except:
+			return True
+		self.path = path
+		self.thisdir = new_thisdir
+
+		self.thisdir.load_content_if_outdated()
+
+		# build the pathway, a tuple of directory objects which lie
+		# on the path to the current directory.
+		if path == '/':
+			self.pathway = (self.fm.get_directory('/'), )
+		else:
+			pathway = []
+			currentpath = '/'
+			for dir in path.split('/'):
+				currentpath = join(currentpath, dir)
+				pathway.append(self.fm.get_directory(currentpath))
+			self.pathway = tuple(pathway)
+
+		self.assign_cursor_positions_for_subdirs()
+
+		# set the current file.
+		self.thisdir.sort_directories_first = self.fm.settings.sort_directories_first
+		self.thisdir.sort_reverse = self.fm.settings.sort_reverse
+		self.thisdir.sort_if_outdated()
+		self._thisfile = self.thisdir.pointed_obj
+
+		if history:
+			self.history.add(new_thisdir)
+
+		self.fm.signal_emit('cd', previous=previous, new=self.thisdir)
+
+		return True
+
+	def __repr__(self):
+		return "<Tab '%s'>" % self.thisdir
diff --git a/ranger/data/ranger b/ranger/data/ranger
deleted file mode 120000
index 99d43c63..00000000
--- a/ranger/data/ranger
+++ /dev/null
@@ -1 +0,0 @@
-../../ranger.py
\ No newline at end of file
diff --git a/ranger/defaults/apps.py b/ranger/defaults/apps.py
deleted file mode 100644
index ffab8bea..00000000
--- a/ranger/defaults/apps.py
+++ /dev/null
@@ -1,325 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2009, 2010, 2011  Roman Zimbelmann <romanz@lavabit.com>
-# This configuration file is licensed under the same terms as ranger.
-# ===================================================================
-# This is the configuration file for file type detection and application
-# handling.  It's all in python; lines beginning with # are comments.
-#
-# You can customize this in the file ~/.config/ranger/apps.py.
-# It has the same syntax as this file.  In fact, you can just copy this
-# file there with `ranger --copy-config=apps' and make your modifications.
-# But make sure you update your configs when you update ranger.
-#
-# In order to add application definitions "on top of" the default ones
-# in your ~/.config/ranger/apps.py, you should subclass the class defined
-# here like this:
-#
-#   from ranger.defaults.apps import CustomApplications as DefaultApps
-#   class CustomApplications(DeafultApps):
-#       <your definitions here>
-#
-# To override app_defaults, you can write something like:
-#
-#       def app_defaults(self, c):
-#           f = c.file
-#           if f.extension == 'lol':
-#               return "lolopener", c
-#           return DefaultApps.app_default(self, c)
-#
-# ===================================================================
-# This system is based on things called MODES and FLAGS.  You can read
-# in the man page about them.  To remind you, here's a list of all flags.
-# An uppercase flag inverts previous flags of the same name.
-#     s   Silent mode.  Output will be discarded.
-#     d   Detach the process.  (Run in background)
-#     p   Redirect output to the pager
-#     w   Wait for an Enter-press when the process is done
-#     c   Run the current file only, instead of the selection
-#     r   Run application with root privilege 
-#     t   Run application in a new terminal window
-#
-# To implement flags in this file, you could do this:
-#     context.flags += "d"
-# Another example:
-#     context.flags += "Dw"
-#
-# To implement modes in this file, you can do something like:
-#     if context.mode == 1:
-#         <run in one way>
-#     elif context.mode == 2:
-#         <run in another way>
-#
-# ===================================================================
-# The methods are called with a "context" object which provides some
-# attributes that transfer information.  Relevant attributes are:
-#
-# mode -- a number, mainly used in determining the action in app_xyz()
-# flags -- a string with flags which change the way programs are run
-# files -- a list containing files, mainly used in app_xyz
-# filepaths -- a list of the paths of each file
-# file -- an arbitrary file from that list (or None)
-# fm -- the filemanager instance
-# popen_kws -- keyword arguments which are directly passed to Popen
-#
-# ===================================================================
-# The return value of the functions should be either:
-# 1. A reference to another app, like:
-#     return self.app_editor(context)
-#
-# 2. A call to the "either" method, which uses the first program that
-# is installed on your system.  If none are installed, None is returned.
-#     return self.either(context, "libreoffice", "soffice", "ooffice")
-#
-# 3. A tuple of arguments that should be run.
-#     return "mplayer", "-fs", context.file.path
-# If you use lists instead of strings, they will be flattened:
-#     args = ["-fs", "-shuf"]
-#     return "mplayer", args, context.filepaths
-# "context.filepaths" can, and will often be abbreviated with just "context":
-#     return "mplayer", context
-#
-# 4. "None" to indicate that no action was found.
-#     return None
-#
-# ===================================================================
-# When using the "either" method, ranger determines which program to
-# pick by looking at its dependencies.  You can set dependencies by
-# adding the decorator "depends_on":
-#     @depends_on("vim")
-#     def app_vim(self, context):
-#         ....
-# There is a special keyword which you can use as a dependence: "X"
-# This ensures that the program will only run when X is running.
-# ===================================================================
-
-import ranger
-from ranger.api.apps import *
-from ranger.ext.get_executables import get_executables
-
-class CustomApplications(Applications):
-	def app_default(self, c):
-		"""How to determine the default application?"""
-		f = c.file
-
-		if f.basename.lower() == 'makefile' and c.mode == 1:
-			made = self.either(c, 'make')
-			if made: return made
-
-		if f.extension is not None:
-			if f.extension in ('pdf', ):
-				return self.either(c, 'llpp', 'zathura', 'mupdf', 'apvlv',
-						'evince', 'okular', 'epdfview')
-			if f.extension == 'djvu':
-				return self.either(c, 'evince')
-			if f.extension in ('xml', 'csv'):
-				return self.either(c, 'editor')
-			if f.extension == 'mid':
-				return self.either(c, 'wildmidi')
-			if f.extension in ('html', 'htm', 'xhtml') or f.extension == 'swf':
-				c.flags += 'd'
-				handler = self.either(c,
-						'luakit', 'uzbl', 'vimprobable', 'vimprobable2', 'jumanji',
-						'firefox', 'seamonkey', 'iceweasel', 'opera',
-						'surf', 'midori', 'epiphany', 'konqueror')
-				# Only return if some program was found:
-				if handler:
-					return handler
-			if f.extension in ('html', 'htm', 'xhtml'):
-				# These browsers can't handle flash, so they're not called above.
-				c.flags += 'D'
-				return self.either(c, 'elinks', 'links', 'links2', 'lynx', 'w3m')
-			if f.extension == 'nes':
-				return self.either(c, 'fceux')
-			if f.extension in ('swc', 'smc', 'sfc'):
-				return self.either(c, 'zsnes')
-			if f.extension == 'doc':
-				return self.either(c, 'abiword', 'libreoffice',
-						'soffice', 'ooffice')
-			if f.extension in ('odt', 'ods', 'odp', 'odf', 'odg', 'odb', 'sxc',
-					'stc', 'xls', 'xlsx', 'xlt', 'xlw', 'gnm', 'gnumeric'):
-				return self.either(c, 'gnumeric', 'kspread',
-						'libreoffice', 'soffice', 'ooffice')
-
-		if f.mimetype is not None:
-			if INTERPRETED_LANGUAGES.match(f.mimetype):
-				return self.either(c, 'edit_or_run')
-
-		if f.container:
-			return self.either(c, 'aunpack', 'file_roller')
-
-		if f.video or f.audio:
-			if f.video:
-				c.flags += 'd'
-			return self.either(c, 'smplayer', 'gmplayer', 'mplayer2',
-					'mplayer', 'vlc', 'totem')
-
-		if f.image:
-			if c.mode in (11, 12, 13, 14):
-				return self.either(c, 'set_bg_with_feh')
-			else:
-				return self.either(c, 'sxiv', 'feh', 'eog', 'mirage')
-
-		if f.document or f.filetype.startswith('text') or f.size == 0:
-			return self.either(c, 'editor')
-
-		# You can put this at the top of the function and mimeopen will
-		# always be used for every file.
-		return self.either(c, 'mimeopen')
-
-
-	# ----------------------------------------- application definitions
-	# Note: Trivial application definitions are at the bottom
-	def app_pager(self, c):
-		return 'less', '-R', c
-
-	def app_editor(self, c):
-		try:
-			default_editor = os.environ['EDITOR']
-		except KeyError:
-			pass
-		else:
-			parts = default_editor.split()
-			exe_name = os.path.basename(parts[0])
-			if exe_name in get_executables():
-				return tuple(parts) + tuple(c)
-
-		return self.either(c, 'vim', 'emacs', 'nano')
-
-	def app_edit_or_run(self, c):
-		if c.mode is 1:
-			return self.app_self(c)
-		return self.app_editor(c)
-
-	@depends_on('mplayer')
-	def app_mplayer(self, c):
-		if c.mode is 1:
-			return 'mplayer', '-fs', c
-
-		elif c.mode is 2:
-			args = "mplayer -fs -sid 0 -vfm ffmpeg -lavdopts " \
-					"lowres=1:fast:skiploopfilter=all:threads=8".split()
-			args.extend(c)
-			return args
-
-		elif c.mode is 3:
-			return 'mplayer', '-mixer', 'software', c
-
-		else:
-			return 'mplayer', c
-
-	@depends_on('mplayer2')
-	def app_mplayer2(self, c):
-		args = list(self.app_mplayer(c))
-		args[0] += '2'
-		return args
-
-	# A dependence on "X" means: this programs requires a running X server!
-	@depends_on('feh', 'X')
-	def app_set_bg_with_feh(self, c):
-		c.flags += 'd'
-		arg = {11: '--bg-scale', 12: '--bg-tile', 13: '--bg-center',
-				14: '--bg-fill'}
-		if c.mode in arg:
-			return 'feh', arg[c.mode], c.file.path
-		return 'feh', arg[11], c.file.path
-
-	@depends_on('feh', 'X')
-	def app_feh(self, c):
-		c.flags += 'd'
-		if c.mode is 0 and len(c.files) is 1 and self.fm.env.cwd:
-			# view all files in the cwd
-			images = [f.basename for f in self.fm.env.cwd.files if f.image]
-			return 'feh', '--start-at', c.file.basename, images
-		return 'feh', c
-
-	@depends_on('sxiv', 'X')
-	def app_sxiv(self, c):
-		c.flags = 'd' + c.flags
-		if len(c.files) is 1 and self.fm.env.cwd:
-			images = [f.basename for f in self.fm.env.cwd.files if f.image]
-			try:
-				position = images.index(c.file.basename) + 1
-			except:
-				return None
-			return 'sxiv', '-n', str(position), images
-		return 'sxiv', c
-
-	@depends_on('aunpack')
-	def app_aunpack(self, c):
-		if c.mode is 0:
-			c.flags += 'p'
-			return 'aunpack', '-l', c.file.path
-		return 'aunpack', c.file.path
-
-	@depends_on('file-roller', 'X')
-	def app_file_roller(self, c):
-		c.flags += 'd'
-		return 'file-roller', c.file.path
-
-	@depends_on('make')
-	def app_make(self, c):
-		if c.mode is 0:
-			return "make"
-		if c.mode is 1:
-			return "make", "install"
-		if c.mode is 2:
-			return "make", "clear"
-
-	@depends_on('java')
-	def app_java(self, c):
-		def strip_extensions(file):
-			if '.' in file.basename:
-				return file.path[:file.path.index('.')]
-			return file.path
-		files_without_extensions = map(strip_extensions, c.files)
-		return "java", files_without_extensions
-
-	@depends_on('totem', 'X')
-	def app_totem(self, c):
-		if c.mode is 0:
-			return "totem", c
-		if c.mode is 1:
-			return "totem", "--fullscreen", c
-
-	@depends_on('mimeopen')
-	def app_mimeopen(self, c):
-		if c.mode is 0:
-			return "mimeopen", c
-		if c.mode is 1: 
-			# Will ask user to select program
-			# aka "Open with..."
-			return "mimeopen", "--ask", c
-
-
-# Often a programs invocation is trivial.  For example:
-#    vim test.py readme.txt [...]
-#
-# This could be implemented like:
-#    @depends_on("vim")
-#    def app_vim(self, context):
-#        return "vim", context
-#
-# But this is redundant and ranger does this automatically.  However, sometimes
-# you want to change some properties like flags or dependencies.
-# The method "generic" defines a generic method for the given programs which
-# looks like the one above, but you can add dependencies and flags here.
-# Add programs (that are not defined yet) here if they should only run in X:
-CustomApplications.generic('fceux', 'wine', 'zsnes', deps=['X'])
-
-# Add those which should only run in X AND should be detached/forked here:
-CustomApplications.generic(
-	'luakit', 'uzbl', 'vimprobable', 'vimprobable2', 'jumanji',
-	'firefox', 'seamonkey', 'iceweasel', 'opera',
-	'surf', 'midori', 'epiphany', 'konqueror',
-	'evince', 'zathura', 'apvlv', 'okular', 'epdfview', 'mupdf', 'llpp',
-	'eog', 'mirage', 'gimp',
-	'libreoffice', 'soffice', 'ooffice', 'gnumeric', 'kspread', 'abiword',
-	'gmplayer', 'smplayer', 'vlc',
-			flags='d', deps=['X'])
-
-# What filetypes are recognized as scripts for interpreted languages?
-# This regular expression is used in app_default()
-INTERPRETED_LANGUAGES = re.compile(r'''
-	^(text|application)/x-(
-		haskell|perl|python|ruby|sh
-	)$''', re.VERBOSE)
diff --git a/ranger/ext/accumulator.py b/ranger/ext/accumulator.py
index d68bc656..7665c99f 100644
--- a/ranger/ext/accumulator.py
+++ b/ranger/ext/accumulator.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 from ranger.ext.direction import Direction
 
diff --git a/ranger/ext/cached_function.py b/ranger/ext/cached_function.py
index 4d9ded18..ad7c5c11 100644
--- a/ranger/ext/cached_function.py
+++ b/ranger/ext/cached_function.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2012  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 def cached_function(fnc):
   cache = {}
diff --git a/ranger/ext/curses_interrupt_handler.py b/ranger/ext/curses_interrupt_handler.py
index 7c5b153e..eb86e0a9 100644
--- a/ranger/ext/curses_interrupt_handler.py
+++ b/ranger/ext/curses_interrupt_handler.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 """
 This module can catch interrupt signals which would otherwise
diff --git a/ranger/ext/direction.py b/ranger/ext/direction.py
index 8a37c54a..742f54da 100644
--- a/ranger/ext/direction.py
+++ b/ranger/ext/direction.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 """
 Directions provide convenient methods for movement operations.
diff --git a/ranger/ext/get_executables.py b/ranger/ext/get_executables.py
index 03f438dd..0c01ffd6 100644
--- a/ranger/ext/get_executables.py
+++ b/ranger/ext/get_executables.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 from stat import S_IXOTH, S_IFREG
 from ranger.ext.iter_tools import unique
@@ -23,13 +11,11 @@ _cached_executables = None
 
 def get_executables():
 	"""
-	Return all executable files in each of the given directories.
-
-	Looks in $PATH by default.
+	Return all executable files in $PATH. Cached version.
 	"""
 	global _cached_executables
 	if _cached_executables is None:
-		_cached_executables = sorted(get_executables_uncached())
+		_cached_executables = get_executables_uncached()
 	return _cached_executables
 
 
diff --git a/ranger/ext/human_readable.py b/ranger/ext/human_readable.py
index 1a667519..9667421a 100644
--- a/ranger/ext/human_readable.py
+++ b/ranger/ext/human_readable.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 def human_readable(byte, separator=' '):
 	"""
diff --git a/ranger/ext/iter_tools.py b/ranger/ext/iter_tools.py
index e515fa07..341230f4 100644
--- a/ranger/ext/iter_tools.py
+++ b/ranger/ext/iter_tools.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 from collections import deque
 
diff --git a/ranger/ext/keybinding_parser.py b/ranger/ext/keybinding_parser.py
index 295855fb..eed85a2b 100644
--- a/ranger/ext/keybinding_parser.py
+++ b/ranger/ext/keybinding_parser.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 import sys
 import copy
diff --git a/ranger/ext/mount_path.py b/ranger/ext/mount_path.py
index 31d6c602..19747eba 100644
--- a/ranger/ext/mount_path.py
+++ b/ranger/ext/mount_path.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 from os.path import realpath, abspath, dirname, ismount
 
diff --git a/ranger/ext/next_available_filename.py b/ranger/ext/next_available_filename.py
index 696063cf..705e1558 100644
--- a/ranger/ext/next_available_filename.py
+++ b/ranger/ext/next_available_filename.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 import os.path
 
diff --git a/ranger/ext/openstruct.py b/ranger/ext/openstruct.py
index b538d384..fa521d34 100644
--- a/ranger/ext/openstruct.py
+++ b/ranger/ext/openstruct.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 # prepend __ to arguments because one might use "args"
 # or "keywords" as a keyword argument.
diff --git a/ranger/ext/relative_symlink.py b/ranger/ext/relative_symlink.py
index 420f186c..de1cb908 100644
--- a/ranger/ext/relative_symlink.py
+++ b/ranger/ext/relative_symlink.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 from os import symlink, sep
 
diff --git a/ranger/ext/rifle.py b/ranger/ext/rifle.py
new file mode 100755
index 00000000..feef4cac
--- /dev/null
+++ b/ranger/ext/rifle.py
@@ -0,0 +1,420 @@
+#!/usr/bin/python -S
+# Copyright (C) 2012  Roman Zimbelmann <romanz@lavabit.com>
+# This software is distributed under the terms of the GNU GPL version 3.
+
+"""
+rifle, the file executor/opener of ranger
+
+This can be used as a standalone program or can be embedded in python code.
+When used together with ranger, it doesn't have to be installed to $PATH.
+
+Example usage:
+
+	rifle = Rifle("rilfe.conf")
+	rifle.reload_config()
+	rifle.execute(["file1", "file2"])
+"""
+
+import os.path
+import re
+from subprocess import Popen, PIPE
+import sys
+
+DEFAULT_PAGER = 'less'
+DEFAULT_EDITOR = 'nano'
+ASK_COMMAND = 'ask'
+ENCODING = 'utf-8'
+
+try:
+	from ranger.ext.get_executables import get_executables
+except ImportError:
+	_cached_executables = None
+
+	def get_executables():
+		"""
+		Return all executable files in $PATH + Cache them.
+		"""
+		global _cached_executables
+		if _cached_executables is not None:
+			return _cached_executables
+
+		if 'PATH' in os.environ:
+			paths = os.environ['PATH'].split(':')
+		else:
+			paths = ['/usr/bin', '/bin']
+
+		from stat import S_IXOTH, S_IFREG
+		paths_seen = set()
+		_cached_executables = set()
+		for path in paths:
+			if path in paths_seen:
+				continue
+			paths_seen.add(path)
+			try:
+				content = os.listdir(path)
+			except OSError:
+				continue
+			for item in content:
+				abspath = path + '/' + item
+				try:
+					filestat = os.stat(abspath)
+				except OSError:
+					continue
+				if filestat.st_mode & (S_IXOTH | S_IFREG):
+					_cached_executables.add(item)
+		return _cached_executables
+
+
+def _is_terminal():
+	# Check if stdin (file descriptor 0), stdout (fd 1) and
+	# stderr (fd 2) are connected to a terminal
+	try:
+		os.ttyname(0)
+		os.ttyname(1)
+		os.ttyname(2)
+	except:
+		return False
+	return True
+
+
+def squash_flags(flags):
+	"""
+	Remove lowercase flags if the respective uppercase flag exists
+
+	>>> squash_flags('abc')
+	'abc'
+	>>> squash_flags('abcC')
+	'ab'
+	>>> squash_flags('CabcAd')
+	'bd'
+	"""
+	exclude = ''.join(f.upper() + f.lower() for f in flags if f == f.upper())
+	return ''.join(f for f in flags if f not in exclude)
+
+
+class Rifle(object):
+	delimiter1 = '='
+	delimiter2 = ','
+
+	def hook_before_executing(self, command, mimetype, flags):
+		pass
+
+	def hook_after_executing(self, command, mimetype, flags):
+		pass
+
+	def hook_command_preprocessing(self, command):
+		return command
+
+	def hook_command_postprocessing(self, command):
+		return command
+
+	def hook_environment(self, env):
+		return env
+
+	def hook_logger(self, string):
+		sys.stderr.write(string + "\n")
+
+	def __init__(self, config_file):
+		self.config_file = config_file
+		self._app_flags = ''
+		self._app_label = None
+
+	def reload_config(self, config_file=None):
+		"""Replace the current configuration with the one in config_file"""
+		if config_file is None:
+			config_file = self.config_file
+		f = open(config_file, 'r')
+		self.rules = []
+		lineno = 1
+		for line in f:
+			if line.startswith('#') or line == '\n':
+				continue
+			line = line.strip()
+			try:
+				if self.delimiter1 not in line:
+					raise Exception("Line without delimiter")
+				tests, command = line.split(self.delimiter1, 1)
+				tests = tests.split(self.delimiter2)
+				tests = tuple(tuple(f.strip().split(None, 1)) for f in tests)
+				command = command.strip()
+				self.rules.append((command, tests))
+			except Exception as e:
+				self.hook_logger("Syntax error in %s line %d (%s)" % \
+					(config_file, lineno, str(e)))
+			lineno += 1
+		f.close()
+
+	def _eval_condition(self, condition, files, label):
+		# Handle the negation of conditions starting with an exclamation mark,
+		# then pass on the arguments to _eval_condition2().
+
+		if not condition:
+			return True
+		if condition[0].startswith('!'):
+			new_condition = tuple([condition[0][1:]]) + tuple(condition[1:])
+			return not self._eval_condition2(new_condition, files, label)
+		return self._eval_condition2(condition, files, label)
+
+	def _eval_condition2(self, rule, files, label):
+		# This function evaluates the condition, after _eval_condition() handled
+		# negation of conditions starting with a "!".
+
+		if not files:
+			return False
+
+		function = rule[0]
+		argument = rule[1] if len(rule) > 1 else ''
+
+		if function == 'ext':
+			extension = os.path.basename(files[0]).rsplit('.', 1)[-1].lower()
+			return bool(re.search('^(' + argument + ')$', extension))
+		elif function == 'name':
+			return bool(re.search(argument, os.path.basename(files[0])))
+		elif function == 'match':
+			return bool(re.search(argument, files[0]))
+		elif function == 'file':
+			return os.path.isfile(files[0])
+		elif function == 'directory':
+			return os.path.isdir(files[0])
+		elif function == 'path':
+			return bool(re.search(argument, os.path.abspath(files[0])))
+		elif function == 'mime':
+			return bool(re.search(argument, self._get_mimetype(files[0])))
+		elif function == 'has':
+			return argument in get_executables()
+		elif function == 'terminal':
+			return _is_terminal()
+		elif function == 'number':
+			if argument.isdigit():
+				self._skip = int(argument)
+			return True
+		elif function == 'label':
+			self._app_label = argument
+			if label:
+				return argument == label
+			return True
+		elif function == 'flag':
+			self._app_flags = argument
+			return True
+		elif function == 'X':
+			return 'DISPLAY' in os.environ
+		elif function == 'else':
+			return True
+
+	def _get_mimetype(self, fname):
+		# Spawn "file" to determine the mime-type of the given file.
+		if self._mimetype:
+			return self._mimetype
+		process = Popen(["file", "--mime-type", "-Lb", fname],
+				stdout=PIPE, stderr=PIPE)
+		mimetype, _ = process.communicate()
+		self._mimetype = mimetype.decode(ENCODING).strip()
+		return self._mimetype
+
+	def _build_command(self, files, action, flags):
+		# Get the flags
+		if isinstance(flags, str):
+			self._app_flags += flags
+		self._app_flags = squash_flags(self._app_flags)
+		flags = self._app_flags
+
+		_filenames = "' '".join(f.replace("'", "'\\\''") for f in files
+				if "\x00" not in f)
+		command = "set -- '%s'" % _filenames + '\n'
+
+		# Apply flags
+		command += action
+		return command
+
+	def _apply_flags(self, action, flags):
+		# FIXME: Flags do not work properly when pipes are in the command.
+		# NOTE: "r" flag is handled in execute()
+		if 't' in flags:
+			if 'TERMCMD' not in os.environ:
+				term = os.environ['TERM']
+				if term.startswith('rxvt-unicode'):
+					term = 'urxvt'
+				if term not in get_executables():
+					self.hook_logger("Can not determine terminal command.  "
+						"Please set $TERMCMD manually.")
+				os.environ['TERMCMD'] = term
+			action = "$TERMCMD -e %s" % action
+		if 'f' in flags:
+			if 'setsid' in get_executables():
+				action = "setsid %s > /dev/null 2> /dev/null &" % action
+			else:
+				action = "nohup %s > /dev/null 2> /dev/null &" % action
+		return action
+
+	def list_commands(self, files, mimetype=None):
+		"""
+		Returns one 4-tuple for all currently applicable commands
+		The 4-tuple contains (count, command, label, flags).
+		count is the index, counted from 0 upwards,
+		command is the command that will be executed.
+		label and flags are the label and flags specified in the rule.
+		"""
+		self._mimetype = mimetype
+		count = -1
+		for cmd, tests in self.rules:
+			self._skip = None
+			self._app_flags = ''
+			self._app_label = None
+			for test in tests:
+				if not self._eval_condition(test, files, None):
+					break
+			else:
+				if self._skip is None:
+					count += 1
+				else:
+					count = self._skip
+				yield (count, cmd, self._app_label, self._app_flags)
+
+	def execute(self, files, number=0, label=None, flags="", mimetype=None):
+		"""
+		Executes the given list of files.
+
+		By default, this executes the first command where all conditions apply,
+		but by specifying number=N you can run the 1+Nth command.
+
+		If a label is specified, only rules with this label will be considered.
+
+		If you specify the mimetype, rifle will not try to determine it itself.
+
+		By specifying a flag, you extend the flag that is defined in the rule.
+		Uppercase flags negate the respective lowercase flags.
+		For example: if the flag in the rule is "pw" and you specify "Pf", then
+		the "p" flag is negated and the "f" flag is added, resulting in "wf".
+		"""
+		command = None
+		found_at_least_one = None
+
+		# Determine command
+		for count, cmd, lbl, flgs in self.list_commands(files, mimetype):
+			if label and label == lbl or not label and count == number:
+				cmd = self.hook_command_preprocessing(cmd)
+				if cmd == ASK_COMMAND:
+					return ASK_COMMAND
+				command = self._build_command(files, cmd, flags + flgs)
+				flags = self._app_flags
+				break
+			else:
+				found_at_least_one = True
+		else:
+			if label and label in get_executables():
+				cmd = '%s "$@"' % label
+				command = self._build_command(files, cmd, flags)
+
+		# Execute command
+		if command is None:
+			if found_at_least_one:
+				if label:
+					self.hook_logger("Label '%s' is undefined" % label)
+				else:
+					self.hook_logger("Method number %d is undefined." % number)
+			else:
+				self.hook_logger("No action found.")
+		else:
+			if 'PAGER' not in os.environ:
+				os.environ['PAGER'] = DEFAULT_PAGER
+			if 'EDITOR' not in os.environ:
+				os.environ['EDITOR'] = DEFAULT_EDITOR
+			command = self.hook_command_postprocessing(command)
+			self.hook_before_executing(command, self._mimetype, self._app_flags)
+			try:
+				if 'r' in flags:
+					prefix = ['sudo', '-E', 'su', '-mc']
+				else:
+					prefix = ['/bin/sh', '-c']
+				if 't' in flags:
+					if 'TERMCMD' not in os.environ:
+						term = os.environ['TERM']
+						if term.startswith('rxvt-unicode'):
+							term = 'urxvt'
+						if term not in get_executables():
+							self.hook_logger("Can not determine terminal command.  "
+								"Please set $TERMCMD manually.")
+						os.environ['TERMCMD'] = term
+					cmd = [os.environ['TERMCMD'], '-e'] + prefix + [command]
+				elif 'f' in flags:
+					exe = 'setsid' if 'setsid' in get_executables() else 'nohup'
+					cmd = [exe] + prefix + [command]
+				else:
+					cmd = prefix + [command]
+				if 'f' in flags or 't' in flags:
+					devnull_r = open(os.devnull, 'r')
+					devnull_w = open(os.devnull, 'w')
+					p = Popen(cmd, env=self.hook_environment(os.environ),
+						stdin=devnull_r, stdout=devnull_w, stderr=devnull_w)
+				else:
+					p = Popen(cmd, env=self.hook_environment(os.environ))
+					p.wait()
+			finally:
+				self.hook_after_executing(command, self._mimetype, self._app_flags)
+
+
+def main():
+	"""The main function which is run when you start this program direectly."""
+	import sys
+
+	# Find configuration file path
+	if 'XDG_CONFIG_HOME' in os.environ and os.environ['XDG_CONFIG_HOME']:
+		conf_path = os.environ['XDG_CONFIG_HOME'] + '/ranger/rifle.conf'
+	else:
+		conf_path = os.path.expanduser('~/.config/ranger/rifle.conf')
+	if not os.path.isfile(conf_path):
+		conf_path = os.path.normpath(os.path.join(os.path.dirname(__file__),
+			'../config/rifle.conf'))
+
+	# Evaluate arguments
+	from optparse import OptionParser
+	parser = OptionParser(usage="%prog [-fhlpw] [files]")
+	parser.add_option('-f', type="string", default="", metavar="FLAGS",
+			help="use additional flags: f=fork, r=root, t=terminal. "
+			"Uppercase flag negates respective lowercase flags.")
+	parser.add_option('-l', action="store_true",
+			help="list possible ways to open the files (id:label:flags:command)")
+	parser.add_option('-p', type='string', default='0', metavar="KEYWORD",
+			help="pick a method to open the files.  KEYWORD is either the "
+			"number listed by 'rifle -l' or a string that matches a label in "
+			"the configuration file")
+	parser.add_option('-w', type='string', default=None, metavar="PROGRAM",
+			help="open the files with PROGRAM")
+	options, positional = parser.parse_args()
+	if not positional:
+		parser.print_help()
+		raise SystemExit(1)
+
+	if options.p.isdigit():
+		number = int(options.p)
+		label = None
+	else:
+		number = 0
+		label = options.p
+
+	if options.w is not None and not options.l:
+		p = Popen([options.w] + list(positional))
+		p.wait()
+	else:
+		# Start up rifle
+		rifle = Rifle(conf_path)
+		rifle.reload_config()
+		#print(rifle.list_commands(sys.argv[1:]))
+		if options.l:
+			for count, cmd, label, flags in rifle.list_commands(positional):
+				print("%d:%s:%s:%s" % (count, label or '', flags, cmd))
+		else:
+			result = rifle.execute(positional, number=number, label=label,
+					flags=options.f)
+			if result == ASK_COMMAND:
+				# TODO: implement interactive asking for file type?
+				print("Unknown file type: %s" % rifle._get_mimetype(positional[0]))
+
+
+
+if __name__ == '__main__':
+	if 'RANGER_DOCTEST' in os.environ:
+		import doctest
+		doctest.testmod()
+	else:
+		main()
diff --git a/ranger/ext/shell_escape.py b/ranger/ext/shell_escape.py
index b68afc33..c9a22074 100644
--- a/ranger/ext/shell_escape.py
+++ b/ranger/ext/shell_escape.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 """
 Functions to escape metacharacters of arguments for shell commands.
diff --git a/ranger/ext/signals.py b/ranger/ext/signals.py
index 0df39fe0..c3e546e3 100644
--- a/ranger/ext/signals.py
+++ b/ranger/ext/signals.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 """
 An efficient and minimalistic signaling/hook module.
diff --git a/ranger/ext/spawn.py b/ranger/ext/spawn.py
index 5e5d6a95..0f3a629a 100644
--- a/ranger/ext/spawn.py
+++ b/ranger/ext/spawn.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 from subprocess import Popen, PIPE
 ENCODING = 'utf-8'
diff --git a/ranger/ext/widestring.py b/ranger/ext/widestring.py
index 525e1bc1..44972c9c 100644
--- a/ranger/ext/widestring.py
+++ b/ranger/ext/widestring.py
@@ -1,18 +1,6 @@
 # -*- encoding: utf8 -*-
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 import sys
 from unicodedata import east_asian_width
diff --git a/ranger/fsobject/directory.py b/ranger/fsobject/directory.py
index 81e50ed9..823eaeaf 100644
--- a/ranger/fsobject/directory.py
+++ b/ranger/fsobject/directory.py
@@ -1,19 +1,8 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 import os.path
+import re
 from os import stat as os_stat, lstat as os_lstat
 from collections import deque
 from time import time
@@ -95,6 +84,7 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware):
 	def __init__(self, path, **kw):
 		assert not os.path.isfile(path), "No directory given!"
 
+		Loadable.__init__(self, None, None)
 		Accumulator.__init__(self)
 		FileSystemObject.__init__(self, path, **kw)
 
@@ -176,6 +166,7 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware):
 		"""
 
 		self.loading = True
+		self.percent = 0
 		self.load_if_outdated()
 
 		try:
@@ -185,8 +176,17 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware):
 
 				self.mount_path = mount_path(mypath)
 
-				hidden_filter = not self.settings.show_hidden \
-						and self.settings.hidden_filter
+				if not self.settings.show_hidden and self.settings.hidden_filter:
+					# COMPAT
+					# hidden_filter used to be a regex, not a string.  If an
+					# old config is used, we don't need to re.compile it.
+					if hasattr(self.settings.hidden_filter, 'search'):
+						hidden_filter = self.settings.hidden_filter
+					else:
+						hidden_filter = re.compile(self.settings.hidden_filter)
+				else:
+					hidden_filter = None
+
 				filelist = os.listdir(mypath)
 
 				if self._cumulative_size_calculated:
@@ -232,7 +232,7 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware):
 						is_a_dir = False
 					if is_a_dir:
 						try:
-							item = self.fm.env.get_directory(name)
+							item = self.fm.get_directory(name)
 							item.load_if_outdated()
 						except:
 							item = Directory(name, preload=stats,
@@ -243,6 +243,7 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware):
 						item.load()
 						disk_usage += item.size
 					files.append(item)
+					self.percent = 100 * len(files) // len(filenames)
 					yield
 				self.disk_usage = disk_usage
 
@@ -456,8 +457,8 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware):
 		Accumulator.correct_pointer(self)
 
 		try:
-			if self == self.fm.env.cwd:
-				self.fm.env.cf = self.pointed_obj
+			if self == self.fm.thisdir:
+				self.fm.thisfile = self.pointed_obj
 		except:
 			pass
 
diff --git a/ranger/fsobject/file.py b/ranger/fsobject/file.py
index 66bc549b..39146cd5 100644
--- a/ranger/fsobject/file.py
+++ b/ranger/fsobject/file.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 import re
 from ranger.fsobject import FileSystemObject
diff --git a/ranger/fsobject/fsobject.py b/ranger/fsobject/fsobject.py
index 943c8aa4..073da74f 100644
--- a/ranger/fsobject/fsobject.py
+++ b/ranger/fsobject/fsobject.py
@@ -1,21 +1,14 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 CONTAINER_EXTENSIONS = ('7z', 'ace', 'ar', 'arc', 'bz', 'bz2', 'cab', 'cpio',
 	'cpt', 'deb', 'dgc', 'dmg', 'gz', 'iso', 'jar', 'msi', 'pkg', 'rar',
 	'shar', 'tar', 'tbz', 'tgz', 'xar', 'xpi', 'xz', 'zip')
+DOCUMENT_EXTENSIONS = ('cfg', 'css', 'cvs', 'djvu', 'doc', 'docx', 'gnm',
+	'gnumeric', 'htm', 'html', 'md', 'odf', 'odg', 'odp', 'ods', 'odt', 'pdf',
+	'pod', 'ps', 'rtf', 'sxc', 'txt', 'xls', 'xlw', 'xml', 'xslx')
+DOCUMENT_BASENAMES = ('bugs', 'bugs', 'changelog', 'copying', 'credits',
+	'hacking', 'help', 'install', 'license', 'readme', 'todo')
 
 import re
 from os import lstat, stat
@@ -84,6 +77,7 @@ class FileSystemObject(FileManagerAware):
 		self.extension = splitext(self.basename)[1].lstrip(extsep) or None
 		self.dirname = dirname(path)
 		self.preload = preload
+		self.display_data = {}
 
 		try:
 			lastdot = self.basename.rindex('.') + 1
@@ -147,7 +141,9 @@ class FileSystemObject(FileManagerAware):
 		self.image = self._mimetype.startswith('image')
 		self.audio = self._mimetype.startswith('audio')
 		self.media = self.video or self.image or self.audio
-		self.document = self._mimetype.startswith('text')
+		self.document = self._mimetype.startswith('text') \
+				or self.extension in DOCUMENT_EXTENSIONS \
+				or self.basename.lower() in DOCUMENT_BASENAMES
 		self.container = self.extension in CONTAINER_EXTENSIONS
 
 		keys = ('video', 'audio', 'image', 'media', 'document', 'container')
@@ -173,7 +169,7 @@ class FileSystemObject(FileManagerAware):
 			return self._mimetype_tuple
 
 	def mark(self, boolean):
-		directory = self.env.get_directory(self.dirname)
+		directory = self.fm.get_directory(self.dirname)
 		directory.mark_item(self)
 
 	def _mark(self, boolean):
@@ -195,6 +191,7 @@ class FileSystemObject(FileManagerAware):
 		filesystem and caches it for later use
 		"""
 
+		self.display_data = {}
 		self.fm.update_preview(self.path)
 		self.loaded = True
 
diff --git a/ranger/gui/ansi.py b/ranger/gui/ansi.py
index 65038120..96b95669 100644
--- a/ranger/gui/ansi.py
+++ b/ranger/gui/ansi.py
@@ -1,18 +1,6 @@
 # Copyright (C) 2010 David Barnett <davidbarnett2@gmail.com>
 # Copyright (C) 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 """
 A library to help to convert ANSI codes to curses instructions.
diff --git a/ranger/gui/bar.py b/ranger/gui/bar.py
index 40218fb1..ae07dd35 100644
--- a/ranger/gui/bar.py
+++ b/ranger/gui/bar.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 from ranger.ext.widestring import WideString, utf_char_width
 import sys
diff --git a/ranger/gui/color.py b/ranger/gui/color.py
index 889f9e9a..037ff5c1 100644
--- a/ranger/gui/color.py
+++ b/ranger/gui/color.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 """
 Contains abbreviations to curses color/attribute constants.
diff --git a/ranger/gui/colorscheme.py b/ranger/gui/colorscheme.py
index cc72d6a8..b36048e2 100644
--- a/ranger/gui/colorscheme.py
+++ b/ranger/gui/colorscheme.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 """
 Colorschemes define colors for specific contexts.
@@ -44,7 +32,7 @@ from curses import color_pair
 import ranger
 from ranger.gui.color import get_color
 from ranger.gui.context import Context
-from ranger.core.helper import allow_access_to_confdir
+from ranger.core.main import allow_access_to_confdir
 from ranger.core.shared import SettingsAware
 from ranger.ext.cached_function import cached_function
 from ranger.ext.iter_tools import flatten
diff --git a/ranger/gui/context.py b/ranger/gui/context.py
index 3e7ca505..cdccecde 100644
--- a/ranger/gui/context.py
+++ b/ranger/gui/context.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 CONTEXT_KEYS = ['reset', 'error', 'badinfo',
 		'in_browser', 'in_statusbar', 'in_titlebar', 'in_console',
@@ -26,7 +14,7 @@ CONTEXT_KEYS = ['reset', 'error', 'badinfo',
 		'marked', 'tagged', 'tag_marker', 'cut', 'copied',
 		'help_markup', # COMPAT
 		'seperator', 'key', 'special', 'border', # COMPAT
-		'title', 'text', 'highlight', 'bars', 'quotes', 'tab',
+		'title', 'text', 'highlight', 'bars', 'quotes', 'tab', 'loaded',
 		'keybuffer']
 
 class Context(object):
diff --git a/ranger/gui/curses_shortcuts.py b/ranger/gui/curses_shortcuts.py
index a383ab4c..43b583a6 100644
--- a/ranger/gui/curses_shortcuts.py
+++ b/ranger/gui/curses_shortcuts.py
@@ -1,18 +1,6 @@
 # Copyright (C) 2009, 2010, 2011  Roman Zimbelmann <romanz@lavabit.com>
 # Copyright (C) 2010 David Barnett <davidbarnett2@gmail.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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 import curses
 import _curses
diff --git a/ranger/gui/displayable.py b/ranger/gui/displayable.py
index 5e9562a7..cbd77fe4 100644
--- a/ranger/gui/displayable.py
+++ b/ranger/gui/displayable.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 from ranger.core.shared import FileManagerAware, EnvironmentAware
 from ranger.gui.curses_shortcuts import CursesShortcuts
@@ -49,7 +37,7 @@ class Displayable(EnvironmentAware, FileManagerAware, CursesShortcuts):
 		win -- the own curses window object
 		parent -- the parent (DisplayableContainer) object or None
 		x, y, wid, hei -- absolute coordinates and boundaries
-		settings, fm, env -- inherited shared variables
+		settings, fm -- inherited shared variables
 	"""
 
 	def __init__(self, win, env=None, fm=None, settings=None):
@@ -154,7 +142,7 @@ class Displayable(EnvironmentAware, FileManagerAware, CursesShortcuts):
 		"""Resize the widget"""
 		do_move = True
 		try:
-			maxy, maxx = self.env.termsize
+			maxy, maxx = self.fm.ui.termsize
 		except TypeError:
 			pass
 		else:
diff --git a/ranger/gui/mouse_event.py b/ranger/gui/mouse_event.py
index cb697d8d..ed370e54 100644
--- a/ranger/gui/mouse_event.py
+++ b/ranger/gui/mouse_event.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 import curses
 
diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py
index 69b8463b..6f021b68 100644
--- a/ranger/gui/ui.py
+++ b/ranger/gui/ui.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 import os
 import sys
@@ -20,7 +8,7 @@ import _curses
 
 from .displayable import DisplayableContainer
 from .mouse_event import MouseEvent
-from ranger.ext.keybinding_parser import ALT_KEY
+from ranger.ext.keybinding_parser import KeyBuffer, KeyMaps, ALT_KEY
 
 TERMINALS_WITH_TITLE = ("xterm", "xterm-256color", "rxvt",
 		"rxvt-256color", "rxvt-unicode", "rxvt-unicode-256color",
@@ -53,23 +41,25 @@ class UI(DisplayableContainer):
 	is_set_up = False
 	load_mode = False
 	is_on = False
+	termsize = None
+
 	def __init__(self, env=None, fm=None):
 		self._draw_title = os.environ["TERM"] in TERMINALS_WITH_TITLE
-		os.environ['ESCDELAY'] = '25'   # don't know a cleaner way
+		self.keybuffer = KeyBuffer()
+		self.keymaps = KeyMaps(self.keybuffer)
 
-		if env is not None:
-			self.env = env
 		if fm is not None:
 			self.fm = fm
 
+	def setup_curses(self):
+		os.environ['ESCDELAY'] = '25'   # don't know a cleaner way
 		try:
 			self.win = curses.initscr()
 		except _curses.error as e:
 			if e.args[0] == "setupterm: could not find terminal":
 				os.environ['TERM'] = 'linux'
 				self.win = curses.initscr()
-		self.env.keymaps.use_keymap('browser')
-
+		self.keymaps.use_keymap('browser')
 		DisplayableContainer.__init__(self, None)
 
 	def initialize(self):
@@ -147,14 +137,14 @@ class UI(DisplayableContainer):
 			self.hint()
 
 		if key < 0:
-			self.env.keybuffer.clear()
+			self.keybuffer.clear()
 
 		elif not DisplayableContainer.press(self, key):
-			self.env.keymaps.use_keymap('browser')
+			self.keymaps.use_keymap('browser')
 			self.press(key)
 
 	def press(self, key):
-		keybuffer = self.env.keybuffer
+		keybuffer = self.keybuffer
 		self.status.clear_message()
 
 		keybuffer.add(key)
@@ -271,8 +261,8 @@ class UI(DisplayableContainer):
 
 	def update_size(self):
 		"""resize all widgets"""
-		self.env.termsize = self.win.getmaxyx()
-		y, x = self.env.termsize
+		self.termsize = self.win.getmaxyx()
+		y, x = self.termsize
 
 		self.browser.resize(1, 0, y - 2, x)
 		self.taskview.resize(1, 0, y - 2, x)
@@ -286,9 +276,9 @@ class UI(DisplayableContainer):
 		self.win.touchwin()
 		DisplayableContainer.draw(self)
 		if self._draw_title and self.settings.update_title:
-			cwd = self.fm.env.cwd.path
-			if cwd.startswith(self.env.home_path):
-				cwd = '~' + cwd[len(self.env.home_path):]
+			cwd = self.fm.thisdir.path
+			if cwd.startswith(self.fm.home_path):
+				cwd = '~' + cwd[len(self.fm.home_path):]
 			if self.settings.shorten_title:
 				split = cwd.rsplit(os.sep, self.settings.shorten_title)
 				if os.sep in split[0]:
diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py
index e9c08439..1cb943d2 100644
--- a/ranger/gui/widgets/browsercolumn.py
+++ b/ranger/gui/widgets/browsercolumn.py
@@ -1,20 +1,9 @@
 # -*- encoding: utf8 -*-
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 """The BrowserColumn widget displays the contents of a directory or file."""
+import curses
 import stat
 from time import time
 
@@ -32,7 +21,7 @@ class BrowserColumn(Pager):
 	ellipsis = { False: '~', True: '…' }
 
 	old_dir = None
-	old_cf = None
+	old_thisfile = None
 
 	def __init__(self, win, level):
 		"""
@@ -86,7 +75,7 @@ class BrowserColumn(Pager):
 						if clicked_file.is_directory:
 							self.fm.enter_dir(clicked_file.path)
 						elif self.level == 0:
-							self.fm.env.cwd.move_to_obj(clicked_file)
+							self.fm.thisdir.move_to_obj(clicked_file)
 							self.fm.execute_file(clicked_file)
 					except:
 						pass
@@ -97,6 +86,22 @@ class BrowserColumn(Pager):
 
 		return True
 
+	def execute_curses_batch(self, line, commands):
+		"""
+		Executes a list of "commands" which can be easily cached.
+
+		"commands" is a list of lists.  Each element contains
+		a text and an attribute.  First, the attribute will be
+		set with attrset, then the text is printed.
+
+		Example:
+		execute_curses_batch(0, [["hello ", 0], ["world", curses.A_BOLD]])
+		"""
+		self.win.move(line, 0)
+		for entry in commands:
+			text, attr = entry
+			self.addstr(text, attr)
+
 	def has_preview(self):
 		if self.target is None:
 			return False
@@ -113,7 +118,7 @@ class BrowserColumn(Pager):
 
 	def poke(self):
 		Widget.poke(self)
-		self.target = self.env.at_level(self.level)
+		self.target = self.fm.thistab.at_level(self.level)
 
 	def draw(self):
 		"""Call either _draw_file() or _draw_directory()"""
@@ -126,9 +131,9 @@ class BrowserColumn(Pager):
 
 		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:
+			if self.target.pointed_obj != self.old_thisfile:
 				self.need_redraw = True
-				self.old_cf = self.target.pointed_obj
+				self.old_thisfile = self.target.pointed_obj
 
 			if self.target.load_content_if_outdated() \
 			or self.target.sort_if_outdated() \
@@ -200,31 +205,45 @@ class BrowserColumn(Pager):
 
 		self._set_scroll_begin()
 
-		copied = [f.path for f in self.env.copy]
+		copied = [f.path for f in self.fm.copy_buffer]
 		ellipsis = self.ellipsis[self.settings.unicode_ellipsis]
 
 		selected_i = self.target.pointer
 		for line in range(self.hei):
 			i = line + self.scroll_begin
+			if line > self.hei:
+				break
 
 			try:
 				drawn = self.target.files[i]
 			except IndexError:
 				break
 
+			tagged = self.fm.tags and drawn.realpath in self.fm.tags
+			if tagged:
+				tagged_marker = self.fm.tags.marker(drawn.realpath)
+			else:
+				tagged_marker = " "
+
+			key = (self.wid, selected_i == i, drawn.marked, self.main_column,
+					drawn.path in copied, tagged_marker, drawn.infostring)
+
+			if key in drawn.display_data:
+				self.execute_curses_batch(line, drawn.display_data[key])
+				self.color_reset()
+				continue
+
+			display_data = []
+			drawn.display_data[key] = display_data
+
 			if self.display_infostring and drawn.infostring \
 					and self.settings.display_size_in_main_column:
 				infostring = str(drawn.infostring) + " "
 			else:
 				infostring = ""
 
-			bad_info_color = None
 			this_color = base_color + list(drawn.mimetype_tuple)
 			text = drawn.basename
-			tagged = self.fm.tags and drawn.realpath in self.fm.tags
-
-			if tagged:
-				tagged_marker = self.fm.tags.marker(drawn.realpath)
 
 			space = self.wid - len(infostring)
 			if self.main_column:
@@ -232,9 +251,6 @@ class BrowserColumn(Pager):
 			elif self.settings.display_tags_in_all_columns:
 				space -= 1
 
-#			if len(text) > space:
-#				text = text[:space-1] + self.ellipsis
-
 			if i == selected_i:
 				this_color.append('selected')
 
@@ -245,8 +261,6 @@ class BrowserColumn(Pager):
 
 			if tagged:
 				this_color.append('tagged')
-				if self.main_column or self.settings.display_tags_in_all_columns:
-					text = tagged_marker + text
 
 			if drawn.is_directory:
 				this_color.append('directory')
@@ -265,40 +279,45 @@ class BrowserColumn(Pager):
 					this_color.append('device')
 
 			if drawn.path in copied:
-				this_color.append('cut' if self.env.cut else 'copied')
+				this_color.append('cut' if self.fm.do_cut else 'copied')
 
 			if drawn.is_link:
 				this_color.append('link')
 				this_color.append(drawn.exists and 'good' or 'bad')
 
-			wtext = WideString(text)
-			if len(wtext) > space:
-				wtext = wtext[:space - 1] + ellipsis
-			if self.main_column or self.settings.display_tags_in_all_columns:
-				if tagged:
-					self.addstr(line, 0, str(wtext))
-				elif self.wid > 1:
-					self.addstr(line, 1, str(wtext))
-			else:
-				self.addstr(line, 0, str(wtext))
-
-			if infostring:
-				x = self.wid - 1 - len(infostring)
-				if infostring is BAD_INFO:
-					bad_info_color = (x, len(infostring))
-				if x > 0:
-					self.addstr(line, x, infostring)
-
-			self.color_at(line, 0, self.wid, tuple(this_color))
-			if bad_info_color:
-				start, wid = bad_info_color
-				self.color_at(line, start, wid, tuple(this_color), 'badinfo')
+			attr = self.settings.colorscheme.get_attr(*this_color)
 
 			if (self.main_column or self.settings.display_tags_in_all_columns) \
 					and tagged and self.wid > 2:
 				this_color.append('tag_marker')
-				self.color_at(line, 0, len(tagged_marker), tuple(this_color))
+				tag_attr = self.settings.colorscheme.get_attr(*this_color)
+				display_data.append([tagged_marker, tag_attr])
+			else:
+				text = " " + text
+
+			wtext = WideString(text)
+			if len(wtext) > space:
+				wtext = wtext[:max(0, space - 1)] + ellipsis
+			text = str(wtext)
+
+			display_data.append([text, attr])
+
+			padding = self.wid - len(wtext)
+			if tagged and (self.main_column or \
+					self.settings.display_tags_in_all_columns):
+				padding -= 1
+			if infostring:
+				if len(text) + 1 + len(infostring) > self.wid:
+					pass
+				else:
+					padding -= len(infostring)
+					padding = max(0, padding)
+					infostring = (" " * padding) + infostring
+					display_data.append([infostring, attr])
+			else:
+				display_data.append([" " * max(0, padding), attr])
 
+			self.execute_curses_batch(line, display_data)
 			self.color_reset()
 
 	def _get_scroll_begin(self):
diff --git a/ranger/gui/widgets/browserview.py b/ranger/gui/widgets/browserview.py
index ea04c1e0..3bfaf3fd 100644
--- a/ranger/gui/widgets/browserview.py
+++ b/ranger/gui/widgets/browserview.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 """The BrowserView manages a set of BrowserColumns."""
 import curses
@@ -31,6 +19,7 @@ class BrowserView(Widget, DisplayableContainer):
 	need_clear = False
 	old_collapse = False
 	draw_hints = False
+	draw_info = False
 
 	def __init__(self, win, ratios, preview = True):
 		DisplayableContainer.__init__(self, win)
@@ -47,7 +36,7 @@ class BrowserView(Widget, DisplayableContainer):
 			self.settings.signal_bind('setopt.' + option,
 					self._request_clear_if_has_borders, weak=True)
 
-		self.fm.env.signal_bind('move', self.request_clear)
+		self.fm.signal_bind('move', self.request_clear)
 		self.settings.signal_bind('setopt.column_ratios', self.request_clear)
 
 	def change_ratios(self, ratios):
@@ -98,9 +87,9 @@ class BrowserView(Widget, DisplayableContainer):
 			self.win.erase()
 			self.need_redraw = True
 			self.need_clear = False
-		for path in self.fm.tabs.values():
-			if path is not None:
-				directory = self.env.get_directory(path)
+		for tab in self.fm.tabs.values():
+			directory = tab.thisdir
+			if directory:
 				directory.load_content_if_outdated()
 				directory.use()
 		DisplayableContainer.draw(self)
@@ -110,6 +99,8 @@ class BrowserView(Widget, DisplayableContainer):
 			self._draw_bookmarks()
 		elif self.draw_hints:
 			self._draw_hints()
+		elif self.draw_info:
+			self._draw_info(self.draw_info)
 
 	def finalize(self):
 		if self.pager.visible:
@@ -197,10 +188,23 @@ class BrowserView(Widget, DisplayableContainer):
 
 		self.win.chgat(ystart - 1, 0, curses.A_UNDERLINE)
 
+	def _draw_info(self, lines):
+		self.need_clear = True
+		hei = min(self.hei - 1, len(lines))
+		ystart = self.hei - hei
+		i = ystart
+		whitespace = " " * self.wid
+		for line in lines:
+			if i >= self.hei:
+				break
+			self.addstr(i, 0, whitespace)
+			self.addnstr(i, 0, line, self.wid)
+			i += 1
+
 	def _draw_hints(self):
 		self.need_clear = True
 		hints = []
-		for k, v in self.fm.env.keybuffer.pointer.items():
+		for k, v in self.fm.ui.keybuffer.pointer.items():
 			k = key_to_string(k)
 			if isinstance(v, dict):
 				text = '...'
diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py
index 2376ba5a..ba6dca86 100644
--- a/ranger/gui/widgets/console.py
+++ b/ranger/gui/widgets/console.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 """
 The Console widget implements a vim-like console for entering
@@ -70,7 +58,10 @@ class Console(Widget):
 				pass
 			else:
 				for entry in self.history_backup:
-					f.write(entry + '\n')
+					try:
+						f.write(entry + '\n')
+					except UnicodeEncodeError:
+						pass
 				f.close()
 
 	def draw(self):
@@ -131,6 +122,7 @@ class Console(Widget):
 			except:
 				pass
 			self.last_cursor_mode = None
+		self.fm.hide_console_info()
 		self.add_to_history()
 		self.tab_deque = None
 		self.clear()
@@ -145,7 +137,7 @@ class Console(Widget):
 		self.line = ''
 
 	def press(self, key):
-		self.env.keymaps.use_keymap('console')
+		self.fm.ui.keymaps.use_keymap('console')
 		if not self.fm.ui.press(key):
 			self.type_key(key)
 
diff --git a/ranger/gui/widgets/pager.py b/ranger/gui/widgets/pager.py
index 38451781..cf156715 100644
--- a/ranger/gui/widgets/pager.py
+++ b/ranger/gui/widgets/pager.py
@@ -1,18 +1,6 @@
 # Copyright (C) 2009, 2010, 2011  Roman Zimbelmann <romanz@lavabit.com>
 # Copyright (C) 2010 David Barnett <davidbarnett2@gmail.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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 """
 The pager displays text and allows you to scroll inside it.
@@ -109,7 +97,7 @@ class Pager(Widget):
 					offset=-self.hei + 1)
 
 	def press(self, key):
-		self.env.keymaps.use_keymap('pager')
+		self.fm.ui.keymaps.use_keymap('pager')
 		self.fm.ui.press(key)
 
 	def set_source(self, source, strip=False):
diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py
index 1ffb9fa3..bf5ee641 100644
--- a/ranger/gui/widgets/statusbar.py
+++ b/ranger/gui/widgets/statusbar.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 """
 The statusbar displays information about the current file and directory.
@@ -21,6 +9,7 @@ print for the current file.  The right side shows directory information
 such as the space used by all the files in this directory.
 """
 
+import os
 from pwd import getpwuid
 from grp import getgrgid
 from os import getuid, readlink
@@ -38,7 +27,7 @@ class StatusBar(Widget):
 	hint = None
 	msg = None
 
-	old_cf = None
+	old_thisfile = None
 	old_ctime = None
 	old_du = None
 	old_hint = None
@@ -81,10 +70,10 @@ class StatusBar(Widget):
 				self.msg = None
 				self.need_redraw = True
 
-		if self.env.cf:
-			self.env.cf.load_if_outdated()
+		if self.fm.thisfile:
+			self.fm.thisfile.load_if_outdated()
 			try:
-				ctime = self.env.cf.stat.st_ctime
+				ctime = self.fm.thisfile.stat.st_ctime
 			except:
 				ctime = -1
 		else:
@@ -93,12 +82,12 @@ class StatusBar(Widget):
 		if not self.result:
 			self.need_redraw = True
 
-		if self.old_du and not self.env.cwd.disk_usage:
-			self.old_du = self.env.cwd.disk_usage
+		if self.old_du and not self.fm.thisdir.disk_usage:
+			self.old_du = self.fm.thisdir.disk_usage
 			self.need_redraw = True
 
-		if self.old_cf != self.env.cf:
-			self.old_cf = self.env.cf
+		if self.old_thisfile != self.fm.thisfile:
+			self.old_thisfile = self.fm.thisfile
 			self.need_redraw = True
 
 		if self.old_ctime != ctime:
@@ -151,7 +140,7 @@ class StatusBar(Widget):
 				and self.column.target.is_directory:
 			target = self.column.target.pointed_obj
 		else:
-			target = self.env.at_level(0).pointed_obj
+			target = self.fm.thistab.at_level(0).pointed_obj
 		try:
 			stat = target.stat
 		except:
@@ -227,9 +216,9 @@ class StatusBar(Widget):
 		max_pos = len(target) - self.column.hei
 		base = 'scroll'
 
-		if self.env.cwd.filter:
+		if self.fm.thisdir.filter:
 			right.add(" f=", base, 'filter')
-			right.add(repr(self.env.cwd.filter), base, 'filter')
+			right.add(repr(self.fm.thisdir.filter), base, 'filter')
 			right.add(", ", "space")
 
 		if target.marked_items:
@@ -243,7 +232,7 @@ class StatusBar(Widget):
 		else:
 			right.add(human_readable(target.disk_usage, separator='') + " sum")
 			try:
-				free = self.env.get_free_space(target.mount_path)
+				free = get_free_space(target.mount_path)
 			except OSError:
 				pass
 			else:
@@ -277,6 +266,10 @@ class StatusBar(Widget):
 			self.addstr(str(part))
 		self.color_reset()
 
+def get_free_space(path):
+	stat = os.statvfs(path)
+	return stat.f_bavail * stat.f_bsize
+
 class Message(object):
 	elapse = None
 	text = None
diff --git a/ranger/gui/widgets/taskview.py b/ranger/gui/widgets/taskview.py
index a3f8e314..53da5826 100644
--- a/ranger/gui/widgets/taskview.py
+++ b/ranger/gui/widgets/taskview.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 """
 The TaskView allows you to modify what the loader is doing.
@@ -63,8 +51,14 @@ class TaskView(Widget, Accumulator):
 						clr.append('selected')
 
 					descr = obj.get_description()
-					self.addstr(y, 0, descr, self.wid)
-					self.color_at(y, 0, self.wid, tuple(clr))
+					if obj.percent >= 0 and obj.percent <= 100:
+						self.addstr(y, 0, "%3d%% - %s" % (obj.percent, descr), self.wid)
+						wid = int(self.wid / 100.0 * obj.percent)
+						self.color_at(y, 0, self.wid, tuple(clr))
+						self.color_at(y, 0, wid, tuple(clr), 'loaded')
+					else:
+						self.addstr(y, 0, descr, self.wid)
+						self.color_at(y, 0, self.wid, tuple(clr))
 
 			else:
 				if self.hei > 1:
@@ -92,7 +86,7 @@ class TaskView(Widget, Accumulator):
 		self.fm.loader.move(_from=i, to=to)
 
 	def press(self, key):
-		self.env.keymaps.use_keymap('taskview')
+		self.fm.ui.keymaps.use_keymap('taskview')
 		self.fm.ui.press(key)
 
 	def get_list(self):
diff --git a/ranger/gui/widgets/titlebar.py b/ranger/gui/widgets/titlebar.py
index 2b5e836b..d37a2fd3 100644
--- a/ranger/gui/widgets/titlebar.py
+++ b/ranger/gui/widgets/titlebar.py
@@ -1,17 +1,5 @@
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 """
 The titlebar is the widget at the top, giving you broad overview.
@@ -25,7 +13,7 @@ from . import Widget
 from ranger.gui.bar import Bar
 
 class TitleBar(Widget):
-	old_cf = None
+	old_thisfile = None
 	old_keybuffer = None
 	old_wid = None
 	result = None
@@ -42,12 +30,12 @@ class TitleBar(Widget):
 
 	def draw(self):
 		if self.need_redraw or \
-				self.env.cf != self.old_cf or\
-				str(self.env.keybuffer) != str(self.old_keybuffer) or\
+				self.fm.thisfile != self.old_thisfile or\
+				str(self.fm.ui.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.old_thisfile = self.fm.thisfile
 			self._calc_bar()
 		self._print_result(self.result)
 		if self.wid > 2:
@@ -103,20 +91,20 @@ class TitleBar(Widget):
 
 	def _get_left_part(self, bar):
 		# TODO: Properly escape non-printable chars without breaking unicode
-		if self.env.username == 'root':
+		if self.fm.username == 'root':
 			clr = 'bad'
 		else:
 			clr = 'good'
 
-		bar.add(self.env.username, 'hostname', clr, fixed=True)
+		bar.add(self.fm.username, 'hostname', clr, fixed=True)
 		bar.add('@', 'hostname', clr, fixed=True)
-		bar.add(self.env.hostname, 'hostname', clr, fixed=True)
+		bar.add(self.fm.hostname, 'hostname', clr, fixed=True)
 		bar.add(':', 'hostname', clr, fixed=True)
 
-		pathway = self.env.pathway
+		pathway = self.fm.thistab.pathway
 		if self.settings.tilde_in_titlebar and \
-				self.fm.env.cwd.path.startswith(self.env.home_path):
-			pathway = pathway[self.env.home_path.count('/')+1:]
+				self.fm.thisdir.path.startswith(self.fm.home_path):
+			pathway = pathway[self.fm.home_path.count('/')+1:]
 			bar.add('~/', 'directory', fixed=True)
 
 		for path in pathway:
@@ -128,12 +116,12 @@ class TitleBar(Widget):
 			bar.add(path.basename, clr, directory=path)
 			bar.add('/', clr, fixed=True, directory=path)
 
-		if self.env.cf is not None:
-			bar.add(self.env.cf.basename, 'file')
+		if self.fm.thisfile is not None:
+			bar.add(self.fm.thisfile.basename, 'file')
 
 	def _get_right_part(self, bar):
 		# TODO: fix that pressed keys are cut off when chaining CTRL keys
-		kb = str(self.env.keybuffer)
+		kb = str(self.fm.ui.keybuffer)
 		self.old_keybuffer = kb
 		bar.addright(kb, 'keybuffer', fixed=True)
 		bar.addright('  ', 'space', fixed=True)
@@ -148,7 +136,7 @@ class TitleBar(Widget):
 	def _get_tab_text(self, tabname):
 		result = ' ' + str(tabname)
 		if self.settings.dirname_in_tabs:
-			dirname = basename(self.fm.tabs[tabname])
+			dirname = basename(self.fm.tabs[tabname].path)
 			if not dirname:
 				result += ":/"
 			elif len(dirname) > 15:
diff --git a/scripts/ranger b/scripts/ranger
new file mode 120000
index 00000000..21b7d3ee
--- /dev/null
+++ b/scripts/ranger
@@ -0,0 +1 @@
+../ranger.py
\ No newline at end of file
diff --git a/scripts/rifle b/scripts/rifle
new file mode 120000
index 00000000..1dbaa2d8
--- /dev/null
+++ b/scripts/rifle
@@ -0,0 +1 @@
+../ranger/ext/rifle.py
\ No newline at end of file
diff --git a/setup.py b/setup.py
index ec782794..d8de3130 100755
--- a/setup.py
+++ b/setup.py
@@ -1,18 +1,6 @@
 #!/usr/bin/env python
 # Copyright (C) 2009, 2010, 2011  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/>.
+# This software is distributed under the terms of the GNU GPL version 3.
 
 import distutils.core
 import ranger
@@ -27,15 +15,16 @@ if __name__ == '__main__':
 		author_email=ranger.__email__,
 		license=ranger.__license__,
 		url='http://savannah.nongnu.org/projects/ranger',
-		scripts=['ranger/data/ranger'],
+		scripts=['scripts/ranger', 'scripts/rifle'],
 		data_files=[('share/man/man1', ['doc/ranger.1'])],
-		package_data={'ranger': ['data/*', 'defaults/rc.conf']},
+		package_data={'ranger': ['data/*', 'config/rc.conf',
+			'config/rifle.conf']},
 		packages=('ranger',
 		          'ranger.api',
 		          'ranger.colorschemes',
 		          'ranger.container',
 		          'ranger.core',
-		          'ranger.defaults',
+		          'ranger.config',
 		          'ranger.ext',
 		          'ranger.fsobject',
 		          'ranger.gui',