summary refs log tree commit diff stats
path: root/ranger
diff options
context:
space:
mode:
authorhut <hut@lavabit.com>2012-03-05 11:56:05 +0100
committerhut <hut@lavabit.com>2012-03-05 11:56:05 +0100
commite1155824b39d584beb8c178ceca8999eb7783daf (patch)
tree22dae0662d03ce8562159d9196fd8c854d6af009 /ranger
parent2bcfbe93584f3e9bdd4f80ff4cce43e39a545914 (diff)
parent23b7f16961a7b679dec16f6ee91401a6b8f6ca82 (diff)
downloadranger-e1155824b39d584beb8c178ceca8999eb7783daf.tar.gz
Merge branch 'master' into stable
Diffstat (limited to 'ranger')
-rw-r--r--ranger/__init__.py2
-rw-r--r--ranger/api/commands.py8
-rw-r--r--ranger/container/settingobject.py2
-rw-r--r--ranger/core/actions.py139
-rw-r--r--ranger/core/fm.py7
-rw-r--r--ranger/core/helper.py10
-rw-r--r--ranger/core/loader.py2
-rw-r--r--ranger/core/main.py53
-rw-r--r--ranger/core/runner.py42
-rwxr-xr-xranger/data/scope.sh2
-rw-r--r--ranger/defaults/apps.py63
-rw-r--r--ranger/defaults/commands.py56
-rw-r--r--ranger/defaults/options.py18
-rw-r--r--ranger/defaults/rc.conf8
-rw-r--r--ranger/ext/cached_function.py27
-rw-r--r--ranger/ext/human_readable.py24
-rw-r--r--ranger/ext/next_available_filename.py30
-rw-r--r--ranger/ext/shell_escape.py7
-rw-r--r--ranger/ext/signals.py20
-rw-r--r--ranger/fsobject/directory.py52
-rw-r--r--ranger/fsobject/fsobject.py17
-rw-r--r--ranger/gui/colorscheme.py42
-rw-r--r--ranger/gui/curses_shortcuts.py3
-rw-r--r--ranger/gui/ui.py12
-rw-r--r--ranger/gui/widgets/browsercolumn.py12
-rw-r--r--ranger/gui/widgets/browserview.py5
-rw-r--r--ranger/gui/widgets/statusbar.py27
-rw-r--r--ranger/gui/widgets/taskview.py12
-rw-r--r--ranger/gui/widgets/titlebar.py4
29 files changed, 572 insertions, 134 deletions
diff --git a/ranger/__init__.py b/ranger/__init__.py
index df413a2c..df759dc8 100644
--- a/ranger/__init__.py
+++ b/ranger/__init__.py
@@ -36,7 +36,7 @@ TIME_BEFORE_FILE_BECOMES_GARBAGE = 1200
 MACRO_DELIMITER = '%'
 LOGFILE = '/tmp/ranger_errorlog'
 USAGE = '%prog [options] [path/filename]'
-STABLE = True
+STABLE = False
 
 # If the environment variable XDG_CONFIG_HOME is non-empty, CONFDIR is ignored
 # and the configuration directory will be $XDG_CONFIG_HOME/ranger instead.
diff --git a/ranger/api/commands.py b/ranger/api/commands.py
index ae3bdc94..a2501c7f 100644
--- a/ranger/api/commands.py
+++ b/ranger/api/commands.py
@@ -13,6 +13,8 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+# TODO: Add an optional "!" to all commands and set a flag if it's there
+
 import os
 import ranger
 import re
@@ -264,9 +266,11 @@ class Command(FileManagerAware):
 			if len(names) == 0:
 				return
 
-			# one result. since it must be a directory, append a slash.
+			# one result. append a slash if it's a directory
 			if len(names) == 1:
-				return self.start(1) + join(rel_dirname, names[0]) + '/'
+				path = join(rel_dirname, names[0])
+				slash = '/' if os.path.isdir(path) else ''
+				return self.start(1) + path + slash
 
 			# more than one result. append no slash, so the user can
 			# manually type in the slash to advance into that directory
diff --git a/ranger/container/settingobject.py b/ranger/container/settingobject.py
index 5c24d663..e7ded15e 100644
--- a/ranger/container/settingobject.py
+++ b/ranger/container/settingobject.py
@@ -19,6 +19,7 @@ from ranger.core.shared import FileManagerAware
 
 ALLOWED_SETTINGS = {
 	'autosave_bookmarks': bool,
+	'autoupdate_cumulative_size': bool,
 	'collapse_preview': bool,
 	'colorscheme_overlay': (type(None), type(lambda:0)),
 	'colorscheme': str,
@@ -31,6 +32,7 @@ ALLOWED_SETTINGS = {
 	'draw_borders': bool,
 	'flushinput': bool,
 	'hidden_filter': lambda x: isinstance(x, str) or hasattr(x, 'match'),
+	'init_function': (type(None), type(lambda:0)),
 	'load_default_rc': (bool, type(None)),
 	'max_console_history_size': (int, type(None)),
 	'max_history_size': (int, type(None)),
diff --git a/ranger/core/actions.py b/ranger/core/actions.py
index c8922734..4e72de77 100644
--- a/ranger/core/actions.py
+++ b/ranger/core/actions.py
@@ -19,8 +19,8 @@ import re
 import shutil
 import string
 import tempfile
-from os.path import join, isdir, realpath
-from os import link, symlink, getcwd
+from os.path import join, isdir, realpath, exists
+from os import link, symlink, getcwd, listdir, stat
 from inspect import cleandoc
 
 import ranger
@@ -28,6 +28,7 @@ from ranger.ext.direction import Direction
 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.core.shared import FileManagerAware, EnvironmentAware, \
 		SettingsAware
 from ranger.fsobject import File
@@ -41,6 +42,11 @@ class _MacroTemplate(string.Template):
 
 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
@@ -56,6 +62,32 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 		self.previews = {}
 		self.env.garbage_collect(-1, self.tabs)
 		self.enter_dir(old_path)
+		self.change_mode('normal')
+
+	def change_mode(self, mode):
+		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.mark_files(val=not self._visual_reverse, movedown=False)
+		elif mode == 'normal':
+			if self.mode == 'visual':
+				self._visual_start       = None
+				self._visual_start_pos   = None
+				self._previous_selection = None
+		else:
+			return
+		self.mode = mode
+		self.ui.status.request_redraw()
+
+	def toggle_visual_mode(self, reverse=False):
+		if self.mode == 'normal':
+			self._visual_reverse = reverse
+			self.change_mode('visual')
+		else:
+			self.change_mode('normal')
 
 	def reload_cwd(self):
 		try:
@@ -87,12 +119,19 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 			self.notify("Aborting: " + item.get_description())
 			self.loader.remove(index=0)
 
+	def get_cumulative_size(self):
+		for f in self.env.get_selection() or ():
+			f.look_up_cumulative_size()
+		self.ui.status.request_redraw()
+		self.ui.redraw_main_column()
+
 	def redraw_window(self):
 		"""Redraw the window"""
 		self.ui.redraw_window()
 
 	def open_console(self, string='', prompt=None, position=None):
-		"""Open the console if the current UI supports that"""
+		"""Open the console"""
+		self.change_mode('normal')
 		self.ui.open_console(string, prompt=prompt, position=position)
 
 	def execute_console(self, string='', wildcards=[], quantifier=None):
@@ -247,15 +286,16 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 		Both flags and mode specify how the program is run."""
 
 		# ranger can act as a file chooser when running with --choosefile=...
-		if ranger.arg.choosefile:
-			open(ranger.arg.choosefile, 'w').write(self.fm.env.cf.path)
+		if ('mode' not in kw or kw['mode'] == 0) and 'app' not in kw:
+			if ranger.arg.choosefile:
+				open(ranger.arg.choosefile, 'w').write(self.fm.env.cf.path)
 
-		if ranger.arg.choosefiles:
-			open(ranger.arg.choosefiles, 'w').write("".join(
-				f.path + "\n" for f in self.fm.env.get_selection()))
+			if ranger.arg.choosefiles:
+				open(ranger.arg.choosefiles, 'w').write("".join(
+					f.path + "\n" for f in self.fm.env.get_selection()))
 
-		if ranger.arg.choosefile or ranger.arg.choosefiles:
-			raise SystemExit()
+			if ranger.arg.choosefile or ranger.arg.choosefiles:
+				raise SystemExit()
 
 		if isinstance(files, set):
 			files = list(files)
@@ -306,6 +346,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 			except:
 				return
 			self.env.enter_dir(directory)
+			self.change_mode('normal')
 		if cwd and cwd.accessible and cwd.content_loaded:
 			if 'right' in direction:
 				mode = 0
@@ -324,8 +365,34 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 						current=cwd.pointer,
 						pagesize=self.ui.browser.hei)
 				cwd.move(to=newpos)
+				if self.mode == 'visual':
+					try:
+						startpos = cwd.index(self._visual_start)
+					except:
+						self._visual_start = None
+						startpos = min(self._visual_start_pos, len(cwd))
+					# The files between here and _visual_start_pos
+					targets = set(cwd.files[min(startpos, newpos):\
+							max(startpos, newpos) + 1])
+					# The selection before activating visual mode
+					old = self._previous_selection
+					# The current selection
+					current = set(cwd.marked_items)
+
+					# Set theory anyone?
+					if not self._visual_reverse:
+						for f in targets - current:
+							cwd.mark_item(f, True)
+						for f in current - old - targets:
+							cwd.mark_item(f, False)
+					else:
+						for f in targets & current:
+							cwd.mark_item(f, False)
+						for f in old - current - targets:
+							cwd.mark_item(f, True)
 
 	def move_parent(self, n, narg=None):
+		self.change_mode('normal')
 		if narg is not None:
 			n *= narg
 		parent = self.env.at_level(-1)
@@ -354,18 +421,20 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 
 	def enter_dir(self, path, remember=False, history=True):
 		"""Enter the directory at the given path"""
-		if remember:
-			cwd = self.env.cwd
-			result = self.env.enter_dir(path, history=history)
-			self.bookmarks.remember(cwd)
-			return result
-		return self.env.enter_dir(path, history=history)
+		cwd = self.env.cwd
+		result = self.env.enter_dir(path, history=history)
+		if cwd != self.env.cwd:
+			if remember:
+				self.bookmarks.remember(cwd)
+			self.change_mode('normal')
+		return result
 
 	def cd(self, path, remember=True):
 		"""enter the directory at the given path, remember=True"""
 		self.enter_dir(path, remember=remember)
 
 	def traverse(self):
+		self.change_mode('normal')
 		cf = self.env.cf
 		cwd = self.env.cwd
 		if cf is not None and cf.is_directory:
@@ -470,6 +539,8 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 				cwd.toggle_all_marks()
 			else:
 				cwd.mark_all(val)
+			if self.mode == 'visual':
+				self.change_mode('normal')
 		else:
 			for i in range(cwd.pointer, min(cwd.pointer + narg, len(cwd))):
 				item = cwd.files[i]
@@ -762,13 +833,14 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 	# directory paths only.
 
 	def tab_open(self, name, path=None):
-		do_emit_signal = 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])
 		else:
 			self._update_current_tab()
-		if do_emit_signal:
+		if tab_has_changed:
+			self.change_mode('normal')
 			self.signal_emit('tab.change')
 
 	def tab_close(self, name=None):
@@ -913,21 +985,46 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 	def paste_symlink(self, relative=False):
 		copied_files = self.env.copy
 		for f in copied_files:
+			self.notify(next_available_filename(f.basename))
 			try:
+				new_name = next_available_filename(f.basename)
 				if relative:
-					relative_symlink(f.path, join(getcwd(), f.basename))
+					relative_symlink(f.path, join(getcwd(), new_name))
 				else:
-					symlink(f.path, join(getcwd(), f.basename))
+					symlink(f.path, join(getcwd(), new_name))
 			except Exception as x:
 				self.notify(x)
 
 	def paste_hardlink(self):
 		for f in self.env.copy:
 			try:
-				link(f.path, join(getcwd(), f.basename))
+				new_name = next_available_filename(f.basename)
+				link(f.path, join(getcwd(), new_name))
 			except Exception as x:
 				self.notify(x)
 
+	def paste_hardlinked_subtree(self):
+		for f in self.env.copy:
+			try:
+				target_path = join(getcwd(), f.basename)
+				self._recurse_hardlinked_tree(f.path, target_path)
+			except Exception as x:
+				self.notify(x)
+
+	def _recurse_hardlinked_tree(self, source_path, target_path):
+		if isdir(source_path):
+			if not exists(target_path):
+				os.mkdir(target_path, stat(source_path).st_mode)
+			for item in listdir(source_path):
+				self._recurse_hardlinked_tree(
+					join(source_path, item),
+					join(target_path, item))
+		else:
+			if not exists(target_path) \
+			or stat(source_path).st_ino != stat(target_path).st_ino:
+				link(source_path,
+					next_available_filename(target_path))
+
 	def paste(self, overwrite=False):
 		"""Paste the selected items into the current directory"""
 		copied_files = tuple(self.env.copy)
diff --git a/ranger/core/fm.py b/ranger/core/fm.py
index 59eb4e18..20327a71 100644
--- a/ranger/core/fm.py
+++ b/ranger/core/fm.py
@@ -85,10 +85,13 @@ class FM(Actions, SignalDispatcher):
 		def mylogfunc(text):
 			self.notify(text, bad=True)
 		self.run = Runner(ui=self.ui, apps=self.apps,
-				logfunc=mylogfunc)
+				logfunc=mylogfunc, fm=self)
 
 		self.env.signal_bind('cd', self._update_current_tab)
 
+		if self.settings.init_function:
+			self.settings.init_function(self)
+
 	def destroy(self):
 		debug = ranger.arg.debug
 		if self.ui:
@@ -209,6 +212,8 @@ class FM(Actions, SignalDispatcher):
 
 		finally:
 			if ranger.arg.choosedir and self.env.cwd and self.env.cwd.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)
 			self.bookmarks.save()
diff --git a/ranger/core/helper.py b/ranger/core/helper.py
index c22a52b8..c556b9bd 100644
--- a/ranger/core/helper.py
+++ b/ranger/core/helper.py
@@ -66,6 +66,16 @@ def parse_arguments():
 			", 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)
diff --git a/ranger/core/loader.py b/ranger/core/loader.py
index e1262718..59d3e6c0 100644
--- a/ranger/core/loader.py
+++ b/ranger/core/loader.py
@@ -146,7 +146,7 @@ def safeDecode(string):
 		return string.decode("utf-8")
 	except (UnicodeDecodeError):
 		if HAVE_CHARDET:
-			return string.decode(chardet.detect(str)["encoding"])
+			return string.decode(chardet.detect(string)["encoding"])
 		else:
 			return ""
 
diff --git a/ranger/core/main.py b/ranger/core/main.py
index c87a4660..b4629801 100644
--- a/ranger/core/main.py
+++ b/ranger/core/main.py
@@ -38,6 +38,13 @@ def main():
 	except:
 		print("Warning: Unable to set locale.  Expect encoding problems.")
 
+	# so that programs can know that ranger spawned them:
+	level = 'RANGER_LEVEL'
+	if level in os.environ and os.environ[level].isdigit():
+		os.environ[level] = str(int(os.environ[level]) + 1)
+	else:
+		os.environ[level] = '1'
+
 	if not 'SHELL' in os.environ:
 		os.environ['SHELL'] = 'bash'
 
@@ -46,9 +53,27 @@ def main():
 		fm = FM()
 		fm.copy_config_files(arg.copy_config)
 		return 1 if arg.fail_unless_cd else 0
+	if arg.list_tagged_files:
+		fm = FM()
+		try:
+			f = open(fm.confpath('tagged'), 'r')
+		except:
+			pass
+		else:
+			for line in f.readlines():
+				if len(line) > 2 and line[1] == ':':
+					if line[0] in arg.list_tagged_files:
+						sys.stdout.write(line[2:])
+				elif len(line) > 0 and '*' in arg.list_tagged_files:
+					sys.stdout.write(line)
+		return 1 if arg.fail_unless_cd else 0
 
 	SettingsAware._setup(clean=arg.clean)
 
+	if arg.selectfile:
+		arg.selectfile = os.path.abspath(arg.selectfile)
+		arg.targets.insert(0, os.path.dirname(arg.selectfile))
+
 	targets = arg.targets or ['.']
 	target = targets[0]
 	if arg.targets:
@@ -62,7 +87,8 @@ def main():
 				print(string)
 			from ranger.core.runner import Runner
 			from ranger.fsobject import File
-			runner = Runner(logfunc=print_function)
+			fm = FM()
+			runner = Runner(logfunc=print_function, fm=fm)
 			load_apps(runner, arg.clean)
 			runner(files=[File(target)], mode=arg.mode, flags=arg.flags)
 			return 1 if arg.fail_unless_cd else 0
@@ -97,10 +123,26 @@ def main():
 			from ranger.ext import curses_interrupt_handler
 			curses_interrupt_handler.install_interrupt_handler()
 
+		if arg.selectfile:
+			fm.select_file(arg.selectfile)
+
 		# Run the file manager
 		fm.initialize()
 		fm.ui.initialize()
-		fm.loop()
+
+		if arg.cmd:
+			for command in arg.cmd:
+				fm.execute_console(command)
+
+		if ranger.arg.profile:
+			import cProfile
+			import pstats
+			profile = None
+			ranger.__fm = fm
+			cProfile.run('ranger.__fm.loop()', '/tmp/ranger_profile')
+			profile = pstats.Stats('/tmp/ranger_profile', stream=sys.stderr)
+		else:
+			fm.loop()
 	except Exception:
 		import traceback
 		crash_traceback = traceback.format_exc()
@@ -116,11 +158,16 @@ def main():
 			fm.ui.destroy()
 		except (AttributeError, NameError):
 			pass
+		if ranger.arg.profile and profile:
+			profile.strip_dirs().sort_stats('cumulative').print_callees()
 		if crash_traceback:
 			print("ranger version: %s, executed with python %s" %
 					(ranger.__version__, sys.version.split()[0]))
 			print("Locale: %s" % '.'.join(str(s) for s in locale.getlocale()))
-			print("Current file: %s" % filepath)
+			try:
+				print("Current file: %s" % filepath)
+			except:
+				pass
 			print(crash_traceback)
 			print("ranger crashed.  " \
 				"Please report this traceback at:")
diff --git a/ranger/core/runner.py b/ranger/core/runner.py
index 940f410e..17cdcca5 100644
--- a/ranger/core/runner.py
+++ b/ranger/core/runner.py
@@ -30,15 +30,18 @@ d: detach the process.
 p: redirect output to the pager
 c: run only the current file (not handled here)
 w: wait for enter-press afterwards
+r: run application with root privilege (requires sudo)
+t: run application in a new terminal window
 (An uppercase key negates the respective lower case flag)
 """
 
 import os
 import sys
 from subprocess import Popen, PIPE
+from ranger.ext.get_executables import get_executables
 
 
-ALLOWED_FLAGS = 'sdpwcSDPWC'
+ALLOWED_FLAGS = 'sdpwcrtSDPWCRT'
 
 
 def press_enter():
@@ -94,8 +97,9 @@ class Context(object):
 
 
 class Runner(object):
-	def __init__(self, ui=None, logfunc=None, apps=None):
+	def __init__(self, ui=None, logfunc=None, apps=None, fm=None):
 		self.ui = ui
+		self.fm = fm
 		self.logfunc = logfunc
 		self.apps = apps
 		self.zombies = set()
@@ -132,7 +136,7 @@ class Runner(object):
 		# creating a Context object and passing it to
 		# an Application object.
 
-		context = Context(app=app, files=files, mode=mode,
+		context = Context(app=app, files=files, mode=mode, fm=self.fm,
 				flags=flags, wait=wait, popen_kws=popen_kws,
 				file=files and files[0] or None)
 
@@ -159,7 +163,6 @@ class Runner(object):
 		wait_for_enter = False
 		devnull = None
 
-		popen_kws['args'] = action
 		if 'shell' not in popen_kws:
 			popen_kws['shell'] = isinstance(action, str)
 		if 'stdout' not in popen_kws:
@@ -188,16 +191,45 @@ class Runner(object):
 		if 'w' in context.flags:
 			if not pipe_output and context.wait: # <-- sanity check
 				wait_for_enter = True
+		if 'r' in context.flags:
+			if 'sudo' not in get_executables():
+				return self._log("Can not run with 'r' flag, sudo is not installed!")
+			dflag = ('d' in context.flags)
+			if isinstance(action, str):
+				action = 'sudo ' + (dflag and '-b ' or '') + action
+			else:
+				action = ['sudo'] + (dflag and ['-b'] or []) + action
+			toggle_ui = True
+			context.wait = True
+		if 't' in context.flags:
+			if 'DISPLAY' not in os.environ:
+				return self._log("Can not run with 't' flag, no display found!")
+			term = os.environ.get('TERMCMD', os.environ.get('TERM'))
+			if term not in get_executables():
+				term = 'x-terminal-emulator'
+			if term not in get_executables():
+				term = 'xterm'
+			if isinstance(action, str):
+				action = term + ' -e ' + action
+			else:
+				action = [term, '-e'] + action
+			toggle_ui = False
+			context.wait = False
 
+		popen_kws['args'] = action
 		# Finally, run it
 
 		if toggle_ui:
 			self._activate_ui(False)
 		try:
+			error = None
 			process = None
+			self.fm.signal_emit('runner.execute.before',
+					popen_kws=popen_kws, context=context)
 			try:
 				process = Popen(**popen_kws)
 			except Exception as e:
+				error = e
 				self._log("Failed to run: %s\n%s" % (str(action), str(e)))
 			else:
 				if context.wait:
@@ -207,6 +239,8 @@ class Runner(object):
 				if wait_for_enter:
 					press_enter()
 		finally:
+			self.fm.signal_emit('runner.execute.after',
+					popen_kws=popen_kws, context=context, error=error)
 			if devnull:
 				devnull.close()
 			if toggle_ui:
diff --git a/ranger/data/scope.sh b/ranger/data/scope.sh
index aeb47a13..ed4f01e1 100755
--- a/ranger/data/scope.sh
+++ b/ranger/data/scope.sh
@@ -26,7 +26,7 @@ maxln=200    # Stop after $maxln lines.  Can be used like ls | head -n $maxln
 
 # Find out something about the file:
 mimetype=$(file --mime-type -Lb "$path")
-extension=$(echo "$path" | grep '\.' | grep -o '[^.]\+$')
+extension=${path##*.}
 
 # Functions:
 # "have $1" succeeds if $1 is an existing command/installed program
diff --git a/ranger/defaults/apps.py b/ranger/defaults/apps.py
index 3ec6bff2..fbcc83c0 100644
--- a/ranger/defaults/apps.py
+++ b/ranger/defaults/apps.py
@@ -14,9 +14,17 @@
 # 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>
+#   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
@@ -27,6 +35,8 @@
 #     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"
@@ -97,23 +107,38 @@ class CustomApplications(Applications):
 
 		if f.extension is not None:
 			if f.extension in ('pdf', ):
-				return self.either(c, 'evince', 'zathura', 'apvlv')
+				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', ):
+			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'):
-				return self.either(c, 'firefox', 'opera', 'jumanji',
-						'luakit', 'elinks', 'lynx')
-			if f.extension == 'swf':
-				return self.either(c, 'firefox', 'opera', 'jumanji', 'luakit')
+				# 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 in ('odt', 'ods', 'odp', 'odf', 'odg',
-					'doc', 'xls'):
-				return self.either(c, 'libreoffice', 'soffice', 'ooffice')
+			if f.extension == 'doc':
+				return self.either(c, 'abiword', 'libreoffice',
+						'soffice', 'ooffice')
+			if f.extension in ('odt', 'ods', 'odp', 'odf', 'odg', '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):
@@ -125,8 +150,8 @@ class CustomApplications(Applications):
 		if f.video or f.audio:
 			if f.video:
 				c.flags += 'd'
-			return self.either(c, 'mplayer2', 'mplayer', 'smplayer', 'vlc',
-					'totem')
+			return self.either(c, 'smplayer', 'gmplayer', 'mplayer2',
+					'mplayer', 'vlc', 'totem')
 
 		if f.image:
 			if c.mode in (11, 12, 13, 14):
@@ -281,8 +306,14 @@ class CustomApplications(Applications):
 CustomApplications.generic('fceux', 'wine', 'zsnes', deps=['X'])
 
 # Add those which should only run in X AND should be detached/forked here:
-CustomApplications.generic('opera', 'firefox', 'apvlv', 'evince',
-		'zathura', 'gimp', 'mirage', 'eog', 'jumanji',
+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?
diff --git a/ranger/defaults/commands.py b/ranger/defaults/commands.py
index 6cfeff17..a89dd0f7 100644
--- a/ranger/defaults/commands.py
+++ b/ranger/defaults/commands.py
@@ -218,7 +218,11 @@ class shell(Command):
 			return (start + program + ' ' for program \
 					in get_executables() if program.startswith(command))
 		if position_of_last_space == len(command) - 1:
-			return self.line + '%s '
+			selection = self.fm.env.get_selection()
+			if len(selection) == 1:
+				return self.line + selection[0].shell_escaped_basename + ' '
+			else:
+				return self.line + '%s '
 		else:
 			before_word, start_of_word = self.line.rsplit(' ', 1)
 			return (before_word + ' ' + file.shell_escaped_basename \
@@ -455,7 +459,12 @@ class terminal(Command):
 	Spawns an "x-terminal-emulator" starting in the current directory.
 	"""
 	def execute(self):
-		self.fm.run('x-terminal-emulator', flags='d')
+		command = os.environ.get('TERMCMD', os.environ.get('TERM'))
+		if command not in get_executables():
+			command = 'x-terminal-emulator'
+		if command not in get_executables():
+			command = 'xterm'
+		self.fm.run(command, flags='d')
 
 
 class delete(Command):
@@ -797,11 +806,11 @@ class bulkrename(Command):
 		cmdfile.write(b"# This file will be executed when you close the editor.\n")
 		cmdfile.write(b"# Please double-check everything, clear the file to abort.\n")
 		if py3:
-			cmdfile.write("\n".join("mv -vi " + esc(old) + " " + esc(new) \
+			cmdfile.write("\n".join("mv -vi -- " + esc(old) + " " + esc(new) \
 				for old, new in zip(filenames, new_filenames) \
 				if old != new).encode("utf-8"))
 		else:
-			cmdfile.write("\n".join("mv -vi " + esc(old) + " " + esc(new) \
+			cmdfile.write("\n".join("mv -vi -- " + esc(old) + " " + esc(new) \
 				for old, new in zip(filenames, new_filenames) if old != new))
 		cmdfile.flush()
 		self.fm.execute_file([File(cmdfile.name)], app='editor')
@@ -809,6 +818,45 @@ class bulkrename(Command):
 		cmdfile.close()
 
 
+class relink(Command):
+	"""
+	:relink <newpath>
+
+	Changes the linked path of the currently highlighted symlink to <newpath>
+	"""
+
+	def execute(self):
+		from ranger.fsobject import File
+
+		new_path = self.rest(1)
+		cf = self.fm.env.cf
+
+		if not new_path:
+			return self.fm.notify('Syntax: relink <newpath>', bad=True)
+
+		if not cf.is_link:
+			return self.fm.notify('%s is not a symlink!' % cf.basename, bad=True)
+
+		if new_path == os.readlink(cf.path):
+			return
+
+		try:
+			os.remove(cf.path)
+			os.symlink(new_path, cf.path)
+		except OSError as err:
+			self.fm.notify(err)
+
+		self.fm.reset()
+		self.fm.env.cwd.pointed_obj = cf
+		self.fm.env.cf = cf
+
+	def tab(self):
+		if not self.rest(1):
+			return self.line+os.readlink(self.fm.env.cf.path)
+		else:
+			return self._tab_directory_content()
+
+
 class help_(Command):
 	"""
 	:help
diff --git a/ranger/defaults/options.py b/ranger/defaults/options.py
index 3b44d4f6..d076a96d 100644
--- a/ranger/defaults/options.py
+++ b/ranger/defaults/options.py
@@ -104,19 +104,35 @@ padding_right = True
 # When false, bookmarks are saved when ranger is exited.
 autosave_bookmarks = True
 
+# You can display the "real" cumulative size of directories by using the
+# command :get_cumulative_size or typing "dc".  The size is expensive to
+# calculate and will not be updated automatically.  You can choose
+# to update it automatically though by turning on this option:
+autoupdate_cumulative_size = False
+
 # Makes sense for screen readers:
 show_cursor = False
 
 # One of: size, basename, mtime, type
 sort = 'natural'
 sort_reverse = False
-sort_case_insensitive = False
+sort_case_insensitive = True
 sort_directories_first = True
 
 # Enable this if key combinations with the Alt Key don't work for you.
 # (Especially on xterm)
 xterm_alt_key = False
 
+# A function that is called when the user interface is being set up.
+init_function = None
+
+# You can use it to initialize some custom functionality or bind singals
+#def init_function(fm):
+#	fm.notify("Hello :)")
+#	def on_tab_change(signal):
+#		signal.origin.notify("Changing tab! Yay!")
+#	fm.signal_bind("tab.change", on_tab_change)
+
 # The color scheme overlay.  Explained below.
 colorscheme_overlay = None
 
diff --git a/ranger/defaults/rc.conf b/ranger/defaults/rc.conf
index a9e64622..77ffa5c3 100644
--- a/ranger/defaults/rc.conf
+++ b/ranger/defaults/rc.conf
@@ -39,6 +39,7 @@ map R     reload_cwd
 map <C-r> reset
 map <C-l> redraw_window
 map <C-c> abort
+map <esc> change_mode normal
 
 map i display_file
 map ? help
@@ -62,8 +63,9 @@ map T       tag_remove
 map "<any>  tag_toggle tag=%any
 map <Space> mark_files toggle=True
 map v       mark_files all=True toggle=True
-map V       mark_files all=True val=False
 map uv      mark_files all=True val=False
+map V       toggle_visual_mode
+map uV      toggle_visual_mode reverse=True
 
 # For the nostalgics: Midnight Commander bindings
 map <F1> help
@@ -145,6 +147,7 @@ map po paste overwrite=True
 map pl paste_symlink relative=False
 map pL paste_symlink relative=True
 map phl paste_hardlink
+map pht paste_hardlinked_subtree
 
 map dd cut
 map ud uncut
@@ -216,6 +219,8 @@ map oC chain set sort=ctime;     set sort_reverse=True
 map oA chain set sort=atime;     set sort_reverse=True
 map oT chain set sort="type";    set sort_reverse=True
 
+map dc get_cumulative_size
+
 # Settings
 map zc    toggle_option collapse_preview
 map zd    toggle_option sort_directories_first
@@ -226,6 +231,7 @@ map zm    toggle_option mouse_enabled
 map zp    toggle_option preview_files
 map zP    toggle_option preview_directories
 map zs    toggle_option sort_case_insensitive
+map zu    toggle_option autoupdate_cumulative_size
 map zv    toggle_option use_preview_script
 map zf    console filter 
 
diff --git a/ranger/ext/cached_function.py b/ranger/ext/cached_function.py
new file mode 100644
index 00000000..4d9ded18
--- /dev/null
+++ b/ranger/ext/cached_function.py
@@ -0,0 +1,27 @@
+# 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/>.
+
+def cached_function(fnc):
+  cache = {}
+  def inner_cached_function(*args):
+    try:
+      return cache[args]
+    except:
+      value = fnc(*args)
+      cache[args] = value
+      return value
+  inner_cached_function._cache = cache
+  return inner_cached_function
+
diff --git a/ranger/ext/human_readable.py b/ranger/ext/human_readable.py
index 9cdce409..c5bd2aac 100644
--- a/ranger/ext/human_readable.py
+++ b/ranger/ext/human_readable.py
@@ -13,7 +13,7 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-def human_readable(byte, seperator=' '):
+def human_readable(byte, separator=' '):
 	"""
 	Convert a large number of bytes to an easily readable format.
 
@@ -27,27 +27,27 @@ def human_readable(byte, seperator=' '):
 	if byte <= 0:
 		return '0'
 	if byte < 2**10:
-		return '%d%sB'   % (byte, seperator)
+		return '%d%sB'   % (byte, separator)
 	if byte < 2**10 * 999:
-		return '%.3g%sK' % (byte / 2**10.0, seperator)
+		return '%.3g%sK' % (byte / 2**10.0, separator)
 	if byte < 2**20:
-		return '%.4g%sK' % (byte / 2**10.0, seperator)
+		return '%.4g%sK' % (byte / 2**10.0, separator)
 	if byte < 2**20 * 999:
-		return '%.3g%sM' % (byte / 2**20.0, seperator)
+		return '%.3g%sM' % (byte / 2**20.0, separator)
 	if byte < 2**30:
-		return '%.4g%sM' % (byte / 2**20.0, seperator)
+		return '%.4g%sM' % (byte / 2**20.0, separator)
 	if byte < 2**30 * 999:
-		return '%.3g%sG' % (byte / 2**30.0, seperator)
+		return '%.3g%sG' % (byte / 2**30.0, separator)
 	if byte < 2**40:
-		return '%.4g%sG' % (byte / 2**30.0, seperator)
+		return '%.4g%sG' % (byte / 2**30.0, separator)
 	if byte < 2**40 * 999:
-		return '%.3g%sT' % (byte / 2**40.0, seperator)
+		return '%.3g%sT' % (byte / 2**40.0, separator)
 	if byte < 2**50:
-		return '%.4g%sT' % (byte / 2**40.0, seperator)
+		return '%.4g%sT' % (byte / 2**40.0, separator)
 	if byte < 2**50 * 999:
-		return '%.3g%sP' % (byte / 2**50.0, seperator)
+		return '%.3g%sP' % (byte / 2**50.0, separator)
 	if byte < 2**60:
-		return '%.4g%sP' % (byte / 2**50.0, seperator)
+		return '%.4g%sP' % (byte / 2**50.0, separator)
 	return '>9000'
 
 if __name__ == '__main__':
diff --git a/ranger/ext/next_available_filename.py b/ranger/ext/next_available_filename.py
new file mode 100644
index 00000000..696063cf
--- /dev/null
+++ b/ranger/ext/next_available_filename.py
@@ -0,0 +1,30 @@
+# 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/>.
+
+import os.path
+
+def next_available_filename(fname, directory="."):
+	existing_files = os.listdir(directory)
+
+	if fname not in existing_files:
+		return fname
+	if not fname.endswith("_"):
+		fname += "_"
+		if fname not in existing_files:
+			return fname
+
+	for i in range(1, len(existing_files) + 1):
+		if fname + str(i) not in existing_files:
+			return fname + str(i)
diff --git a/ranger/ext/shell_escape.py b/ranger/ext/shell_escape.py
index 28a502bf..b68afc33 100644
--- a/ranger/ext/shell_escape.py
+++ b/ranger/ext/shell_escape.py
@@ -18,17 +18,20 @@ Functions to escape metacharacters of arguments for shell commands.
 """
 
 META_CHARS = (' ', "'", '"', '`', '&', '|', ';',
-		'$', '!', '(', ')', '[', ']', '<', '>')
+		'$', '!', '(', ')', '[', ']', '<', '>', '\t')
+UNESCAPABLE = set(map(chr, list(range(9)) + list(range(10, 32)) \
+		+ list(range(127, 256))))
 META_DICT = dict([(mc, '\\' + mc) for mc in META_CHARS])
 
 def shell_quote(string):
 	"""Escapes by quoting"""
 	return "'" + str(string).replace("'", "'\\''") + "'"
 
-
 def shell_escape(arg):
 	"""Escapes by adding backslashes"""
 	arg = str(arg)
+	if UNESCAPABLE & set(arg):
+		return shell_quote(arg)
 	arg = arg.replace('\\', '\\\\') # make sure this comes at the start
 	for k, v in META_DICT.items():
 		arg = arg.replace(k, v)
diff --git a/ranger/ext/signals.py b/ranger/ext/signals.py
index ecb48de3..0df39fe0 100644
--- a/ranger/ext/signals.py
+++ b/ranger/ext/signals.py
@@ -126,7 +126,7 @@ class SignalDispatcher(object):
 				handler._function = None
 		self._signals = dict()
 
-	def signal_bind(self, signal_name, function, priority=0.5, weak=False):
+	def signal_bind(self, signal_name, function, priority=0.5, weak=False, autosort=True):
 		"""
 		Bind a function to the signal.
 
@@ -162,9 +162,25 @@ class SignalDispatcher(object):
 
 		handler = SignalHandler(signal_name, function, priority, nargs > 0)
 		handlers.append(handler)
-		handlers.sort(key=lambda handler: -handler._priority)
+		if autosort:
+			handlers.sort(key=lambda handler: -handler._priority)
 		return handler
 
+	def signal_force_sort(self, signal_name=None):
+		"""
+		Forces a sorting of signal handlers by priority.
+
+		This is only necessary if you used signal_bind with autosort=False
+		after finishing to bind many signals at once.
+		"""
+		if signal_name is None:
+			for handlers in self._signals.values():
+				handlers.sort(key=lambda handler: -handler._priority)
+		elif signal_name in self._signals:
+			self._signals[signal_name].sort(key=lambda handler: -handler._priority)
+		else:
+			return False
+
 	def signal_unbind(self, signal_handler):
 		"""
 		Removes a signal binding.
diff --git a/ranger/fsobject/directory.py b/ranger/fsobject/directory.py
index b3a35a0a..81e50ed9 100644
--- a/ranger/fsobject/directory.py
+++ b/ranger/fsobject/directory.py
@@ -25,6 +25,7 @@ from ranger.fsobject import File, FileSystemObject
 from ranger.core.shared import SettingsAware
 from ranger.ext.accumulator import Accumulator
 from ranger.ext.lazy_property import lazy_property
+from ranger.ext.human_readable import human_readable
 
 def sort_by_basename(path):
 	"""returns path.basename (for sorting)"""
@@ -79,6 +80,8 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware):
 	content_outdated = False
 	content_loaded = False
 
+	_cumulative_size_calculated = False
+
 	sort_dict = {
 		'basename': sort_by_basename,
 		'natural': sort_naturally,
@@ -100,11 +103,11 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware):
 		for opt in ('sort_directories_first', 'sort', 'sort_reverse',
 				'sort_case_insensitive'):
 			self.settings.signal_bind('setopt.' + opt,
-					self.request_resort, weak=True)
+					self.request_resort, weak=True, autosort=False)
 
 		for opt in ('hidden_filter', 'show_hidden'):
 			self.settings.signal_bind('setopt.' + opt,
-				self.request_reload, weak=True)
+				self.request_reload, weak=True, autosort=False)
 		self.use()
 
 	def request_resort(self):
@@ -185,8 +188,25 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware):
 				hidden_filter = not self.settings.show_hidden \
 						and self.settings.hidden_filter
 				filelist = os.listdir(mypath)
-				self.size = len(filelist)
-				self.infostring = ' %d' % self.size
+
+				if self._cumulative_size_calculated:
+					# If self.content_loaded is true, this is not the first
+					# time loading.  So I can't really be sure if the
+					# size has changed and I'll add a "?".
+					if self.content_loaded:
+						if self.fm.settings.autoupdate_cumulative_size:
+							self.look_up_cumulative_size()
+						else:
+							self.infostring = ' %s' % human_readable(
+								self.size, separator='? ')
+					else:
+						self.infostring = ' %s' % human_readable(self.size)
+				else:
+					self.size = len(filelist)
+					self.infostring = ' %d' % self.size
+				if self.is_link:
+					self.infostring = '->' + self.infostring
+
 				filenames = [mypath + (mypath == '/' and fname or '/' + fname)\
 						for fname in filelist if accept_file(
 							fname, mypath, hidden_filter, self.filter)]
@@ -327,6 +347,30 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware):
 		else:
 			self.correct_pointer()
 
+	def _get_cumulative_size(self):
+		if self.size == 0:
+			return 0
+		cum = 0
+		realpath = os.path.realpath
+		for dirpath, dirnames, filenames in os.walk(self.path,
+				onerror=lambda _: None):
+			for file in filenames:
+				try:
+					if dirpath == self.path:
+						stat = os_stat(realpath(dirpath + "/" + file))
+					else:
+						stat = os_stat(dirpath + "/" + file)
+					cum += stat.st_size
+				except:
+					pass
+		return cum
+
+	def look_up_cumulative_size(self):
+		self._cumulative_size_calculated = True
+		self.size = self._get_cumulative_size()
+		self.infostring = ('-> ' if self.is_link else ' ') + \
+				human_readable(self.size)
+
 	@lazy_property
 	def size(self):
 		try:
diff --git a/ranger/fsobject/fsobject.py b/ranger/fsobject/fsobject.py
index d74b21c1..943c8aa4 100644
--- a/ranger/fsobject/fsobject.py
+++ b/ranger/fsobject/fsobject.py
@@ -26,8 +26,17 @@ from ranger.ext.spawn import spawn
 from ranger.ext.lazy_property import lazy_property
 from ranger.ext.human_readable import human_readable
 
+if hasattr(str, 'maketrans'):
+	maketrans = str.maketrans
+else:
+	from string import maketrans
+_unsafe_chars = '\n' + ''.join(map(chr, range(32))) + ''.join(map(chr, range(128, 256)))
+_safe_string_table = maketrans(_unsafe_chars, '?' * len(_unsafe_chars))
 _extract_number_re = re.compile(r'([^0-9]?)(\d*)')
 
+def safe_path(path):
+	return path.translate(_safe_string_table)
+
 class FileSystemObject(FileManagerAware):
 	(basename,
 	basename_lower,
@@ -106,6 +115,11 @@ class FileSystemObject(FileManagerAware):
 		return [c if i % 3 == 1 else (int(c) if c else 0) for i, c in \
 			enumerate(_extract_number_re.split(self.basename_lower))]
 
+	@lazy_property
+	def safe_basename(self):
+		return self.basename.translate(_safe_string_table)
+
+
 	for attr in ('video', 'audio', 'image', 'media', 'document', 'container'):
 		exec("%s = lazy_property("
 			"lambda self: self.set_mimetype() or self.%s)" % (attr, attr))
@@ -117,6 +131,9 @@ class FileSystemObject(FileManagerAware):
 	def use(self):
 		"""Used in garbage-collecting.  Override in Directory"""
 
+	def look_up_cumulative_size(self):
+		pass # normal files have no cumulative size
+
 	def set_mimetype(self):
 		"""assign attributes such as self.video according to the mimetype"""
 		basename = self.basename
diff --git a/ranger/gui/colorscheme.py b/ranger/gui/colorscheme.py
index bc5a67a5..cc72d6a8 100644
--- a/ranger/gui/colorscheme.py
+++ b/ranger/gui/colorscheme.py
@@ -46,6 +46,8 @@ 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.shared import SettingsAware
+from ranger.ext.cached_function import cached_function
+from ranger.ext.iter_tools import flatten
 
 # ColorScheme is not SettingsAware but it will gain access
 # to the settings during the initialization.  We can't import
@@ -60,9 +62,6 @@ class ColorScheme(SettingsAware):
 	which fits to the given keys.
 	"""
 
-	def __init__(self):
-		self.cache = {}
-
 	def get(self, *keys):
 		"""
 		Returns the (fg, bg, attr) for the given keys.
@@ -70,33 +69,28 @@ class ColorScheme(SettingsAware):
 		Using this function rather than use() will cache all
 		colors for faster access.
 		"""
-		keys = frozenset(keys)
-		try:
-			return self.cache[keys]
-
-		except KeyError:
-			context = Context(keys)
-
-			# add custom error messages for broken colorschemes
-			color = self.use(context)
-			if self.settings.colorscheme_overlay:
-				result = self.settings.colorscheme_overlay(context, *color)
-				assert isinstance(result, (tuple, list)), \
-						"Your colorscheme overlay doesn't return a tuple!"
-				assert all(isinstance(val, int) for val in result), \
-						"Your colorscheme overlay doesn't return a tuple"\
-						" containing 3 integers!"
-				color = result
-			self.cache[keys] = color
-			return color
-
+		context = Context(keys)
+
+		# add custom error messages for broken colorschemes
+		color = self.use(context)
+		if self.settings.colorscheme_overlay:
+			result = self.settings.colorscheme_overlay(context, *color)
+			assert isinstance(result, (tuple, list)), \
+					"Your colorscheme overlay doesn't return a tuple!"
+			assert all(isinstance(val, int) for val in result), \
+					"Your colorscheme overlay doesn't return a tuple"\
+					" containing 3 integers!"
+			color = result
+		return color
+
+	@cached_function
 	def get_attr(self, *keys):
 		"""
 		Returns the curses attribute for the specified keys
 
 		Ready to use for curses.setattr()
 		"""
-		fg, bg, attr = self.get(*keys)
+		fg, bg, attr = self.get(*flatten(keys))
 		return attr | color_pair(get_color(fg, bg))
 
 	def use(self, context):
diff --git a/ranger/gui/curses_shortcuts.py b/ranger/gui/curses_shortcuts.py
index 10a159a1..a383ab4c 100644
--- a/ranger/gui/curses_shortcuts.py
+++ b/ranger/gui/curses_shortcuts.py
@@ -17,7 +17,6 @@
 import curses
 import _curses
 
-from ranger.ext.iter_tools import flatten
 from ranger.gui.color import get_color
 from ranger.core.shared import SettingsAware
 
@@ -63,7 +62,6 @@ class CursesShortcuts(SettingsAware):
 
 	def color(self, *keys):
 		"""Change the colors from now on."""
-		keys = flatten(keys)
 		attr = self.settings.colorscheme.get_attr(*keys)
 		try:
 			self.win.attrset(attr)
@@ -72,7 +70,6 @@ class CursesShortcuts(SettingsAware):
 
 	def color_at(self, y, x, wid, *keys):
 		"""Change the colors at the specified position"""
-		keys = flatten(keys)
 		attr = self.settings.colorscheme.get_attr(*keys)
 		try:
 			self.win.chgat(y, x, wid, attr)
diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py
index 91d0d774..e6c7d065 100644
--- a/ranger/gui/ui.py
+++ b/ranger/gui/ui.py
@@ -23,8 +23,8 @@ from .mouse_event import MouseEvent
 from ranger.ext.keybinding_parser import ALT_KEY
 
 TERMINALS_WITH_TITLE = ("xterm", "xterm-256color", "rxvt",
-		"rxvt-256color", "rxvt-unicode", "aterm", "Eterm",
-		"screen", "screen-256color")
+		"rxvt-256color", "rxvt-unicode", "rxvt-unicode-256color",
+		"aterm", "Eterm", "screen", "screen-256color")
 
 MOUSEMASK = curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION
 
@@ -293,10 +293,10 @@ class UI(DisplayableContainer):
 				split = cwd.rsplit(os.sep, self.settings.shorten_title)
 				if os.sep in split[0]:
 					cwd = os.sep.join(split[1:])
-			try:
-				sys.stdout.write("\033]2;ranger:" + cwd + "\007")
-			except UnicodeEncodeError:
-				sys.stdout.write("\033]2;ranger:" + ascii_only(cwd) + "\007")
+			fixed_cwd = cwd.encode('utf-8', 'surrogateescape'). \
+					decode('utf-8', 'replace')
+			sys.stdout.write("\033]2;ranger:" + fixed_cwd + "\007")
+			sys.stdout.flush()
 		self.win.refresh()
 
 	def finalize(self):
diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py
index b6b745aa..e9c08439 100644
--- a/ranger/gui/widgets/browsercolumn.py
+++ b/ranger/gui/widgets/browsercolumn.py
@@ -178,7 +178,7 @@ class BrowserColumn(Pager):
 		self.win.move(0, 0)
 
 		if not self.target.content_loaded:
-			self.color(base_color)
+			self.color(tuple(base_color))
 			self.addnstr("...", self.wid)
 			self.color_reset()
 			return
@@ -187,13 +187,13 @@ class BrowserColumn(Pager):
 			base_color.append('main_column')
 
 		if not self.target.accessible:
-			self.color(base_color, 'error')
+			self.color(tuple(base_color + ['error']))
 			self.addnstr("not accessible", self.wid)
 			self.color_reset()
 			return
 
 		if self.target.empty():
-			self.color(base_color, 'empty')
+			self.color(tuple(base_color + ['empty']))
 			self.addnstr("empty", self.wid)
 			self.color_reset()
 			return
@@ -289,15 +289,15 @@ class BrowserColumn(Pager):
 				if x > 0:
 					self.addstr(line, x, infostring)
 
-			self.color_at(line, 0, self.wid, this_color)
+			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, this_color, 'badinfo')
+				self.color_at(line, start, wid, tuple(this_color), 'badinfo')
 
 			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), this_color)
+				self.color_at(line, 0, len(tagged_marker), tuple(this_color))
 
 			self.color_reset()
 
diff --git a/ranger/gui/widgets/browserview.py b/ranger/gui/widgets/browserview.py
index d386d389..ea04c1e0 100644
--- a/ranger/gui/widgets/browserview.py
+++ b/ranger/gui/widgets/browserview.py
@@ -215,7 +215,10 @@ class BrowserView(Widget, DisplayableContainer):
 		ystart = self.hei - hei
 		self.addnstr(ystart - 1, 0, "key          command".ljust(self.wid),
 				self.wid)
-		self.win.chgat(ystart - 1, 0, curses.A_UNDERLINE)
+		try:
+			self.win.chgat(ystart - 1, 0, curses.A_UNDERLINE)
+		except:
+			pass
 		whitespace = " " * self.wid
 		i = ystart
 		for key, cmd in hints:
diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py
index 1e2e2520..1ffb9fa3 100644
--- a/ranger/gui/widgets/statusbar.py
+++ b/ranger/gui/widgets/statusbar.py
@@ -159,7 +159,10 @@ class StatusBar(Widget):
 		if stat is None:
 			return
 
-		perms = target.get_permission_string()
+		if self.fm.mode != 'normal':
+			perms = '--%s--' % self.fm.mode.upper()
+		else:
+			perms = target.get_permission_string()
 		how = getuid() == stat.st_uid and 'good' or 'bad'
 		left.add(perms, 'permissions', how)
 		left.add_space()
@@ -231,17 +234,21 @@ class StatusBar(Widget):
 
 		if target.marked_items:
 			if len(target.marked_items) == len(target.files):
-				right.add(human_readable(target.disk_usage, seperator=''))
+				right.add(human_readable(target.disk_usage, separator=''))
 			else:
-				right.add(human_readable(sum(f.size \
-					for f in target.marked_items \
-					if f.is_file), seperator=''))
+				sumsize = sum(f.size for f in target.marked_items if not
+						f.is_directory or f._cumulative_size_calculated)
+				right.add(human_readable(sumsize, separator=''))
 			right.add("/" + str(len(target.marked_items)))
 		else:
-			right.add(human_readable(target.disk_usage, seperator='') +
-					" sum, ")
-			right.add(human_readable(self.env.get_free_space( \
-					target.mount_path), seperator='') + " free")
+			right.add(human_readable(target.disk_usage, separator='') + " sum")
+			try:
+				free = self.env.get_free_space(target.mount_path)
+			except OSError:
+				pass
+			else:
+				right.add(", ", "space")
+				right.add(human_readable(free, separator='') + " free")
 		right.add("  ", "space")
 
 		if target.marked_items:
@@ -251,7 +258,7 @@ class StatusBar(Widget):
 		elif len(target.files):
 			right.add(str(target.pointer + 1) + '/'
 					+ str(len(target.files)) + '  ', base)
-			if max_pos == 0:
+			if max_pos <= 0:
 				right.add('All', base, 'all')
 			elif pos == 0:
 				right.add('Top', base, 'top')
diff --git a/ranger/gui/widgets/taskview.py b/ranger/gui/widgets/taskview.py
index c4476b9c..a3f8e314 100644
--- a/ranger/gui/widgets/taskview.py
+++ b/ranger/gui/widgets/taskview.py
@@ -17,8 +17,6 @@
 The TaskView allows you to modify what the loader is doing.
 """
 
-from collections import deque
-
 from . import Widget
 from ranger.ext.accumulator import Accumulator
 
@@ -31,7 +29,7 @@ class TaskView(Widget, Accumulator):
 		self.scroll_begin = 0
 
 	def draw(self):
-		base_clr = deque()
+		base_clr = []
 		base_clr.append('in_taskview')
 		lst = self.get_list()
 
@@ -48,7 +46,7 @@ class TaskView(Widget, Accumulator):
 				return
 
 			self.addstr(0, 0, "Task View")
-			self.color_at(0, 0, self.wid, base_clr, 'title')
+			self.color_at(0, 0, self.wid, tuple(base_clr), 'title')
 
 			if lst:
 				for i in range(self.hei - 1):
@@ -59,19 +57,19 @@ class TaskView(Widget, Accumulator):
 						break
 
 					y = i + 1
-					clr = deque(base_clr)
+					clr = list(base_clr)
 
 					if self.pointer == i:
 						clr.append('selected')
 
 					descr = obj.get_description()
 					self.addstr(y, 0, descr, self.wid)
-					self.color_at(y, 0, self.wid, clr)
+					self.color_at(y, 0, self.wid, tuple(clr))
 
 			else:
 				if self.hei > 1:
 					self.addstr(1, 0, "No task in the queue.")
-					self.color_at(1, 0, self.wid, base_clr, 'error')
+					self.color_at(1, 0, self.wid, tuple(base_clr), 'error')
 
 			self.color_reset()
 
diff --git a/ranger/gui/widgets/titlebar.py b/ranger/gui/widgets/titlebar.py
index 6b92ccfa..2b5e836b 100644
--- a/ranger/gui/widgets/titlebar.py
+++ b/ranger/gui/widgets/titlebar.py
@@ -102,6 +102,7 @@ class TitleBar(Widget):
 		self.result = bar.combine()
 
 	def _get_left_part(self, bar):
+		# TODO: Properly escape non-printable chars without breaking unicode
 		if self.env.username == 'root':
 			clr = 'bad'
 		else:
@@ -160,5 +161,6 @@ class TitleBar(Widget):
 		self.win.move(0, 0)
 		for part in result:
 			self.color(*part.lst)
-			self.addstr(str(part))
+			y, x = self.win.getyx()
+			self.addstr(y, x, str(part))
 		self.color_reset()