summary refs log tree commit diff stats
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
parent2bcfbe93584f3e9bdd4f80ff4cce43e39a545914 (diff)
parent23b7f16961a7b679dec16f6ee91401a6b8f6ca82 (diff)
downloadranger-e1155824b39d584beb8c178ceca8999eb7783daf.tar.gz
Merge branch 'master' into stable
-rw-r--r--README9
-rw-r--r--doc/ranger.143
-rw-r--r--doc/ranger.pod45
-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
32 files changed, 661 insertions, 142 deletions
diff --git a/README b/README
index 84bf30a3..bc67c16b 100644
--- a/README
+++ b/README
@@ -56,6 +56,7 @@ Dependencies
 Optional:
 * The "file" program for determining file types
 * The python module "chardet", in case of encoding detection problems
+* "sudo" to use the "run as root"-feature
 
 Optional, for enhanced file previews (with "scope.sh"):
 * img2txt (from caca-utils) for previewing images
@@ -89,7 +90,7 @@ to open a file or type Q to quit.  The third column shows a preview of the
 current file.  The second is the main column and the first shows the parent
 directory.
 
-Ranger will automatically copy simple configuration files to ~/.config/ranger.
-If you mess them up, just delete them and ranger will copy them again. Run
-ranger with --dont-copy-config to disable this.  Also check ranger/defaults/
-for the default configuration.
+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
+configuration.
diff --git a/doc/ranger.1 b/doc/ranger.1
index e9f00501..433bb8a7 100644
--- a/doc/ranger.1
+++ b/doc/ranger.1
@@ -124,7 +124,7 @@
 .\" ========================================================================
 .\"
 .IX Title "RANGER 1"
-.TH RANGER 1 "ranger-1.5.2" "10/24/2011" "ranger manual"
+.TH RANGER 1 "ranger-1.5.2" "03/05/2012" "ranger manual"
 .\" For nroff, turn off justification.  Always turn off hyphenation; it makes
 .\" way too many mistakes in technical documents.
 .if n .ad l
@@ -178,6 +178,9 @@ selected files into \fItargetfile\fR, adding one newline after each filename.
 .IX Item "--choosedir=targetfile"
 Allows you to pick a directory with ranger.  When you exit ranger, it will
 write the last visited directory into \fItargetfile\fR.
+.IP "\fB\-\-selectfile\fR=\fItargetfile\fR" 14
+.IX Item "--selectfile=targetfile"
+Open ranger with \fItargetfile\fR selected.
 .IP "\fB\-\-copy\-config\fR=\fIfile\fR" 14
 .IX Item "--copy-config=file"
 Create copies of the default configuration files in your local configuration
@@ -188,6 +191,10 @@ directory.  Existing ones will not be overwritten.  Possible values: \fIall\fR,
 List common keys which are not bound to any action in the \*(L"browser\*(R" context.
 This list is not complete, you can bind any key that is supported by curses:
 use the key code returned by \f(CW\*(C`getch()\*(C'\fR.
+.IP "\fB\-\-list\-tagged\-files\fR=\fItag\fR" 14
+.IX Item "--list-tagged-files=tag"
+List all files which are tagged with the given tag.  Note: Tags are single
+characters.  The default tag is \*(L"*\*(R"
 .IP "\fB\-\-fail\-unless\-cd\fR" 14
 .IX Item "--fail-unless-cd"
 Return the exit code 1 if ranger is used to run a file instead of used for file
@@ -201,6 +208,10 @@ the execution of this file type is explicitly handled in the configuration.
 When a filename is supplied, run it with the given \fIflags\fR to modify
 behavior.  The execution of this file type is explicitly handled in the
 configuration.
+.IP "\fB\-\-cmd\fR=\fIcommand\fR" 14
+.IX Item "--cmd=command"
+Execute the command after the configuration has been read.  Use this option
+multiple times to run multiple commands.
 .IP "\fB\-\-version\fR" 14
 .IX Item "--version"
 Print the version and exit.
@@ -279,18 +290,23 @@ Note: The bookmarks ' (Apostrophe) and ` (Backtick) are the same.
 Flags give you a way to modify the behavior of the spawned process.  They are
 used in the commands :open_with (key \*(L"r\*(R") and :shell (key \*(L"!\*(R").
 .PP
-.Vb 5
+.Vb 7
 \& 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 (requires sudo)
+\& t   Run application in a new terminal window
 .Ve
 .PP
 By default, all the flags are off unless specified otherwise in the \fIapps.py\fR
 configuration file.  You can specify as many flags as you want.  An uppercase
 flag negates the effect: \*(L"ddcccDs\*(R" is equivalent to \*(L"cs\*(R".
 .PP
+The \*(L"t\*(R" flag looks for the environment variable \s-1TERMCMD\s0, and uses it as the
+terminal command, if it's not set it'll use xterm.
+.PP
 Examples: \f(CW\*(C`:open_with p\*(C'\fR will pipe the output of that process into
 the pager.  \f(CW\*(C`:shell \-w df\*(C'\fR will run \*(L"df\*(R" and wait for you to press Enter before
 switching back to ranger.
@@ -733,6 +749,9 @@ it by typing `` or '' the next time you start ranger.
 .IX Item "rename newname"
 Rename the current file.  If a file with that name already exists, the renaming
 will fail.  Also try the key binding A for appending something to a file name.
+.IP "relink \fInewpath\fR" 2
+.IX Item "relink newpath"
+Change the link destination of the current symlink file to <newpath>. First <tab> will load the original link.
 .IP "save_copy_buffer" 2
 .IX Item "save_copy_buffer"
 Save the copy buffer from \fI~/.config/ranger/copy_buffer\fR.  This can be used to
@@ -853,6 +872,11 @@ with T.  To assign a named tag, type "<tagname>.
 .SH "ENVIRONMENT"
 .IX Header "ENVIRONMENT"
 These environment variables have an effect on ranger:
+.IP "\s-1RANGER_LEVEL\s0" 8
+.IX Item "RANGER_LEVEL"
+Ranger sets this environment variable to \*(L"1\*(R" or increments it if it already
+exists.  External programs can determine whether they were spawned from ranger
+by checking for this variable.
 .IP "\s-1EDITOR\s0" 8
 .IX Item "EDITOR"
 Defines the editor to be used for the \*(L"E\*(R" key.  Defaults to the first installed
@@ -861,6 +885,11 @@ program out of \*(L"vim\*(R", \*(L"emacs\*(R" and \*(L"nano\*(R".
 .IX Item "SHELL"
 Defines the shell that ranger is going to use with the :shell command and
 the \*(L"S\*(R" key.  Defaults to \*(L"bash\*(R".
+.IP "\s-1TERMCMD\s0" 8
+.IX Item "TERMCMD"
+Defines the terminal emulator command that ranger is going to use with the
+:terminal command and the \*(L"t\*(R" run flag.  Defaults to \*(L"x\-terminal-emulator\*(R" or
+\&\*(L"xterm\*(R"
 .IP "\s-1XDG_CONFIG_HOME\s0" 8
 .IX Item "XDG_CONFIG_HOME"
 Specifies the directory for configuration files. Defaults to \fI\f(CI$HOME\fI/.config\fR.
@@ -875,6 +904,14 @@ Using PYTHONOPTIMIZE=2 (like python \-OO) will additionally discard any
 docstrings.  Using this will disable the <F1> key on commands.
 .SH "EXAMPLES"
 .IX Header "EXAMPLES"
+.SS "\s-1BASH:\s0 Display that the shell spawned from ranger:"
+.IX Subsection "BASH: Display that the shell spawned from ranger:"
+By putting this in ~/.bashrc, \*(L"(in ranger) \*(R" will be displayed next to your
+prompt to notify you that the shell spawned from ranger.
+.PP
+.Vb 1
+\& [ \-n "$RANGER_LEVEL" ] && PS1="$PS1"\*(Aq(in ranger) \*(Aq
+.Ve
 .SS "\s-1VIM:\s0 File Chooser"
 .IX Subsection "VIM: File Chooser"
 This is a vim function which allows you to use ranger to select a file for
@@ -882,7 +919,7 @@ opening in your current vim session.
 .PP
 .Vb 9
 \& fun! RangerChooser()
-\&   silent !ranger \-\-choosefile=/tmp/chosenfile \`[ \-z \*(Aq%\*(Aq ] && echo \-n . || dirname %\`
+\&   exec "silent !ranger \-\-choosefile=/tmp/chosenfile " . expand("%:p:h")
 \&   if filereadable(\*(Aq/tmp/chosenfile\*(Aq)
 \&     exec \*(Aqedit \*(Aq . system(\*(Aqcat /tmp/chosenfile\*(Aq)
 \&     call system(\*(Aqrm /tmp/chosenfile\*(Aq)
diff --git a/doc/ranger.pod b/doc/ranger.pod
index 069b9de1..9f8b4f04 100644
--- a/doc/ranger.pod
+++ b/doc/ranger.pod
@@ -67,6 +67,10 @@ selected files into I<targetfile>, adding one newline after each filename.
 Allows you to pick a directory with ranger.  When you exit ranger, it will
 write the last visited directory into I<targetfile>.
 
+=item B<--selectfile>=I<targetfile>
+
+Open ranger with I<targetfile> selected.
+
 =item B<--copy-config>=I<file>
 
 Create copies of the default configuration files in your local configuration
@@ -79,6 +83,11 @@ List common keys which are not bound to any action in the "browser" context.
 This list is not complete, you can bind any key that is supported by curses:
 use the key code returned by C<getch()>.
 
+=item B<--list-tagged-files>=I<tag>
+
+List all files which are tagged with the given tag.  Note: Tags are single
+characters.  The default tag is "*"
+
 =item B<--fail-unless-cd>
 
 Return the exit code 1 if ranger is used to run a file instead of used for file
@@ -95,6 +104,11 @@ When a filename is supplied, run it with the given I<flags> to modify
 behavior.  The execution of this file type is explicitly handled in the
 configuration.
 
+=item B<--cmd>=I<command>
+
+Execute the command after the configuration has been read.  Use this option
+multiple times to run multiple commands.
+
 =item B<--version>
 
 Print the version and exit.
@@ -186,11 +200,16 @@ used in the commands :open_with (key "r") and :shell (key "!").
  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 (requires sudo)
+ t   Run application in a new terminal window
 
 By default, all the flags are off unless specified otherwise in the F<apps.py>
 configuration file.  You can specify as many flags as you want.  An uppercase
 flag negates the effect: "ddcccDs" is equivalent to "cs".
 
+The "t" flag looks for the environment variable TERMCMD, and uses it as the
+terminal command, if it's not set it'll use xterm.
+
 Examples: C<:open_with p> will pipe the output of that process into
 the pager.  C<:shell -w df> will run "df" and wait for you to press Enter before
 switching back to ranger.
@@ -778,6 +797,10 @@ it by typing `` or '' the next time you start ranger.
 Rename the current file.  If a file with that name already exists, the renaming
 will fail.  Also try the key binding A for appending something to a file name.
 
+=item relink I<newpath>
+
+Change the link destination of the current symlink file to <newpath>. First <tab> will load the original link.
+
 =item save_copy_buffer
 
 Save the copy buffer from I<~/.config/ranger/copy_buffer>.  This can be used to
@@ -937,6 +960,12 @@ These environment variables have an effect on ranger:
 
 =over 8
 
+=item RANGER_LEVEL
+
+Ranger sets this environment variable to "1" or increments it if it already
+exists.  External programs can determine whether they were spawned from ranger
+by checking for this variable.
+
 =item EDITOR
 
 Defines the editor to be used for the "E" key.  Defaults to the first installed
@@ -947,6 +976,13 @@ program out of "vim", "emacs" and "nano".
 Defines the shell that ranger is going to use with the :shell command and
 the "S" key.  Defaults to "bash".
 
+=item TERMCMD
+
+Defines the terminal emulator command that ranger is going to use with the
+:terminal command and the "t" run flag.  Defaults to "x-terminal-emulator" or
+"xterm"
+
+
 =item XDG_CONFIG_HOME
 
 Specifies the directory for configuration files. Defaults to F<$HOME/.config>.
@@ -968,13 +1004,20 @@ docstrings.  Using this will disable the <F1> key on commands.
 
 =head1 EXAMPLES
 
+=head2 BASH: Display that the shell spawned from ranger:
+
+By putting this in ~/.bashrc, "(in ranger) " will be displayed next to your
+prompt to notify you that the shell spawned from ranger.
+
+ [ -n "$RANGER_LEVEL" ] && PS1="$PS1"'(in ranger) '
+
 =head2 VIM: File Chooser
 
 This is a vim function which allows you to use ranger to select a file for
 opening in your current vim session.
 
  fun! RangerChooser()
-   silent !ranger --choosefile=/tmp/chosenfile `[ -z '%' ] && echo -n . || dirname %`
+   exec "silent !ranger --choosefile=/tmp/chosenfile " . expand("%:p:h")
    if filereadable('/tmp/chosenfile')
      exec 'edit ' . system('cat /tmp/chosenfile')
      call system('rm /tmp/chosenfile')
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()