diff options
28 files changed, 576 insertions, 184 deletions
diff --git a/.gitignore b/.gitignore index 81e1b8b5..e97a99b7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ stuff/* doc/ranger.1.html build pytestdebug.log +install_log.txt diff --git a/doc/ranger.1 b/doc/ranger.1 index ee487e1e..db608eaf 100644 --- a/doc/ranger.1 +++ b/doc/ranger.1 @@ -133,7 +133,7 @@ .\" ======================================================================== .\" .IX Title "RANGER 1" -.TH RANGER 1 "ranger-1.7.2" "02/24/2016" "ranger manual" +.TH RANGER 1 "ranger-1.7.2" "04/15/2016" "ranger manual" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l @@ -332,6 +332,8 @@ The macro \f(CW%rangerdir\fR expands to the directory of ranger's python library can use it for something like this command: alias show_commands shell less \f(CW%rangerdir\fR/config/commands.py .PP +\&\f(CW%confdir\fR expands to the directory given by \fB\-\-confdir\fR. +.PP The macro \f(CW%space\fR expands to a space character. You can use it to add spaces to the end of a command when needed, while preventing editors to strip spaces off the end of the line automatically. @@ -487,10 +489,13 @@ Change the permissions of the selection. For example, \f(CW\*(C`777=\*(C'\fR is \&\f(CW\*(C`chmod 777 %s\*(C'\fR, \f(CW\*(C`+ar\*(C'\fR does \f(CW\*(C`chmod a+r %s\*(C'\fR, \f(CW\*(C`\-ow\*(C'\fR does \f(CW\*(C`chmod o\-w %s\*(C'\fR etc. .IP "yy" 14 .IX Item "yy" -Copy (yank) the selection, like pressing Ctrl+C in modern \s-1GUI\s0 programs. +Copy (yank) the selection, like pressing Ctrl+C in modern \s-1GUI\s0 programs. (You +can also type \*(L"ya\*(R" to add files to the copy buffer, \*(L"yr\*(R" to remove files again, +or \*(L"yt\*(R" for toggling.) .IP "dd" 14 .IX Item "dd" -Cut the selection, like pressing Ctrl+X in modern \s-1GUI\s0 programs. +Cut the selection, like pressing Ctrl+X in modern \s-1GUI\s0 programs. (There are +also \*(L"da\*(R", \*(L"dr\*(R" and \*(L"dt\*(R" shortcuts equivalent to \*(L"ya\*(R", \*(L"yr\*(R" and \*(L"yt\*(R".) .IP "pp" 14 .IX Item "pp" Paste the files which were previously copied or cut, like pressing Ctrl+V in @@ -630,6 +635,12 @@ fly with the command \fB:set option value\fR. Examples: \& set show_hidden true .Ve .PP +Toggling options can be done with: +.PP +.Vb 1 +\& set show_hidden! +.Ve +.PP The different types of settings and an example for each type: .PP .Vb 7 @@ -834,6 +845,9 @@ Sets the state for the version control backend. The possible values are: .IX Item "xterm_alt_key [bool]" Enable this if key combinations with the Alt Key don't work for you. (Especially on xterm) +.IP "clear_filters_on_dir_change [bool]" 4 +.IX Item "clear_filters_on_dir_change [bool]" +If set to 'true', persistent filters would be cleared upon leaving the directory .SH "COMMANDS" .IX Header "COMMANDS" You can enter the commands in the console which is opened by pressing \*(L":\*(R". @@ -889,6 +903,7 @@ ranger. For your convenience, this is a list of the \*(L"public\*(R" commands i \& setintag tags option value \& setlocal [path=<path>] option value \& shell [\-FLAGS] command +\& source filename \& terminal \& tmap key command \& touch filename @@ -1200,6 +1215,15 @@ use \f(CW\*(C`path=~/dl$\*(C'\fR. .IP "shell [\-\fIflags\fR] \fIcommand\fR" 2 .IX Item "shell [-flags] command" Run a shell command. \fIflags\fR are discussed in their own section. +.IP "source \fIfilename\fR" 2 +.IX Item "source filename" +Reads commands from a file and executes them in the ranger console. +.Sp +This can be used to re-evaluate the rc.conf file after changing it: +.Sp +.Vb 1 +\& map X chain shell vim \-p %confdir/rc.conf %rangerdir/config/rc.conf; source %confdir/rc.conf +.Ve .IP "terminal" 2 .IX Item "terminal" Spawns the \fIx\-terminal-emulator\fR starting in the current directory. diff --git a/doc/ranger.pod b/doc/ranger.pod index 91ba904a..43b248ce 100644 --- a/doc/ranger.pod +++ b/doc/ranger.pod @@ -228,6 +228,8 @@ The macro %rangerdir expands to the directory of ranger's python library, you can use it for something like this command: alias show_commands shell less %rangerdir/config/commands.py +%confdir expands to the directory given by B<--confdir>. + The macro %space expands to a space character. You can use it to add spaces to the end of a command when needed, while preventing editors to strip spaces off the end of the line automatically. @@ -404,11 +406,14 @@ C<chmod 777 %s>, C<+ar> does C<chmod a+r %s>, C<-ow> does C<chmod o-w %s> etc. =item yy -Copy (yank) the selection, like pressing Ctrl+C in modern GUI programs. +Copy (yank) the selection, like pressing Ctrl+C in modern GUI programs. (You +can also type "ya" to add files to the copy buffer, "yr" to remove files again, +or "yt" for toggling.) =item dd -Cut the selection, like pressing Ctrl+X in modern GUI programs. +Cut the selection, like pressing Ctrl+X in modern GUI programs. (There are +also "da", "dr" and "dt" shortcuts equivalent to "ya", "yr" and "yt".) =item pp @@ -601,6 +606,10 @@ fly with the command B<:set option value>. Examples: set column_ratios 1,2,3 set show_hidden true +Toggling options can be done with: + + set show_hidden! + The different types of settings and an example for each type: setting type | example values @@ -849,6 +858,10 @@ Sets the state for the version control backend. The possible values are: Enable this if key combinations with the Alt Key don't work for you. (Especially on xterm) +=item clear_filters_on_dir_change [bool] + +If set to 'true', persistent filters would be cleared upon leaving the directory + =back @@ -906,6 +919,7 @@ ranger. For your convenience, this is a list of the "public" commands including setintag tags option value setlocal [path=<path>] option value shell [-FLAGS] command + source filename terminal tmap key command touch filename @@ -1256,6 +1270,14 @@ use C<path=~/dl$>. Run a shell command. I<flags> are discussed in their own section. +=item source I<filename> + +Reads commands from a file and executes them in the ranger console. + +This can be used to re-evaluate the rc.conf file after changing it: + + map X chain shell vim -p %confdir/rc.conf %rangerdir/config/rc.conf; source %confdir/rc.conf + =item terminal Spawns the I<x-terminal-emulator> starting in the current directory. diff --git a/examples/rc_emacs.conf b/examples/rc_emacs.conf index f5e66b89..f0752e62 100644 --- a/examples/rc_emacs.conf +++ b/examples/rc_emacs.conf @@ -347,7 +347,7 @@ map <C-r> search_next forward=False map <C-x>b tab_move 1 map <A-Right> tab_move 1 map <A-Left> tab_move -1 -map <C-x>f tab_new ~ +map <C-x><C-f> tab_new ~ map <C-_>k tab_restore map <a-1> tab_open 1 map <a-2> tab_open 2 diff --git a/ranger/api/commands.py b/ranger/api/commands.py index a20adecb..c3e0a59a 100644 --- a/ranger/api/commands.py +++ b/ranger/api/commands.py @@ -152,6 +152,27 @@ class Command(FileManagerAware): return ''.join([self._tabinsert_left, word, self._tabinsert_right]) def parse_setting_line(self): + """ + Parses the command line argument that is passed to the `:set` command. + Returns [option, value, name_complete]. + + Can parse incomplete lines too, and `name_complete` is a boolean + indicating whether the option name looks like it's completed or + unfinished. This is useful for generating tab completions. + + >>> Command("set foo=bar").parse_setting_line() + ['foo', 'bar', True] + >>> Command("set foo").parse_setting_line() + ['foo', '', False] + >>> Command("set foo=").parse_setting_line() + ['foo', '', True] + >>> Command("set foo ").parse_setting_line() + ['foo', '', True] + >>> Command("set myoption myvalue").parse_setting_line() + ['myoption', 'myvalue', True] + >>> Command("set").parse_setting_line() + ['', '', False] + """ if self._setting_line is not None: return self._setting_line match = _SETTINGS_RE.match(self.rest(1)) @@ -163,6 +184,25 @@ class Command(FileManagerAware): self._setting_line = result return result + def parse_setting_line_v2(self): + """ + Parses the command line argument that is passed to the `:set` command. + Returns [option, value, name_complete, toggle]. + + >>> Command("set foo=bar").parse_setting_line_v2() + ['foo', 'bar', True, False] + >>> Command("set foo!").parse_setting_line_v2() + ['foo', '', True, True] + """ + option, value, name_complete = self.parse_setting_line() + if len(option) >= 2 and option[-1] == '!': + toggle = True + option = option[:-1] + name_complete = True + else: + toggle = False + return [option, value, name_complete, toggle] + def parse_flags(self): """Finds and returns flags in the command diff --git a/ranger/colorschemes/default.py b/ranger/colorschemes/default.py index e5dbf3a6..37b372a3 100644 --- a/ranger/colorschemes/default.py +++ b/ranger/colorschemes/default.py @@ -67,6 +67,9 @@ class Default(ColorScheme): else: fg = magenta + if context.inactive_pane: + fg = cyan + elif context.in_titlebar: attr |= bold if context.hostname: diff --git a/ranger/colorschemes/jungle.py b/ranger/colorschemes/jungle.py index a4fb1c1d..6a9c3c52 100644 --- a/ranger/colorschemes/jungle.py +++ b/ranger/colorschemes/jungle.py @@ -9,7 +9,8 @@ class Scheme(Default): def use(self, context): fg, bg, attr = Default.use(self, context) - if context.directory and not context.marked and not context.link: + if context.directory and not context.marked and not context.link \ + and not context.inactive_pane: fg = green if context.in_titlebar and context.hostname: diff --git a/ranger/colorschemes/solarized.py b/ranger/colorschemes/solarized.py index 07552ce2..2177ca3d 100644 --- a/ranger/colorschemes/solarized.py +++ b/ranger/colorschemes/solarized.py @@ -80,6 +80,9 @@ class Solarized(ColorScheme): else: fg = magenta + if context.inactive_pane: + fg = 241 + elif context.in_titlebar: attr |= bold if context.hostname: diff --git a/ranger/config/commands.py b/ranger/config/commands.py index c41bfcb6..4d00b4c8 100755 --- a/ranger/config/commands.py +++ b/ranger/config/commands.py @@ -336,12 +336,17 @@ class set_(Command): """:set <option name>=<python expression> Gives an option a new value. + + Use `:set <option>!` to toggle or cycle it, e.g. `:set flush_input!` """ name = 'set' # don't override the builtin set class def execute(self): name = self.arg(1) - name, value, _ = self.parse_setting_line() - self.fm.set_option_from_string(name, value) + name, value, _, toggle = self.parse_setting_line_v2() + if toggle: + self.fm.toggle_option(name) + else: + self.fm.set_option_from_string(name, value) def tab(self, tabnum): from ranger.gui.colorscheme import get_all_colorschemes diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf index 43a9b99a..914b1d0f 100644 --- a/ranger/config/rc.conf +++ b/ranger/config/rc.conf @@ -19,6 +19,13 @@ # == Options # =================================================================== +# Which viewmode should be used? Possible values are: +# miller: Use miller columns which show multiple levels of the hierarchy +# multipane: Midnight-commander like multipane view showing all tabs next +# to each other +set viewmode miller +#set viewmode multipane + # How many columns are there, and what are their relative widths? set column_ratios 1,3,4 @@ -235,6 +242,7 @@ map <C-r> reset map <C-l> redraw_window map <C-c> abort map <esc> change_mode normal +map ~ set viewmode! map i display_file map ? help @@ -255,7 +263,9 @@ map cd console cd%space # Change the line mode map Mf linemode filename map Mi linemode fileinfo +map Mm linemode mtime map Mp linemode permissions +map Ms linemode sizemtime map Mt linemode metatitle # Tagging / Marking @@ -360,11 +370,13 @@ map dd cut map ud uncut map da cut mode=add map dr cut mode=remove +map dt cut mode=toggle map yy copy map uy uncut map ya copy mode=add map yr copy mode=remove +map yt copy mode=toggle # Temporary workarounds map dgg eval fm.cut(dirarg=dict(to=0), narg=quantifier) @@ -410,7 +422,7 @@ map <a-8> tab_open 8 map <a-9> tab_open 9 # Sorting -map or toggle_option sort_reverse +map or set sort_reverse! map oz set sort=random map os chain set sort=size; set sort_reverse=False map ob chain set sort=basename; set sort_reverse=False @@ -433,18 +445,18 @@ map oE chain set sort=extension; set sort_reverse=True map dc get_cumulative_size # Settings -map zc toggle_option collapse_preview -map zd toggle_option sort_directories_first -map zh toggle_option show_hidden -map <C-h> toggle_option show_hidden -map zI toggle_option flushinput -map zi toggle_option preview_images -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 zc set collapse_preview! +map zd set sort_directories_first! +map zh set show_hidden! +map <C-h> set show_hidden! +map zI set flushinput! +map zi set preview_images! +map zm set mouse_enabled! +map zp set preview_files! +map zP set preview_directories! +map zs set sort_case_insensitive! +map zu set autoupdate_cumulative_size! +map zv set use_preview_script! map zf console filter%space # Bookmarks diff --git a/ranger/config/rifle.conf b/ranger/config/rifle.conf index 31db35a4..f95c94ff 100644 --- a/ranger/config/rifle.conf +++ b/ranger/config/rifle.conf @@ -168,6 +168,7 @@ ext djvu, has atril, X, flag f = atril -- "$@" mime ^image/svg, has inkscape, X, flag f = inkscape -- "$@" mime ^image/svg, has display, X, flag f = display -- "$@" +mime ^image, has pqiv, X, flag f = pqiv -- "$@" mime ^image, has sxiv, X, flag f = sxiv -- "$@" mime ^image, has feh, X, flag f = feh -- "$@" mime ^image, has mirage, X, flag f = mirage -- "$@" diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py index 1daf6d70..fff29c36 100644 --- a/ranger/container/fsobject.py +++ b/ranger/container/fsobject.py @@ -30,7 +30,8 @@ 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'(\d+)') +_extract_number_re = re.compile(r'(\d+|\D)') +_integers = set("0123456789") def safe_path(path): return path.translate(_safe_string_table) @@ -79,7 +80,8 @@ class FileSystemObject(FileManagerAware, SettingsAware): _linemode = DEFAULT_LINEMODE linemode_dict = dict( (linemode.name, linemode()) for linemode in - [DefaultLinemode, TitleLinemode, PermissionsLinemode, FileInfoLinemode] + [DefaultLinemode, TitleLinemode, PermissionsLinemode, FileInfoLinemode, + MtimeLinemode, SizeMtimeLinemode] ) def __init__(self, path, preload=None, path_is_abs=False, basename_is_rel_to=None): @@ -134,12 +136,12 @@ class FileSystemObject(FileManagerAware, SettingsAware): @lazy_property def basename_natural(self): - return [int(s) if s.isdigit() else s \ + return [('0', int(s)) if s in _integers else (s, 0) \ for s in _extract_number_re.split(self.relative_path)] @lazy_property def basename_natural_lower(self): - return [int(s) if s.isdigit() else s \ + return [('0', int(s)) if s in _integers else (s, 0) \ for s in _extract_number_re.split(self.relative_path_lower)] @lazy_property diff --git a/ranger/container/settings.py b/ranger/container/settings.py index d7258d6d..14ff9bca 100644 --- a/ranger/container/settings.py +++ b/ranger/container/settings.py @@ -56,6 +56,7 @@ ALLOWED_SETTINGS = { 'update_title': bool, 'update_tmux_title': bool, 'use_preview_script': bool, + 'viewmode': str, 'vcs_aware': bool, 'vcs_backend_bzr': str, 'vcs_backend_git': str, @@ -65,6 +66,16 @@ ALLOWED_SETTINGS = { 'clear_filters_on_dir_change': bool } +ALLOWED_VALUES = { + 'confirm_on_delete': ['always', 'multiple', 'never'], + 'preview_images_method': ['w3m', 'iterm2'], + 'vcs_backend_bzr': ['enabled', 'local', 'disabled'], + 'vcs_backend_git': ['enabled', 'local', 'disabled'], + 'vcs_backend_hg': ['enabled', 'local', 'disabled'], + 'vcs_backend_svn': ['enabled', 'local', 'disabled'], + 'viewmode': ['miller', 'multipane'], +} + DEFAULT_VALUES = { bool: False, type(None): None, diff --git a/ranger/core/actions.py b/ranger/core/actions.py index a44e30e1..71cb5929 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -23,9 +23,10 @@ from ranger.ext.next_available_filename import next_available_filename from ranger.ext.rifle import squash_flags, ASK_COMMAND from ranger.core.shared import FileManagerAware, SettingsAware from ranger.core.tab import Tab +from ranger.container.directory import Directory from ranger.container.file import File from ranger.core.loader import CommandLoader, CopyLoader -from ranger.container.settings import ALLOWED_SETTINGS +from ranger.container.settings import ALLOWED_SETTINGS, ALLOWED_VALUES from ranger.core.linemode import DEFAULT_LINEMODE MACRO_FAIL = "<\x01\x01MACRO_HAS_NO_VALUE\x01\01>" @@ -245,6 +246,7 @@ class Actions(FileManagerAware, SettingsAware): macros = {} macros['rangerdir'] = ranger.RANGERDIR + macros['confdir'] = self.fm.confpath() macros['space'] = ' ' if self.fm.thisfile: @@ -341,18 +343,19 @@ class Actions(FileManagerAware, SettingsAware): Load a config file. """ filename = os.path.expanduser(filename) - for line in open(filename, 'r'): - line = line.lstrip().rstrip("\r\n") - if line.startswith("#") or not line.strip(): - continue - try: - self.execute_console(line) - except Exception as e: - if ranger.arg.debug: - raise - else: - self.notify('Error in line `%s\':\n %s' % - (line, str(e)), bad=True) + with open(filename, 'r') as f: + for line in f: + line = line.lstrip().rstrip("\r\n") + if line.startswith("#") or not line.strip(): + continue + try: + self.execute_console(line) + except Exception as e: + if ranger.arg.debug: + raise + else: + self.notify('Error in line `%s\':\n %s' % + (line, str(e)), bad=True) def execute_file(self, files, **kw): """Uses the "rifle" module to open/execute a file @@ -564,7 +567,7 @@ class Actions(FileManagerAware, SettingsAware): def pager_close(self): if self.ui.pager.visible: self.ui.close_pager() - if self.ui.browser.pager.visible: + if hasattr(self.ui.browser, 'pager') and self.ui.browser.pager.visible: self.ui.close_embedded_pager() def taskview_open(self): @@ -593,6 +596,17 @@ class Actions(FileManagerAware, SettingsAware): """ if isinstance(self.settings[string], bool): self.settings[string] ^= True + elif string in ALLOWED_VALUES: + current = self.settings[string] + allowed = ALLOWED_VALUES[string] + if len(allowed) > 0: + if current not in allowed and current == "": + current = allowed[0] + if current in allowed: + self.settings[string] = \ + allowed[(allowed.index(current) + 1) % len(allowed)] + else: + self.settings[string] = allowed[0] def set_option(self, optname, value): """:set_option <optname> @@ -782,10 +796,14 @@ class Actions(FileManagerAware, SettingsAware): except KeyError: pass - def set_bookmark(self, key): + def set_bookmark(self, key, val=None): """Set the bookmark with the name <key> to the current directory""" + if val is None: + val = self.thisdir + else: + val = Directory(val) self.bookmarks.update_if_outdated() - self.bookmarks[str(key)] = self.thisdir + self.bookmarks[str(key)] = val def unset_bookmark(self, key): """Delete the bookmark with the name <key>""" @@ -1039,6 +1057,7 @@ class Actions(FileManagerAware, SettingsAware): if tab_has_changed: self.change_mode('normal') self.signal_emit('tab.change', old=previous_tab, new=self.thistab) + self.signal_emit('tab.layoutchange') def tab_close(self, name=None): if name is None: @@ -1053,6 +1072,7 @@ class Actions(FileManagerAware, SettingsAware): if name in self.tabs: del self.tabs[name] self.restorable_tabs.append(tab) + self.signal_emit('tab.layoutchange') def tab_restore(self): # NOTE: The name of the tab is not restored. @@ -1222,7 +1242,7 @@ class Actions(FileManagerAware, SettingsAware): Copy the selected items. Modes are: 'set', 'add', 'remove'. """ - assert mode in ('set', 'add', 'remove') + assert mode in ('set', 'add', 'remove', 'toggle') cwd = self.thisdir if not narg and not dirarg: selected = (f for f in self.thistab.get_selection() if f in cwd.files) @@ -1244,6 +1264,8 @@ class Actions(FileManagerAware, SettingsAware): self.copy_buffer.update(set(selected)) elif mode == 'remove': self.copy_buffer.difference_update(set(selected)) + elif mode == 'toggle': + self.copy_buffer.symmetric_difference_update(set(selected)) self.do_cut = False self.ui.browser.main_column.request_redraw() diff --git a/ranger/core/fm.py b/ranger/core/fm.py index 046eb788..f6792b95 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|imv) ', command): + re.match(r'^(feh|sxiv|imv|pqiv) ', command): images = [f.relative_path for f in self.thisdir.files if f.image] escaped_filenames = " ".join(shell_quote(f) \ @@ -161,6 +161,12 @@ class FM(Actions, SignalDispatcher): new_command = command.replace("imv ", "imv -n %d " % number, 1) + if command[0:5] == 'pqiv ': + number = images.index(self.thisfile.relative_path) + new_command = command.replace("pqiv ", + "pqiv --action \"goto_file_byindex(%d)\" " % \ + number, 1) + if new_command: command = "set -- %s; %s" % (escaped_filenames, new_command) diff --git a/ranger/core/linemode.py b/ranger/core/linemode.py index 76e8f7cc..b7aef23f 100644 --- a/ranger/core/linemode.py +++ b/ranger/core/linemode.py @@ -5,6 +5,8 @@ import sys from abc import * +from datetime import datetime +from ranger.ext.human_readable import human_readable DEFAULT_LINEMODE = "filename" @@ -102,3 +104,22 @@ class FileInfoLinemode(LinemodeBase): return fileinfo else: raise NotImplementedError + +class MtimeLinemode(LinemodeBase): + name = "mtime" + + def filetitle(self, file, metadata): + return file.relative_path + + def infostring(self, file, metadata): + return datetime.fromtimestamp(file.stat.st_mtime).strftime("%Y-%m-%d %H:%M") + +class SizeMtimeLinemode(LinemodeBase): + name = "sizemtime" + + def filetitle(self, file, metadata): + return file.relative_path + + def infostring(self, file, metadata): + return "%s %s" % (human_readable(file.size), + datetime.fromtimestamp(file.stat.st_mtime).strftime("%Y-%m-%d %H:%M")) diff --git a/ranger/core/loader.py b/ranger/core/loader.py index 2184d2b1..b1aabb53 100644 --- a/ranger/core/loader.py +++ b/ranger/core/loader.py @@ -260,7 +260,8 @@ def safeDecode(string): return string.decode("utf-8") except (UnicodeDecodeError): if HAVE_CHARDET: - return string.decode(chardet.detect(string)["encoding"]) + codec = chardet.detect(string)["encoding"] + return string.decode(codec, 'ignore') else: return "" diff --git a/ranger/data/scope.sh b/ranger/data/scope.sh index afaf131f..669d1e34 100755 --- a/ranger/data/scope.sh +++ b/ranger/data/scope.sh @@ -91,8 +91,15 @@ esac case "$mimetype" in # Syntax highlight for text files: text/* | */xml) - try safepipe highlight --out-format=ansi "$path" && { dump | trim; exit 5; } - try safepipe pygmentize "$path" && { dump | trim; exit 5; } + if [ "$(tput colors)" -ge 256 ]; then + pygmentize_format=terminal256 + highlight_format=xterm256 + else + pygmentize_format=terminal + highlight_format=ansi + fi + try safepipe highlight --out-format=${highlight_format} "$path" && { dump | trim; exit 5; } + try safepipe pygmentize -f ${pygmentize_format} "$path" && { dump | trim; exit 5; } exit 2;; # Ascii-previews of images: image/*) diff --git a/ranger/gui/context.py b/ranger/gui/context.py index ac597e5a..2d23d4f1 100644 --- a/ranger/gui/context.py +++ b/ranger/gui/context.py @@ -4,6 +4,7 @@ CONTEXT_KEYS = ['reset', 'error', 'badinfo', 'in_browser', 'in_statusbar', 'in_titlebar', 'in_console', 'in_pager', 'in_taskview', + 'active_pane', 'inactive_pane', 'directory', 'file', 'hostname', 'executable', 'media', 'link', 'fifo', 'socket', 'device', 'video', 'audio', 'image', 'media', 'document', 'container', diff --git a/ranger/gui/displayable.py b/ranger/gui/displayable.py index d3adfe50..c6e21d54 100644 --- a/ranger/gui/displayable.py +++ b/ranger/gui/displayable.py @@ -94,10 +94,9 @@ class Displayable(FileManagerAware, CursesShortcuts): """ def destroy(self): - """Called when the object is destroyed. - - Override this! - """ + """Called when the object is destroyed.""" + if hasattr(self, 'win'): + del self.win def contains_point(self, y, x): """Test whether the point lies inside this object. diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py index ad95d754..e5833839 100644 --- a/ranger/gui/ui.py +++ b/ranger/gui/ui.py @@ -11,6 +11,7 @@ from .displayable import DisplayableContainer from .mouse_event import MouseEvent from ranger.ext.keybinding_parser import KeyBuffer, KeyMaps, ALT_KEY from ranger.ext.lazy_property import lazy_property +from ranger.ext.signals import Signal MOUSEMASK = curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION @@ -34,6 +35,8 @@ def _setup_mouse(signal): curses.mousemask(0) class UI(DisplayableContainer): + ALLOWED_VIEWMODES = 'miller', 'multipane' + is_set_up = False load_mode = False is_on = False @@ -87,6 +90,7 @@ class UI(DisplayableContainer): self.win.addstr("loading...") self.win.refresh() self._draw_title = curses.tigetflag('hs') # has_status_line + self.update_size() self.is_on = True @@ -223,7 +227,7 @@ class UI(DisplayableContainer): def setup(self): """Build up the UI by initializing widgets.""" - from ranger.gui.widgets.browserview import BrowserView + from ranger.gui.widgets.view_miller import ViewMiller from ranger.gui.widgets.titlebar import TitleBar from ranger.gui.widgets.console import Console from ranger.gui.widgets.statusbar import StatusBar @@ -235,9 +239,10 @@ class UI(DisplayableContainer): self.add_child(self.titlebar) # Create the browser view - self.browser = BrowserView(self.win, self.settings.column_ratios) - self.settings.signal_bind('setopt.column_ratios', - self.browser.change_ratios) + self.settings.signal_bind('setopt.viewmode', self._set_viewmode) + self._viewmode = None + # The following line sets self.browser implicitly through the signal + self.viewmode = self.settings.viewmode self.add_child(self.browser) # Create the process manager @@ -339,10 +344,11 @@ class UI(DisplayableContainer): def draw_images(self): if self.pager.visible: self.pager.draw_image() - elif self.browser.pager.visible: - self.browser.pager.draw_image() - else: - self.browser.columns[-1].draw_image() + elif hasattr(self.browser, 'pager'): + if self.browser.pager.visible: + self.browser.pager.draw_image() + else: + self.browser.columns[-1].draw_image() def close_pager(self): if self.console.visible: @@ -411,7 +417,44 @@ class UI(DisplayableContainer): self.status.hint = text def get_pager(self): - if self.browser.pager.visible: + if hasattr(self.browser, 'pager') and self.browser.pager.visible: return self.browser.pager else: return self.pager + + def _get_viewmode(self): + return self._viewmode + + def _set_viewmode(self, value): + if isinstance(value, Signal): + value = value.value + if value == '': + value = self.ALLOWED_VIEWMODES[0] + if value in self.ALLOWED_VIEWMODES: + if self._viewmode != value: + self._viewmode = value + resize = False + if hasattr(self, 'browser'): + old_size = self.browser.y, self.browser.x, self.browser.hei, self.browser.wid + self.remove_child(self.browser) + self.browser.destroy() + resize = True + + self.browser = self._viewmode_to_class(value)(self.win) + self.redraw_window() + self.add_child(self.browser) + if resize: + self.browser.resize(*old_size) + else: + raise ValueError("Attempting to set invalid viewmode `%s`, should " + "be one of `%s`." % (value, "`, `".join(self.ALLOWED_VIEWMODES))) + + viewmode = property(_get_viewmode, _set_viewmode) + + def _viewmode_to_class(self, viewmode): + if viewmode == 'miller': + from ranger.gui.widgets.view_miller import ViewMiller + return ViewMiller + if viewmode == 'multipane': + from ranger.gui.widgets.view_multipane import ViewMultipane + return ViewMultipane diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index 07830b31..129d8486 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -27,7 +27,7 @@ class BrowserColumn(Pager): old_dir = None old_thisfile = None - def __init__(self, win, level): + def __init__(self, win, level, tab=None): """Initializes a Browser Column Widget win = the curses window object of the BrowserView @@ -40,6 +40,7 @@ class BrowserColumn(Pager): Pager.__init__(self, win) Widget.__init__(self, win) self.level = level + self.tab = tab self.original_level = level self.settings.signal_bind('setopt.display_size_in_main_column', @@ -48,9 +49,6 @@ class BrowserColumn(Pager): def request_redraw(self): self.need_redraw = True - def resize(self, y, x, hei, wid): - Widget.resize(self, y, x, hei, wid) - def click(self, event): """Handle a MouseEvent""" direction = event.mouse_wheel_direction() @@ -132,7 +130,11 @@ class BrowserColumn(Pager): def poke(self): Widget.poke(self) - self.target = self.fm.thistab.at_level(self.level) + if self.tab is None: + tab = self.fm.thistab + else: + tab = self.tab + self.target = tab.at_level(self.level) def draw(self): """Call either _draw_file() or _draw_directory()""" @@ -201,6 +203,15 @@ class BrowserColumn(Pager): base_color = ['in_browser'] + if self.fm.ui.viewmode == 'multipane' and self.tab is not None: + active_pane = self.tab == self.fm.thistab + if active_pane: + base_color.append('active_pane') + else: + base_color.append('inactive_pane') + else: + active_pane = False + self.win.move(0, 0) if not self.target.content_loaded: @@ -228,7 +239,7 @@ class BrowserColumn(Pager): copied = [f.path for f in self.fm.copy_buffer] - selected_i = self.target.pointer + selected_i = self._get_index_of_selected_file() for line in range(self.hei): i = line + self.scroll_begin if line > self.hei: @@ -258,7 +269,7 @@ class BrowserColumn(Pager): key = (self.wid, selected_i == i, drawn.marked, self.main_column, drawn.path in copied, tagged_marker, drawn.infostring, drawn.vcsstatus, drawn.vcsremotestatus, self.target.has_vcschild, - self.fm.do_cut, current_linemode.name, metakey) + self.fm.do_cut, current_linemode.name, metakey, active_pane) if key in drawn.display_data: self.execute_curses_batch(line, drawn.display_data[key]) @@ -336,6 +347,12 @@ class BrowserColumn(Pager): self.execute_curses_batch(line, display_data) self.color_reset() + def _get_index_of_selected_file(self): + if self.fm.ui.viewmode == 'multipane' and hasattr(self, 'tab'): + return self.tab.pointer + else: + return self.target.pointer + def _total_len(self, predisplay): return sum([len(WideString(s)) for s, L in predisplay]) @@ -391,7 +408,7 @@ class BrowserColumn(Pager): def _draw_directory_color(self, i, drawn, copied): this_color = [] - if i == self.target.pointer: + if i == self._get_index_of_selected_file(): this_color.append('selected') if drawn.marked: @@ -431,7 +448,7 @@ class BrowserColumn(Pager): dirsize = len(self.target) winsize = self.hei halfwinsize = winsize // 2 - index = self.target.pointer or 0 + index = self._get_index_of_selected_file() or 0 original = self.target.scroll_begin projected = index - original diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py index 159b8f1e..156298ab 100644 --- a/ranger/gui/widgets/console.py +++ b/ranger/gui/widgets/console.py @@ -76,6 +76,7 @@ class Console(Widget): except UnicodeEncodeError: pass f.close() + Widget.destroy(self) def draw(self): self.win.erase() diff --git a/ranger/gui/widgets/pager.py b/ranger/gui/widgets/pager.py index ce3cc1bf..0d185f98 100644 --- a/ranger/gui/widgets/pager.py +++ b/ranger/gui/widgets/pager.py @@ -52,6 +52,7 @@ class Pager(Widget): def destroy(self): self.clear_image(force=True) + Widget.destroy(self) def finalize(self): self.fm.ui.win.move(self.y, self.x) diff --git a/ranger/gui/widgets/view_base.py b/ranger/gui/widgets/view_base.py new file mode 100644 index 00000000..9fbd9586 --- /dev/null +++ b/ranger/gui/widgets/view_base.py @@ -0,0 +1,159 @@ +# This file is part of ranger, the console file manager. +# License: GNU GPL version 3, see the file "AUTHORS" for details. + +"""The base GUI element for views on the directory""" + +import curses, _curses +from ranger.ext.keybinding_parser import key_to_string +from . import Widget +from ..displayable import DisplayableContainer + +class ViewBase(Widget, DisplayableContainer): + draw_bookmarks = False + need_clear = False + draw_hints = False + draw_info = False + + def __init__(self, win): + DisplayableContainer.__init__(self, win) + + self.fm.signal_bind('move', self.request_clear) + self.old_draw_borders = self.settings.draw_borders + + def request_clear(self): + self.need_clear = True + + def draw(self): + if self.need_clear: + self.win.erase() + self.need_redraw = True + self.need_clear = False + for tab in self.fm.tabs.values(): + directory = tab.thisdir + if directory: + directory.load_content_if_outdated() + directory.use() + DisplayableContainer.draw(self) + if self.draw_bookmarks: + self._draw_bookmarks() + elif self.draw_hints: + self._draw_hints() + elif self.draw_info: + self._draw_info(self.draw_info) + + def finalize(self): + if hasattr(self, 'pager') and self.pager.visible: + try: + self.fm.ui.win.move(self.main_column.y, self.main_column.x) + except: + pass + else: + try: + x = self.main_column.x + y = self.main_column.y + self.main_column.target.pointer\ + - self.main_column.scroll_begin + self.fm.ui.win.move(y, x) + except: + pass + + def _draw_bookmarks(self): + self.columns[-1].clear_image(force=True) + self.fm.bookmarks.update_if_outdated() + self.color_reset() + self.need_clear = True + + sorted_bookmarks = sorted((item for item in self.fm.bookmarks \ + if self.fm.settings.show_hidden_bookmarks or \ + '/.' not in item[1].path), key=lambda t: t[0].lower()) + + hei = min(self.hei - 1, len(sorted_bookmarks)) + ystart = self.hei - hei + + maxlen = self.wid + self.addnstr(ystart - 1, 0, "mark path".ljust(self.wid), self.wid) + + whitespace = " " * maxlen + for line, items in zip(range(self.hei-1), sorted_bookmarks): + key, mark = items + string = " " + key + " " + mark.path + self.addstr(ystart + line, 0, whitespace) + self.addnstr(ystart + line, 0, string, self.wid) + + self.win.chgat(ystart - 1, 0, curses.A_UNDERLINE) + + def _draw_info(self, lines): + self.columns[-1].clear_image(force=True) + self.need_clear = True + hei = min(self.hei - 1, len(lines)) + ystart = self.hei - hei + i = ystart + whitespace = " " * self.wid + for line in lines: + if i >= self.hei: + break + self.addstr(i, 0, whitespace) + self.addnstr(i, 0, line, self.wid) + i += 1 + + def _draw_hints(self): + self.columns[-1].clear_image(force=True) + self.need_clear = True + hints = [] + for k, v in self.fm.ui.keybuffer.pointer.items(): + k = key_to_string(k) + if isinstance(v, dict): + text = '...' + else: + text = v + if text.startswith('hint') or text.startswith('chain hint'): + continue + hints.append((k, text)) + hints.sort(key=lambda t: t[1]) + + hei = min(self.hei - 1, len(hints)) + ystart = self.hei - hei + self.addnstr(ystart - 1, 0, "key command".ljust(self.wid), + self.wid) + try: + self.win.chgat(ystart - 1, 0, curses.A_UNDERLINE) + except: + pass + whitespace = " " * self.wid + i = ystart + for key, cmd in hints: + string = " " + key.ljust(11) + " " + cmd + self.addstr(i, 0, whitespace) + self.addnstr(i, 0, string, self.wid) + i += 1 + + def _collapse(self): + # Should the last column be cut off? (Because there is no preview) + if not self.settings.collapse_preview or not self.preview \ + or not self.stretch_ratios: + return False + result = not self.columns[-1].has_preview() + target = self.columns[-1].target + if not result and target and target.is_file: + if self.fm.settings.preview_script and \ + self.fm.settings.use_preview_script: + try: + result = not self.fm.previews[target.realpath]['foundpreview'] + except: + return self.old_collapse + + self.old_collapse = result + return result + + def click(self, event): + if DisplayableContainer.click(self, event): + return True + direction = event.mouse_wheel_direction() + if direction: + self.main_column.scroll(direction) + return False + + def resize(self, y, x, hei, wid): + DisplayableContainer.resize(self, y, x, hei, wid) + + def poke(self): + DisplayableContainer.poke(self) diff --git a/ranger/gui/widgets/browserview.py b/ranger/gui/widgets/view_miller.py index e9640208..dbf59ff8 100644 --- a/ranger/gui/widgets/browserview.py +++ b/ranger/gui/widgets/view_miller.py @@ -1,50 +1,49 @@ # This file is part of ranger, the console file manager. # License: GNU GPL version 3, see the file "AUTHORS" for details. -"""The BrowserView manages a set of BrowserColumns.""" +"""ViewMiller arranges the view in miller columns""" import curses, _curses from ranger.ext.signals import Signal -from ranger.ext.keybinding_parser import key_to_string -from . import Widget from .browsercolumn import BrowserColumn from .pager import Pager from ..displayable import DisplayableContainer +from ranger.gui.widgets.view_base import ViewBase -class BrowserView(Widget, DisplayableContainer): +class ViewMiller(ViewBase): ratios = None preview = True is_collapsed = False - draw_bookmarks = False stretch_ratios = None - need_clear = False old_collapse = False - draw_hints = False - draw_info = False - def __init__(self, win, ratios, preview = True): - DisplayableContainer.__init__(self, win) - self.preview = preview + def __init__(self, win): + ViewBase.__init__(self, win) + self.preview = True self.columns = [] self.pager = Pager(self.win, embedded=True) self.pager.visible = False self.add_child(self.pager) - self.change_ratios(ratios) + self.rebuild() for option in ('preview_directories', 'preview_files'): self.settings.signal_bind('setopt.' + option, self._request_clear_if_has_borders, weak=True) - self.fm.signal_bind('move', self.request_clear) self.settings.signal_bind('setopt.column_ratios', self.request_clear) + self.settings.signal_bind('setopt.column_ratios', self.rebuild) self.old_draw_borders = self.settings.draw_borders - def change_ratios(self, ratios): - if isinstance(ratios, Signal): - ratios = ratios.value + def rebuild(self): + for child in self.container: + if isinstance(child, BrowserColumn): + self.remove_child(child) + child.destroy() + + ratios = self.settings.column_ratios for column in self.columns: column.destroy() @@ -82,9 +81,6 @@ class BrowserView(Widget, DisplayableContainer): if self.settings.draw_borders: self.request_clear() - def request_clear(self): - self.need_clear = True - def draw(self): if self.need_clear: self.win.erase() @@ -105,21 +101,6 @@ class BrowserView(Widget, DisplayableContainer): elif self.draw_info: self._draw_info(self.draw_info) - def finalize(self): - if self.pager.visible: - try: - self.fm.ui.win.move(self.main_column.y, self.main_column.x) - except: - pass - else: - try: - x = self.main_column.x - y = self.main_column.y + self.main_column.target.pointer\ - - self.main_column.scroll_begin - self.fm.ui.win.move(y, x) - except: - pass - def _draw_borders(self): win = self.win self.color('in_browser', 'border') @@ -180,76 +161,6 @@ class BrowserView(Widget, DisplayableContainer): self.addch(0, right_end, curses.ACS_URCORNER) self.addch(self.hei - 1, right_end, curses.ACS_LRCORNER) - def _draw_bookmarks(self): - self.columns[-1].clear_image(force=True) - self.fm.bookmarks.update_if_outdated() - self.color_reset() - self.need_clear = True - - sorted_bookmarks = sorted((item for item in self.fm.bookmarks \ - if self.fm.settings.show_hidden_bookmarks or \ - '/.' not in item[1].path), key=lambda t: t[0].lower()) - - hei = min(self.hei - 1, len(sorted_bookmarks)) - ystart = self.hei - hei - - maxlen = self.wid - self.addnstr(ystart - 1, 0, "mark path".ljust(self.wid), self.wid) - - whitespace = " " * maxlen - for line, items in zip(range(self.hei-1), sorted_bookmarks): - key, mark = items - string = " " + key + " " + mark.path - self.addstr(ystart + line, 0, whitespace) - self.addnstr(ystart + line, 0, string, self.wid) - - self.win.chgat(ystart - 1, 0, curses.A_UNDERLINE) - - def _draw_info(self, lines): - self.columns[-1].clear_image(force=True) - self.need_clear = True - hei = min(self.hei - 1, len(lines)) - ystart = self.hei - hei - i = ystart - whitespace = " " * self.wid - for line in lines: - if i >= self.hei: - break - self.addstr(i, 0, whitespace) - self.addnstr(i, 0, line, self.wid) - i += 1 - - def _draw_hints(self): - self.columns[-1].clear_image(force=True) - self.need_clear = True - hints = [] - for k, v in self.fm.ui.keybuffer.pointer.items(): - k = key_to_string(k) - if isinstance(v, dict): - text = '...' - else: - text = v - if text.startswith('hint') or text.startswith('chain hint'): - continue - hints.append((k, text)) - hints.sort(key=lambda t: t[1]) - - hei = min(self.hei - 1, len(hints)) - ystart = self.hei - hei - self.addnstr(ystart - 1, 0, "key command".ljust(self.wid), - self.wid) - try: - self.win.chgat(ystart - 1, 0, curses.A_UNDERLINE) - except: - pass - whitespace = " " * self.wid - i = ystart - for key, cmd in hints: - string = " " + key.ljust(11) + " " + cmd - self.addstr(i, 0, whitespace) - self.addnstr(i, 0, string, self.wid) - i += 1 - def _collapse(self): # Should the last column be cut off? (Because there is no preview) if not self.settings.collapse_preview or not self.preview \ @@ -270,7 +181,8 @@ class BrowserView(Widget, DisplayableContainer): def resize(self, y, x, hei, wid): """Resize all the columns according to the given ratio""" - DisplayableContainer.resize(self, y, x, hei, wid) + ViewBase.resize(self, y, x, hei, wid) + borders = self.settings.draw_borders pad = 1 if borders else 0 left = pad @@ -291,7 +203,7 @@ class BrowserView(Widget, DisplayableContainer): if not cut_off: wid = int(self.wid - left + 1 - pad) else: - self.columns[i].resize(pad, left - 1, hei - pad * 2, 1) + self.columns[i].resize(pad, max(0, left - 1), hei - pad * 2, 1) self.columns[i].visible = False continue @@ -312,14 +224,6 @@ class BrowserView(Widget, DisplayableContainer): left += wid - def click(self, event): - if DisplayableContainer.click(self, event): - return True - direction = event.mouse_wheel_direction() - if direction: - self.main_column.scroll(direction) - return False - def open_pager(self): self.pager.visible = True self.pager.focused = True @@ -343,7 +247,7 @@ class BrowserView(Widget, DisplayableContainer): pass def poke(self): - DisplayableContainer.poke(self) + ViewBase.poke(self) # Show the preview column when it has a preview but has # been hidden (e.g. because of padding_right = False) diff --git a/ranger/gui/widgets/view_multipane.py b/ranger/gui/widgets/view_multipane.py new file mode 100644 index 00000000..1899687e --- /dev/null +++ b/ranger/gui/widgets/view_multipane.py @@ -0,0 +1,51 @@ +# This file is part of ranger, the console file manager. +# License: GNU GPL version 3, see the file "AUTHORS" for details. + +from ranger.gui.widgets.view_base import ViewBase +from ranger.gui.widgets.browsercolumn import BrowserColumn + +class ViewMultipane(ViewBase): + def __init__(self, win): + ViewBase.__init__(self, win) + + self.fm.signal_bind('tab.layoutchange', self._layoutchange_handler) + self.fm.signal_bind('tab.change', self._tabchange_handler) + self.rebuild() + + def _layoutchange_handler(self): + if self.fm.ui.browser == self: + self.rebuild() + + def _tabchange_handler(self, signal): + if self.fm.ui.browser == self: + if signal.old: + signal.old.need_redraw = True + if signal.new: + signal.new.need_redraw = True + + def rebuild(self): + self.columns = [] + + for child in self.container: + self.remove_child(child) + child.destroy() + for name, tab in self.fm.tabs.items(): + column = BrowserColumn(self.win, 0, tab=tab) + column.main_column = True + column.display_infostring = True + if name == self.fm.current_tab: + self.main_column = column + self.columns.append(column) + self.add_child(column) + self.resize(self.y, self.x, self.hei, self.wid) + + def resize(self, y, x, hei, wid): + ViewBase.resize(self, y, x, hei, wid) + column_width = int(float(wid) / len(self.columns)) + left = 0 + top = 0 + for i, column in enumerate(self.columns): + column.resize(top, left, hei, max(1, column_width - 1)) + left += column_width + column.need_redraw = True + self.need_redraw = True diff --git a/tests/ranger/container/test_fsobject.py b/tests/ranger/container/test_fsobject.py new file mode 100644 index 00000000..3ea52d6f --- /dev/null +++ b/tests/ranger/container/test_fsobject.py @@ -0,0 +1,34 @@ +import pytest +import operator + +from ranger.container.fsobject import FileSystemObject + + +class MockFM(object): + """Used to fullfill the dependency by FileSystemObject.""" + + default_linemodes = [] + + +def create_filesystem_object(path): + """Create a FileSystemObject without an fm object.""" + fso = FileSystemObject.__new__(FileSystemObject) + fso.fm = MockFM() + fso.__init__(path) + return fso + + +def test_basename_natural1(): + """Test filenames without extensions.""" + fsos = [create_filesystem_object(path) + for path in ("hello", "hello1", "hello2")] + assert(fsos == sorted(fsos[::-1], key=operator.attrgetter("basename_natural"))) + assert(fsos == sorted(fsos[::-1], key=operator.attrgetter("basename_natural_lower"))) + + +def test_basename_natural2(): + """Test filenames with extensions.""" + fsos = [create_filesystem_object(path) + for path in ("hello", "hello.txt", "hello1.txt", "hello2.txt")] + assert(fsos == sorted(fsos[::-1], key=operator.attrgetter("basename_natural"))) + assert(fsos == sorted(fsos[::-1], key=operator.attrgetter("basename_natural_lower"))) |