diff options
author | hut <hut@lavabit.com> | 2012-03-05 11:56:05 +0100 |
---|---|---|
committer | hut <hut@lavabit.com> | 2012-03-05 11:56:05 +0100 |
commit | e1155824b39d584beb8c178ceca8999eb7783daf (patch) | |
tree | 22dae0662d03ce8562159d9196fd8c854d6af009 /ranger | |
parent | 2bcfbe93584f3e9bdd4f80ff4cce43e39a545914 (diff) | |
parent | 23b7f16961a7b679dec16f6ee91401a6b8f6ca82 (diff) | |
download | ranger-e1155824b39d584beb8c178ceca8999eb7783daf.tar.gz |
Merge branch 'master' into stable
Diffstat (limited to 'ranger')
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() |