diff options
-rw-r--r-- | README.md | 4 | ||||
-rw-r--r-- | doc/ranger.pod | 7 | ||||
-rw-r--r-- | examples/rc_emacs.conf | 1 | ||||
-rw-r--r-- | ranger/api/commands.py | 2 | ||||
-rw-r--r-- | ranger/config/commands.py | 14 | ||||
-rw-r--r-- | ranger/config/rifle.conf | 8 | ||||
-rw-r--r-- | ranger/core/actions.py | 107 | ||||
-rw-r--r-- | ranger/core/fm.py | 7 | ||||
-rw-r--r-- | ranger/core/loader.py | 10 | ||||
-rwxr-xr-x | ranger/data/scope.sh | 8 | ||||
-rwxr-xr-x | ranger/ext/rifle.py | 9 | ||||
-rw-r--r-- | ranger/gui/colorscheme.py | 2 | ||||
-rw-r--r-- | ranger/gui/curses_shortcuts.py | 8 | ||||
-rw-r--r-- | ranger/gui/widgets/__init__.py | 2 | ||||
-rw-r--r-- | ranger/gui/widgets/browsercolumn.py | 7 | ||||
-rw-r--r-- | ranger/gui/widgets/titlebar.py | 4 |
16 files changed, 153 insertions, 47 deletions
diff --git a/README.md b/README.md index f0d3ca25..57b2631d 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,8 @@ Features Dependencies ------------ -* Python (tested with version 2.6, 2.7, 3.1, 3.2) with support for ncurses - and (optionally) wide-unicode. +* Python (tested with version 2.6, 2.7, 3.1, 3.2) with the "curses" module + and (optionally) wide-unicode support. * A pager ("less" by default) Optional: diff --git a/doc/ranger.pod b/doc/ranger.pod index b68595a9..6c66f817 100644 --- a/doc/ranger.pod +++ b/doc/ranger.pod @@ -201,12 +201,13 @@ Macros can be used in commands to abbreviate things. %f the highlighted file %d the path of the current directory - %s the selected files in the current directory. + %s the selected files in the current directory %t all tagged files in the current directory %c the full paths of the currently copied/cut files + %p the full paths of selected files -The macros %f, %d and %s also have upper case variants, %F, %D and %S, -which refer to the next tab. To refer to specific tabs, add a number in +The macros %f, %d, %p, and %s also have upper case variants, %F, %D, %P, and +%S, which refer to the next tab. To refer to specific tabs, add a number in between. (%7s = selection of the seventh tab.) %c is the only macro which ranges out of the current directory. So you may diff --git a/examples/rc_emacs.conf b/examples/rc_emacs.conf index e8b76ff2..f5e66b89 100644 --- a/examples/rc_emacs.conf +++ b/examples/rc_emacs.conf @@ -419,7 +419,6 @@ eval for arg in "rwxXst": cmd("map <C-x>-{0} shell -f chmod u-{0} %s".format(ar # Search for letters as you type them #eval for arg in "abcdefghijklmnopqrstuvwxyz": cmd("map {0} console search_inc {0}".format(arg)) -map <allow_quantifiers> false # =================================================================== # == Define keys for the console diff --git a/ranger/api/commands.py b/ranger/api/commands.py index 2cf96a9f..ca713d0c 100644 --- a/ranger/api/commands.py +++ b/ranger/api/commands.py @@ -47,7 +47,7 @@ class CommandContainer(object): continue attribute = getattr(obj, attribute_name) if hasattr(attribute, '__call__'): - cmd = type(attribute_name, (FunctionCommand, ), dict()) + cmd = type(attribute_name, (FunctionCommand, ), dict(__doc__=attribute.__doc__)) cmd._based_function = attribute cmd._function_name = attribute.__name__ cmd._object_name = obj.__class__.__name__ diff --git a/ranger/config/commands.py b/ranger/config/commands.py index 9f0481ce..4f7f109a 100644 --- a/ranger/config/commands.py +++ b/ranger/config/commands.py @@ -226,7 +226,7 @@ class shell(Command): else: before_word, start_of_word = self.line.rsplit(' ', 1) return (before_word + ' ' + file.shell_escaped_basename \ - for file in self.fm.thisdir.files \ + for file in self.fm.thisdir.files or [] \ if file.shell_escaped_basename.startswith(start_of_word)) class open_with(Command): @@ -342,12 +342,12 @@ class set_(Command): if not name: return sorted(self.firstpart + setting for setting in settings) if not value and not name_done: - return (self.firstpart + setting for setting in settings \ + return sorted(self.firstpart + setting for setting in settings \ if setting.startswith(name)) if not value: # Cycle through colorschemes when name, but no value is specified if name == "colorscheme": - return (self.firstpart + colorscheme for colorscheme \ + return sorted(self.firstpart + colorscheme for colorscheme \ in get_all_colorschemes()) return self.firstpart + str(settings[name]) if bool in settings.types_of(name): @@ -357,7 +357,7 @@ class set_(Command): return self.firstpart + 'False' # Tab complete colorscheme values if incomplete value is present if name == "colorscheme": - return (self.firstpart + colorscheme for colorscheme \ + return sorted(self.firstpart + colorscheme for colorscheme \ in get_all_colorschemes() if colorscheme.startswith(value)) @@ -548,7 +548,7 @@ class mark_tag(Command): def execute(self): cwd = self.fm.thisdir tags = self.rest(1).replace(" ","") - if not self.fm.tags: + if not self.fm.tags or not cwd.files: return for fileobj in cwd.files: try: @@ -1131,7 +1131,7 @@ class scout(Command): self.fm.thistab.last_search = regex self.fm.set_search_method(order="search") - if self.MARK in flags or self.UNMARK in flags: + if (self.MARK in flags or self.UNMARK in flags) and thisdir.files: value = flags.find(self.MARK) > flags.find(self.UNMARK) if self.FILTER in flags: for f in thisdir.files: @@ -1234,7 +1234,7 @@ class scout(Command): cwd = self.fm.thisdir pattern = self.pattern - if not pattern: + if not pattern or not cwd.files: return 0 if pattern == '.': return 0 diff --git a/ranger/config/rifle.conf b/ranger/config/rifle.conf index 95e4242a..31db35a4 100644 --- a/ranger/config/rifle.conf +++ b/ranger/config/rifle.conf @@ -57,6 +57,7 @@ ext x?html?, has surf, X, flag f = surf -- file://"$1" ext x?html?, has vimprobable, X, flag f = vimprobable -- "$@" ext x?html?, has vimprobable2, X, flag f = vimprobable2 -- "$@" +ext x?html?, has qutebrowser, X, flag f = qutebrowser -- "$@" ext x?html?, has dwb, X, flag f = dwb -- "$@" ext x?html?, has jumanji, X, flag f = jumanji -- "$@" ext x?html?, has luakit, X, flag f = luakit -- "$@" @@ -107,9 +108,9 @@ ext php = php -- "$1" #-------------------------------------------- # Audio without X #------------------------------------------- -mime ^audio|ogg$, terminal, has mplayer = mplayer -- "$@" -mime ^audio|ogg$, terminal, has mplayer2 = mplayer2 -- "$@" mime ^audio|ogg$, terminal, has mpv = mpv -- "$@" +mime ^audio|ogg$, terminal, has mplayer2 = mplayer2 -- "$@" +mime ^audio|ogg$, terminal, has mplayer = mplayer -- "$@" ext midi?, terminal, has wildmidi = wildmidi -- "$@" #-------------------------------------------- @@ -201,3 +202,6 @@ label wallpaper, number 14, mime ^image, has feh, X = feh --bg-fill "$1" !mime ^text, !ext xml|json|csv|tex|py|pl|rb|js|sh|php = ask label editor, !mime ^text, !ext xml|json|csv|tex|py|pl|rb|js|sh|php = $EDITOR -- "$@" label pager, !mime ^text, !ext xml|json|csv|tex|py|pl|rb|js|sh|php = "$PAGER" -- "$@" + +# The very last action, so that it's never triggered accidently, is to execute a program: +mime application/x-executable = "$1" diff --git a/ranger/core/actions.py b/ranger/core/actions.py index 8167dbcf..cbf75dbf 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -41,11 +41,17 @@ class Actions(FileManagerAware, SettingsAware): # -------------------------- def exit(self): - """Exit the program""" + """:exit + + Exit the program. + """ raise SystemExit() def reset(self): - """Reset the filemanager, clearing the directory buffer""" + """:reset + + Reset the filemanager, clearing the directory buffer. + """ old_path = self.thisdir.path self.previews = {} self.garbage_collect(-1) @@ -55,6 +61,10 @@ class Actions(FileManagerAware, SettingsAware): self.metadata.reset() def change_mode(self, mode): + """:change_mode <mode> + + Change mode to "visual" (selection) or "normal" mode. + """ if mode == self.mode: return if mode == 'visual': @@ -103,6 +113,10 @@ class Actions(FileManagerAware, SettingsAware): raise ValueError("Invalid value `%s' for option `%s'!" % (name, value)) def toggle_visual_mode(self, reverse=False, narg=None): + """:toggle_visual_mode + + Toggle the visual mode (see :change_mode). + """ if self.mode == 'normal': self._visual_reverse = reverse if narg != None: @@ -112,14 +126,23 @@ class Actions(FileManagerAware, SettingsAware): self.change_mode('normal') def reload_cwd(self): + """:reload_cwd + + Reload the current working directory. + """ try: cwd = self.thisdir except: pass - cwd.unload() - cwd.load_content() + else: + cwd.unload() + cwd.load_content() def notify(self, text, duration=4, bad=False): + """:notify <text> + + Display the text in the statusbar. + """ if isinstance(text, Exception): if ranger.arg.debug: raise @@ -135,6 +158,10 @@ class Actions(FileManagerAware, SettingsAware): print(text) def abort(self): + """:abort + + Empty the first queued action. + """ try: item = self.loader.queue[0] except: @@ -150,16 +177,25 @@ class Actions(FileManagerAware, SettingsAware): self.ui.redraw_main_column() def redraw_window(self): - """Redraw the window""" + """:redraw + + Redraw the window. + """ self.ui.redraw_window() def open_console(self, string='', prompt=None, position=None): - """Open the console""" + """:open_console [string] + + Open the console. + """ self.change_mode('normal') self.ui.open_console(string, prompt=prompt, position=position) def execute_console(self, string='', wildcards=[], quantifier=None): - """Execute a command for the console""" + """:execute_console [string] + + Execute a command for the console + """ command_name = string.lstrip().split()[0] cmd_class = self.commands.get_command(command_name, abbrev=False) if cmd_class is None: @@ -217,8 +253,11 @@ class Actions(FileManagerAware, SettingsAware): macros['f'] = MACRO_FAIL if self.fm.thistab.get_selection: + macros['p'] = [os.path.join(self.fm.thisdir.path, fl.relative_path) + for fl in self.fm.thistab.get_selection()] macros['s'] = [fl.relative_path for fl in self.fm.thistab.get_selection()] else: + macros['p'] = MACRO_FAIL macros['s'] = MACRO_FAIL if self.fm.copy_buffer: @@ -237,7 +276,7 @@ class Actions(FileManagerAware, SettingsAware): else: macros['d'] = '.' - # define d/f/s macros for each tab + # define d/f/p/s macros for each tab for i in range(1,10): try: tab = self.fm.tabs[i] @@ -249,15 +288,18 @@ class Actions(FileManagerAware, SettingsAware): i = str(i) macros[i + 'd'] = tabdir.path if tabdir.get_selection(): + macros[i + 'p'] = [os.path.join(tabdir.path, fl.relative_path) + for fl in tabdir.get_selection()] macros[i + 's'] = [fl.path for fl in tabdir.get_selection()] else: + macros[i + 'p'] = MACRO_FAIL macros[i + 's'] = MACRO_FAIL if tabdir.pointed_obj: macros[i + 'f'] = tabdir.pointed_obj.path else: macros[i + 'f'] = MACRO_FAIL - # define D/F/S for the next tab + # define D/F/P/S for the next tab found_current_tab = False next_tab = None first_tab = None @@ -280,8 +322,11 @@ class Actions(FileManagerAware, SettingsAware): else: macros['F'] = MACRO_FAIL if next_tab_dir.get_selection(): + macros['P'] = [os.path.join(next_tab.path, fl.path) + for fl in next_tab.get_selection()] macros['S'] = [fl.path for fl in next_tab.get_selection()] else: + macros['P'] = MACRO_FAIL macros['S'] = MACRO_FAIL else: macros['D'] = MACRO_FAIL @@ -291,6 +336,10 @@ class Actions(FileManagerAware, SettingsAware): return macros def source(self, filename): + """:source <filename> + + Load a config file. + """ filename = os.path.expanduser(filename) for line in open(filename, 'r'): line = line.lstrip().rstrip("\r\n") @@ -538,12 +587,18 @@ class Actions(FileManagerAware, SettingsAware): self.execute_file(file, label='editor') def toggle_option(self, string): - """Toggle a boolean option named <string>""" + """:toggle_option <string> + + Toggle a boolean option named <string>. + """ if isinstance(self.settings[string], bool): self.settings[string] ^= True def set_option(self, optname, value): - """Set the value of an option named <optname>""" + """:set_option <optname> + + Set the value of an option named <optname>. + """ self.settings[optname] = value def sort(self, func=None, reverse=None): @@ -680,6 +735,10 @@ class Actions(FileManagerAware, SettingsAware): # file is important to you in any context. def tag_toggle(self, paths=None, value=None, movedown=None, tag=None): + """:tag_toggle <character> + + Toggle a tag <character>. + """ if not self.tags: return if paths is None: @@ -1113,8 +1172,10 @@ class Actions(FileManagerAware, SettingsAware): for cmd_name in sorted(self.commands.commands): cmd = self.commands.commands[cmd_name] if hasattr(cmd, '__doc__') and cmd.__doc__: - write(cleandoc(cmd.__doc__)) - write("\n\n" + "-" * 60 + "\n") + doc = cleandoc(cmd.__doc__) + if doc[0] == ':': + write(doc) + write("\n\n" + "-" * 60 + "\n") else: undocumented.append(cmd) @@ -1144,12 +1205,20 @@ class Actions(FileManagerAware, SettingsAware): # -------------------------- def uncut(self): + """:uncut + + Empty the copy buffer. + """ self.copy_buffer = set() self.do_cut = False self.ui.browser.main_column.request_redraw() def copy(self, mode='set', narg=None, dirarg=None): - """Copy the selected items. Modes are: 'set', 'add', 'remove'.""" + """:copy [mode=set] + + Copy the selected items. + Modes are: 'set', 'add', 'remove'. + """ assert mode in ('set', 'add', 'remove') cwd = self.thisdir if not narg and not dirarg: @@ -1176,6 +1245,11 @@ class Actions(FileManagerAware, SettingsAware): self.ui.browser.main_column.request_redraw() def cut(self, mode='set', narg=None, dirarg=None): + """:cut [mode=set] + + Cut the selected items. + Modes are: 'set, 'add, 'remove. + """ self.copy(mode=mode, narg=narg, dirarg=dirarg) self.do_cut = True self.ui.browser.main_column.request_redraw() @@ -1224,7 +1298,10 @@ class Actions(FileManagerAware, SettingsAware): next_available_filename(target_path)) def paste(self, overwrite=False, append=False): - """Paste the selected items into the current directory""" + """:paste + + Paste the selected items into the current directory. + """ loadable = CopyLoader(self.copy_buffer, self.do_cut, overwrite) self.loader.add(loadable, append=append) self.do_cut = False diff --git a/ranger/core/fm.py b/ranger/core/fm.py index 0313639a..046eb788 100644 --- a/ranger/core/fm.py +++ b/ranger/core/fm.py @@ -136,7 +136,7 @@ class FM(Actions, SignalDispatcher): if self.settings.open_all_images and \ len(self.thisdir.marked_items) == 0 and \ - re.match(r'^(feh|sxiv) ', command): + re.match(r'^(feh|sxiv|imv) ', command): images = [f.relative_path for f in self.thisdir.files if f.image] escaped_filenames = " ".join(shell_quote(f) \ @@ -156,6 +156,11 @@ class FM(Actions, SignalDispatcher): "feh --start-at %s " % \ shell_quote(self.thisfile.relative_path), 1) + if command[0:4] == 'imv ': + number = images.index(self.thisfile.relative_path) + 1 + new_command = command.replace("imv ", + "imv -n %d " % number, 1) + if new_command: command = "set -- %s; %s" % (escaped_filenames, new_command) diff --git a/ranger/core/loader.py b/ranger/core/loader.py index 411b16ec..2184d2b1 100644 --- a/ranger/core/loader.py +++ b/ranger/core/loader.py @@ -6,6 +6,7 @@ from time import time, sleep from subprocess import Popen, PIPE from ranger.core.shared import FileManagerAware from ranger.ext.signals import SignalDispatcher +from ranger.ext.human_readable import human_readable import math import os.path import sys @@ -77,13 +78,14 @@ class CopyLoader(Loadable, FileManagerAware): # TODO: Don't calculate size when renaming (needs detection) bytes_per_tick = shutil_g.BLOCK_SIZE size = max(1, self._calculate_size(bytes_per_tick)) + size_str = " (" + human_readable(self._calculate_size(1)) + ")" done = 0 if self.do_cut: self.original_copy_buffer.clear() if len(self.copy_buffer) == 1: - self.description = "moving: " + self.one_file.path + self.description = "moving: " + self.one_file.path + size_str else: - self.description = "moving files from: " + self.one_file.dirname + self.description = "moving files from: " + self.one_file.dirname + size_str for f in self.copy_buffer: for tf in self.fm.tags.tags: if tf == f.path or str(tf).startswith(f.path): @@ -101,9 +103,9 @@ class CopyLoader(Loadable, FileManagerAware): done += d else: if len(self.copy_buffer) == 1: - self.description = "copying: " + self.one_file.path + self.description = "copying: " + self.one_file.path + size_str else: - self.description = "copying files from: " + self.one_file.dirname + self.description = "copying files from: " + self.one_file.dirname + size_str for f in self.copy_buffer: if os.path.isdir(f.path) and not os.path.islink(f.path): d = 0 diff --git a/ranger/data/scope.sh b/ranger/data/scope.sh index 19404019..afaf131f 100755 --- a/ranger/data/scope.sh +++ b/ranger/data/scope.sh @@ -30,7 +30,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=$(/bin/echo "${path##*.}" | tr "[:upper:]" "[:lower:]") +extension=$(/bin/echo "${path##*.}" | awk '{print tolower($0)}') # Functions: # runs a command and saves its output into $output. Useful if you need @@ -44,7 +44,7 @@ dump() { /bin/echo "$output"; } trim() { head -n "$maxln"; } # wraps highlight to treat exit code 141 (killed by SIGPIPE) as success -highlight() { command highlight "$@"; test $? = 0 -o $? = 141; } +safepipe() { "$@"; test $? = 0 -o $? = 141; } # Image previews, if enabled in ranger. if [ "$preview_images" = "True" ]; then @@ -91,7 +91,9 @@ esac case "$mimetype" in # Syntax highlight for text files: text/* | */xml) - try highlight --out-format=ansi "$path" && { dump | trim; exit 5; } || exit 2;; + try safepipe highlight --out-format=ansi "$path" && { dump | trim; exit 5; } + try safepipe pygmentize "$path" && { dump | trim; exit 5; } + exit 2;; # Ascii-previews of images: image/*) img2txt --gamma=0.6 --width="$width" "$path" && exit 4 || exit 1;; diff --git a/ranger/ext/rifle.py b/ranger/ext/rifle.py index 504851b1..c43de24f 100755 --- a/ranger/ext/rifle.py +++ b/ranger/ext/rifle.py @@ -195,8 +195,11 @@ class Rifle(object): argument = rule[1] if len(rule) > 1 else '' if function == 'ext': - extension = os.path.basename(files[0]).rsplit('.', 1)[-1].lower() - return bool(re.search('^(' + argument + ')$', extension)) + if os.path.isfile(files[0]): + partitions = os.path.basename(files[0]).rpartition('.') + if not partitions[0]: + return False + return bool(re.search('^(' + argument + ')$', partitions[2].lower())) elif function == 'name': return bool(re.search(argument, os.path.basename(files[0]))) elif function == 'match': @@ -231,7 +234,7 @@ class Rifle(object): self._app_flags = argument return True elif function == 'X': - return 'DISPLAY' in os.environ + return sys.platform == 'darwin' or 'DISPLAY' in os.environ elif function == 'env': return bool(os.environ.get(argument)) elif function == 'else': diff --git a/ranger/gui/colorscheme.py b/ranger/gui/colorscheme.py index d6afcacc..d2b3b2d2 100644 --- a/ranger/gui/colorscheme.py +++ b/ranger/gui/colorscheme.py @@ -86,7 +86,7 @@ def _colorscheme_name_to_class(signal): usecustom = not ranger.arg.clean def exists(colorscheme): - return os.path.exists(colorscheme + '.py') + return os.path.exists(colorscheme + '.py') or os.path.exists(colorscheme + '.pyc') def is_scheme(x): try: diff --git a/ranger/gui/curses_shortcuts.py b/ranger/gui/curses_shortcuts.py index 187891c6..e7573f17 100644 --- a/ranger/gui/curses_shortcuts.py +++ b/ranger/gui/curses_shortcuts.py @@ -25,20 +25,28 @@ class CursesShortcuts(SettingsAware): """ def addstr(self, *args): + y, x = self.win.getyx() + try: self.win.addstr(*args) except: if len(args) > 1: + self.win.move(y, x) + try: self.win.addstr(*_fix_surrogates(args)) except: pass def addnstr(self, *args): + y, x = self.win.getyx() + try: self.win.addnstr(*args) except: if len(args) > 2: + self.win.move(y, x) + try: self.win.addnstr(*_fix_surrogates(args)) except: diff --git a/ranger/gui/widgets/__init__.py b/ranger/gui/widgets/__init__.py index f2e52c0a..bd0f2337 100644 --- a/ranger/gui/widgets/__init__.py +++ b/ranger/gui/widgets/__init__.py @@ -21,3 +21,5 @@ class Widget(Displayable): 'ahead': ('>', ["vcsahead"]), 'diverged': ('Y', ["vcsdiverged"]), 'unknown': ('?', ["vcsunknown"])} + + ellipsis = { False: '~', True: '…' } diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index b38d0b5b..52ef62b8 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of ranger, the console file manager. # License: GNU GPL version 3, see the file "AUTHORS" for details. @@ -7,6 +6,7 @@ import curses import stat from time import time +from os.path import splitext from . import Widget from .pager import Pager @@ -23,7 +23,6 @@ class BrowserColumn(Pager): scroll_begin = 0 target = None last_redraw_time = -1 - ellipsis = { False: '~', True: '…' } old_dir = None old_thisfile = None @@ -342,8 +341,12 @@ class BrowserColumn(Pager): def _draw_text_display(self, text, space): wtext = WideString(text) + wext = WideString(splitext(text)[1]) wellip = WideString(self.ellipsis[self.settings.unicode_ellipsis]) if len(wtext) > space: + wtext = wtext[:max(1, space - len(wext) - len(wellip))] + wellip + wext + # Truncate again if still too long. + if len(wtext) > space: wtext = wtext[:max(0, space - len(wellip))] + wellip return [[str(wtext), []]] diff --git a/ranger/gui/widgets/titlebar.py b/ranger/gui/widgets/titlebar.py index c3785986..dbd08981 100644 --- a/ranger/gui/widgets/titlebar.py +++ b/ranger/gui/widgets/titlebar.py @@ -98,7 +98,7 @@ class TitleBar(Widget): bar.add(self.fm.username, 'hostname', clr, fixed=True) bar.add('@', 'hostname', clr, fixed=True) bar.add(self.fm.hostname, 'hostname', clr, fixed=True) - bar.add(':', 'hostname', clr, fixed=True) + bar.add(' ', 'hostname', clr, fixed=True) pathway = self.fm.thistab.pathway if self.settings.tilde_in_titlebar and \ @@ -140,7 +140,7 @@ class TitleBar(Widget): if not dirname: result += ":/" elif len(dirname) > 15: - result += ":" + dirname[:14] + "~" + result += ":" + dirname[:14] + self.ellipsis[self.settings.unicode_ellipsis] else: result += ":" + dirname return result |