diff options
48 files changed, 596 insertions, 186 deletions
diff --git a/.pylintrc b/.pylintrc index d108c93f..75bb7baf 100644 --- a/.pylintrc +++ b/.pylintrc @@ -8,7 +8,7 @@ max-branches=16 [FORMAT] max-line-length = 99 -disable=locally-disabled,locally-enabled,missing-docstring,duplicate-code,fixme,cyclic-import,redefined-variable-type +disable=locally-disabled,locally-enabled,missing-docstring,duplicate-code,fixme,cyclic-import,redefined-variable-type,stop-iteration-return [TYPECHECK] ignored-classes=ranger.core.actions.Actions diff --git a/Makefile b/Makefile index a2fe08cc..99d3f53e 100644 --- a/Makefile +++ b/Makefile @@ -95,7 +95,7 @@ test_doctest: done test_pytest: - echo "Running py.test tests..." + @echo "Running py.test tests..." py.test tests test: test_pylint test_flake8 test_doctest test_pytest diff --git a/README.md b/README.md index edbc2c4d..071b51d6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -ranger 1.9.0b5 +ranger 1.9.0b6 ============== [![Build Status](https://travis-ci.org/ranger/ranger.svg?branch=master)](https://travis-ci.org/ranger/ranger) @@ -108,6 +108,6 @@ current file. The second is the main column and the first shows the parent directory. Ranger can automatically copy default configuration files to `~/.config/ranger` -if you run it with the switch `--copy-config`. See `ranger --help` for a -description of that switch. Also check `ranger/config/` for the default -configuration. +if you run it with the switch `--copy-config=( rc | scope | ... | all )`. +See `ranger --help` for a description of that switch. Also check +`ranger/config/` for the default configuration. diff --git a/doc/ranger.1 b/doc/ranger.1 index 940ffc0a..50b83bdf 100644 --- a/doc/ranger.1 +++ b/doc/ranger.1 @@ -129,7 +129,7 @@ .\" ======================================================================== .\" .IX Title "RANGER 1" -.TH RANGER 1 "ranger-1.9.0b5" "2017-03-23" "ranger manual" +.TH RANGER 1 "ranger-1.9.0b6" "12/07/2017" "ranger manual" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l @@ -143,6 +143,7 @@ ranger \- visual file manager [\fB\-\-copy\-config\fR=\fIwhich\fR] [\fB\-\-choosefile\fR=\fItarget\fR] [\fB\-\-choosefiles\fR=\fItarget\fR] [\fB\-\-choosedir\fR=\fItarget\fR] [\fB\-\-selectfile\fR=\fIfilepath\fR] +[\fB\-\-show\-only\-dirs\fR] [\fB\-\-list\-unused\-keys\fR] [\fB\-\-list\-tagged\-files\fR=\fItag\fR] [\fB\-\-profile\fR] [\fB\-\-cmd\fR=\fIcommand\fR] [\fIpath\fR] .SH "DESCRIPTION" @@ -225,6 +226,10 @@ write the last visited directory into \fItargetfile\fR. .IP "\fB\-\-selectfile\fR=\fItargetfile\fR" 14 .IX Item "--selectfile=targetfile" Open ranger with \fItargetfile\fR selected. +.IP "\fB\-\-show\-only\-dirs\fR" 14 +.IX Item "--show-only-dirs" +Display only the directories. May be used in conjunction with +\&\fB\-\-choosedir\fR=\fItargetfile\fR. .IP "\fB\-\-list\-unused\-keys\fR" 14 .IX Item "--list-unused-keys" List common keys which are not bound to any action in the \*(L"browser\*(R" context. @@ -416,8 +421,8 @@ By default, all the flags are off unless specified otherwise in the An uppercase flag negates the effect: \*(L"ffcccFsf\*(R" is equivalent to \*(L"cs\*(R". .PP The terminal program name for the \*(L"t\*(R" flag is taken from the environment -variable \f(CW$TERMCMD\fR. If it doesn't exist, it tries to extract it from \f(CW$TERM\fR and -uses \*(L"xterm\*(R" as a fallback if that fails. +variable \f(CW$TERMCMD\fR. If it doesn't exist, it tries to extract it from \f(CW$TERM\fR, +uses \*(L"x\-terminal-emulator\*(R" as a fallback, and then \*(L"xterm\*(R" if that fails. .PP Examples: \f(CW\*(C`:open_with c\*(C'\fR will open the file that you currently point at, even if you have selected other files. \f(CW\*(C`:shell \-w df\*(C'\fR will run \*(L"df\*(R" and wait for @@ -484,6 +489,13 @@ Move up and down in the parent directory. .IP "^R" 14 .IX Item "^R" Reload everything +.IP "F" 14 +.IX Item "F" +Toggle \fIfreeze_files\fR setting. When active (indicated by a cyan \fI\s-1FROZEN\s0\fR +message in the status bar), directories and files will not be loaded, improving +performance when all the files you need are already loaded. This does not +affect file previews, which can be toggled with \fIzI\fR. Also try disabling the +preview of directories with \fIzP\fR. .IP "^L" 14 .IX Item "^L" Redraw the screen @@ -762,6 +774,11 @@ currently running tasks which support progress bars? Flush the input after each key hit? One advantage is that when scrolling down with \*(L"j\*(R", ranger stops scrolling instantly when you release the key. One disadvantage is that when you type commands blindly, some keys might get lost. +.IP "freeze_files [bool] <F>" 4 +.IX Item "freeze_files [bool] <F>" +When active, directories and files will not be loaded, improving performance +when all the files you need are already loaded. This does not affect file +previews. .IP "hidden_filter [string]" 4 .IX Item "hidden_filter [string]" A regular expression pattern for files which should be hidden. For example, @@ -799,6 +816,14 @@ all directories above the current one as well? .IP "mouse_enabled [bool] <zm>" 4 .IX Item "mouse_enabled [bool] <zm>" Enable mouse input? +.IP "one_indexed [bool]" 4 +.IX Item "one_indexed [bool]" +Start line numbers from 1. Possible values are: +.Sp +.Vb 2 +\& false start line numbers from 0 +\& true start line numbers from 1 +.Ve .IP "padding_right [bool]" 4 .IX Item "padding_right [bool]" When collapse_preview is on and there is no preview, should there remain a @@ -822,6 +847,10 @@ to disable this feature. Which script should handle generating previews? If the file doesn't exist, or use_preview_script is off, ranger will handle previews itself by just printing the content. +.IP "save_backtick_bookmark [bool]" 4 +.IX Item "save_backtick_bookmark [bool]" +Save the \f(CW\*(C`\`\*(C'\fR bookmark to disk. This bookmark is used to switch to the last +directory by typing \f(CW\*(C`\`\`\*(C'\fR. .IP "save_console_history [bool]" 4 .IX Item "save_console_history [bool]" Should the console history be saved on exit? If disabled, the console history diff --git a/doc/ranger.pod b/doc/ranger.pod index ebeeafbe..310ab1fd 100644 --- a/doc/ranger.pod +++ b/doc/ranger.pod @@ -12,6 +12,7 @@ B<ranger> [B<--version>] [B<--help>] [B<--debug>] [B<--clean>] [B<--copy-config>=I<which>] [B<--choosefile>=I<target>] [B<--choosefiles>=I<target>] [B<--choosedir>=I<target>] [B<--selectfile>=I<filepath>] +[B<--show-only-dirs>] [B<--list-unused-keys>] [B<--list-tagged-files>=I<tag>] [B<--profile>] [B<--cmd>=I<command>] [I<path>] @@ -118,6 +119,11 @@ write the last visited directory into I<targetfile>. Open ranger with I<targetfile> selected. +=item B<--show-only-dirs> + +Display only the directories. May be used in conjunction with +B<--choosedir>=I<targetfile>. + =item B<--list-unused-keys> List common keys which are not bound to any action in the "browser" context. @@ -315,8 +321,8 @@ F<rifle.conf> configuration file. You can specify as many flags as you want. An uppercase flag negates the effect: "ffcccFsf" is equivalent to "cs". The terminal program name for the "t" flag is taken from the environment -variable $TERMCMD. If it doesn't exist, it tries to extract it from $TERM and -uses "xterm" as a fallback if that fails. +variable $TERMCMD. If it doesn't exist, it tries to extract it from $TERM, +uses "x-terminal-emulator" as a fallback, and then "xterm" if that fails. Examples: C<:open_with c> will open the file that you currently point at, even if you have selected other files. C<:shell -w df> will run "df" and wait for @@ -395,6 +401,14 @@ Move up and down in the parent directory. Reload everything +=item F + +Toggle I<freeze_files> setting. When active (indicated by a cyan I<FROZEN> +message in the status bar), directories and files will not be loaded, improving +performance when all the files you need are already loaded. This does not +affect file previews, which can be toggled with I<zI>. Also try disabling the +preview of directories with I<zP>. + =item ^L Redraw the screen @@ -752,6 +766,12 @@ Flush the input after each key hit? One advantage is that when scrolling down with "j", ranger stops scrolling instantly when you release the key. One disadvantage is that when you type commands blindly, some keys might get lost. +=item freeze_files [bool] <F> + +When active, directories and files will not be loaded, improving performance +when all the files you need are already loaded. This does not affect file +previews. + =item hidden_filter [string] A regular expression pattern for files which should be hidden. For example, @@ -792,6 +812,13 @@ all directories above the current one as well? Enable mouse input? +=item one_indexed [bool] + +Start line numbers from 1. Possible values are: + + false start line numbers from 0 + true start line numbers from 1 + =item padding_right [bool] When collapse_preview is on and there is no preview, should there remain a @@ -821,6 +848,11 @@ Which script should handle generating previews? If the file doesn't exist, or use_preview_script is off, ranger will handle previews itself by just printing the content. +=item save_backtick_bookmark [bool] + +Save the C<`> bookmark to disk. This bookmark is used to switch to the last +directory by typing C<``>. + =item save_console_history [bool] Should the console history be saved on exit? If disabled, the console history diff --git a/doc/rifle.1 b/doc/rifle.1 index 95c9ce50..8531d69c 100644 --- a/doc/rifle.1 +++ b/doc/rifle.1 @@ -129,7 +129,7 @@ .\" ======================================================================== .\" .IX Title "RIFLE 1" -.TH RIFLE 1 "rifle-1.9.0b5" "2017-03-23" "rifle manual" +.TH RIFLE 1 "rifle-1.9.0b6" "12/07/2017" "rifle manual" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l diff --git a/doc/tools/convert_papermode_to_metadata.py b/doc/tools/convert_papermode_to_metadata.py index e4010a73..57459097 100755 --- a/doc/tools/convert_papermode_to_metadata.py +++ b/doc/tools/convert_papermode_to_metadata.py @@ -1,4 +1,4 @@ -#!/bin/python +#!/usr/bin/env python """ usage: ./convert_papermode_to_metadata.py diff --git a/doc/tools/performance_test.py b/doc/tools/performance_test.py new file mode 100755 index 00000000..3b9099d5 --- /dev/null +++ b/doc/tools/performance_test.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python + +from __future__ import (absolute_import, division, print_function) + +import sys +import time + +sys.path.insert(0, '../..') +sys.path.insert(0, '.') + + +def main(): + import ranger.container.directory + import ranger.core.shared + import ranger.container.settings + import ranger.core.fm + from ranger.ext.openstruct import OpenStruct + ranger.args = OpenStruct() + ranger.args.clean = True + ranger.args.debug = False + + settings = ranger.container.settings.Settings() + ranger.core.shared.SettingsAware.settings_set(settings) + fm = ranger.core.fm.FM() + ranger.core.shared.FileManagerAware.fm_set(fm) + + time1 = time.time() + fm.initialize() + try: + usr = ranger.container.directory.Directory('/usr') + usr.load_content(schedule=False) + for fileobj in usr.files: + if fileobj.is_directory: + fileobj.load_content(schedule=False) + finally: + fm.destroy() + time2 = time.time() + print("%dms" % ((time2 - time1) * 1000)) + + +if __name__ == '__main__': + main() diff --git a/examples/rc_emacs.conf b/examples/rc_emacs.conf index d3707a12..26074a42 100644 --- a/examples/rc_emacs.conf +++ b/examples/rc_emacs.conf @@ -406,6 +406,7 @@ map <C-x>zs toggle_option sort_case_insensitive map <C-x>zu toggle_option autoupdate_cumulative_size map <C-x>zv toggle_option use_preview_script map <C-x>zf console filter%space +map <C-x>nn narrow # Bookmarks map <C-x>rb<any> enter_bookmark %any diff --git a/examples/rifle_sxiv.sh b/examples/rifle_sxiv.sh index 8cb13907..0bdd892d 100755 --- a/examples/rifle_sxiv.sh +++ b/examples/rifle_sxiv.sh @@ -24,7 +24,7 @@ if [ $# -eq 0 ]; then exit fi -[ "$1" == '--' ] && shift +[ "$1" = '--' ] && shift abspath () { case "$1" in diff --git a/ranger/__init__.py b/ranger/__init__.py index d98a2bf5..f3495197 100644 --- a/ranger/__init__.py +++ b/ranger/__init__.py @@ -14,7 +14,7 @@ import os # Information __license__ = 'GPL3' -__version__ = '1.9.0b5' +__version__ = '1.9.0b6' __author__ = __maintainer__ = 'Roman Zimbelmann' __email__ = 'hut@hut.pm' diff --git a/ranger/api/commands.py b/ranger/api/commands.py index 24a82d0b..9c687927 100644 --- a/ranger/api/commands.py +++ b/ranger/api/commands.py @@ -54,8 +54,8 @@ class CommandContainer(FileManagerAware): cmd_cls = self.get_command(cmd_name) if cmd_cls is None: self.fm.notify('alias failed: No such command: {0}'.format(cmd_name), bad=True) - return None - self.commands[name] = _command_init(command_alias_factory(name, cmd_cls, full_command)) + else: + self.commands[name] = _command_init(command_alias_factory(name, cmd_cls, full_command)) def load_commands_from_module(self, module): for var in vars(module).values(): @@ -293,7 +293,7 @@ class Command(FileManagerAware): # no results, return None if not dirnames: - return + return None # one result. since it must be a directory, append a slash. if len(dirnames) == 1: @@ -357,7 +357,7 @@ class Command(FileManagerAware): else: # no results, return None if not names: - return + return None # one result. append a slash if it's a directory if len(names) == 1: @@ -374,7 +374,7 @@ class Command(FileManagerAware): programs = [program for program in get_executables() if program.startswith(self.rest(1))] if not programs: - return + return None if len(programs) == 1: return self.start(1) + programs[0] programs.sort() @@ -397,7 +397,7 @@ def command_function_factory(func): def execute(self): # pylint: disable=too-many-branches if not func: - return + return None if len(self.args) == 1: try: return func(**{'narg': self.quantifier}) diff --git a/ranger/colorschemes/default.py b/ranger/colorschemes/default.py index 7d2b124f..c88cdc7c 100644 --- a/ranger/colorschemes/default.py +++ b/ranger/colorschemes/default.py @@ -98,6 +98,9 @@ class Default(ColorScheme): if context.marked: attr |= bold | reverse fg = yellow + if context.frozen: + attr |= bold | reverse + fg = cyan if context.message: if context.bad: attr |= bold diff --git a/ranger/config/commands.py b/ranger/config/commands.py index 4cedc4e1..299f17c2 100755 --- a/ranger/config/commands.py +++ b/ranger/config/commands.py @@ -219,6 +219,8 @@ class cd(Command): return matches paths = matches + return None + def _tab_smart(self, dest, dest_abs): tokens = [] basepath = dest_abs @@ -457,6 +459,7 @@ class set_(Command): if name == "colorscheme": return sorted(self.firstpart + colorscheme for colorscheme in get_all_colorschemes(self.fm) if colorscheme.startswith(value)) + return None class setlocal(set_): @@ -785,6 +788,7 @@ class load_copy_buffer(Command): for g in fobj.read().split("\n") if exists(g)) fobj.close() self.fm.ui.redraw_main_column() + return None class save_copy_buffer(Command): @@ -804,6 +808,7 @@ class save_copy_buffer(Command): (fname or self.copy_buffer_filename), bad=True) fobj.write("\n".join(fobj.path for fobj in self.fm.copy_buffer)) fobj.close() + return None class unmark_tag(mark_tag): @@ -925,7 +930,7 @@ class rename(Command): return self.fm.notify('Syntax: rename <newname>', bad=True) if new_name == self.fm.thisfile.relative_path: - return + return None if access(new_name, os.F_OK): return self.fm.notify("Can't rename: file already exists!", bad=True) @@ -937,6 +942,8 @@ class rename(Command): self.fm.thisdir.pointed_obj = file_new self.fm.thisfile = file_new + return None + def tab(self, tabnum): return self._tab_directory_content() @@ -1118,7 +1125,7 @@ class relink(Command): return self.fm.notify('%s is not a symlink!' % tfile.relative_path, bad=True) if new_path == os.readlink(tfile.path): - return + return None try: os.remove(tfile.path) @@ -1130,6 +1137,8 @@ class relink(Command): self.fm.thisdir.pointed_obj = tfile self.fm.thisfile = tfile + return None + def tab(self, tabnum): if not self.rest(1): return self.line + os.readlink(self.fm.thisfile.path) @@ -1159,7 +1168,7 @@ class help_(Command): self.fm.ui.console.ask( "View [m]an page, [k]ey bindings, [c]ommands or [s]ettings? (press q to abort)", callback, - list("mkcsq") + [chr(27)] + list("mqkcs") ) @@ -1177,6 +1186,8 @@ class copymap(Command): for arg in self.args[2:]: self.fm.ui.keymaps.copy(self.context, self.arg(1), arg) + return None + class copypmap(copymap): """:copypmap <keys> <newkeys1> [<newkeys2>...] @@ -1361,7 +1372,10 @@ class scout(Command): if self.OPEN_ON_ENTER in flags or \ (self.AUTO_OPEN in flags and count == 1): - self.fm.move(right=1) + if pattern == '..': + self.fm.cd(pattern) + else: + self.fm.move(right=1) if self.KEEP_OPEN in flags and thisdir != self.fm.thisdir: # reopen the console: @@ -1469,6 +1483,22 @@ class scout(Command): return count == 1 +class narrow(Command): + """ + :narrow + + Show only the files selected right now. If no files are selected, + disable narrowing. + """ + def execute(self): + if self.fm.thisdir.marked_items: + selection = [f.basename for f in self.fm.thistab.get_selection()] + self.fm.thisdir.narrow_filter = selection + else: + self.fm.thisdir.narrow_filter = None + self.fm.thisdir.refilter() + + class filter_inode_type(Command): """ :filter_inode_type [dfl] @@ -1480,22 +1510,12 @@ class filter_inode_type(Command): f display files l display links """ - # pylint: disable=bad-whitespace - FILTER_DIRS = 'd' - FILTER_FILES = 'f' - FILTER_LINKS = 'l' - # pylint: enable=bad-whitespace def execute(self): if not self.arg(1): - self.fm.thisdir.inode_type_filter = None + self.fm.thisdir.inode_type_filter = "" else: - self.fm.thisdir.inode_type_filter = lambda file: ( - True if ( - (self.FILTER_DIRS in self.arg(1) and file.is_directory) or - (self.FILTER_FILES in self.arg(1) and file.is_file and not file.is_link) or - (self.FILTER_LINKS in self.arg(1) and file.is_link) - ) else False) + self.fm.thisdir.inode_type_filter = self.arg(1) self.fm.thisdir.refilter() @@ -1670,3 +1690,63 @@ class linemode(default_linemode): # Ask the browsercolumns to redraw for col in self.fm.ui.browser.columns: col.need_redraw = True + + +class yank(Command): + """:yank [name|dir|path] + + Copies the file's name (default), directory or path into both the primary X + selection and the clipboard. + """ + + modes = { + '': 'basename', + 'name': 'basename', + 'dir': 'dirname', + 'path': 'path', + } + + def execute(self): + import subprocess + + def clipboards(): + from ranger.ext.get_executables import get_executables + clipboard_managers = { + 'xclip': [ + ['xclip'], + ['xclip', '-selection', 'clipboard'], + ], + 'xsel': [ + ['xsel'], + ['xsel', '-b'], + ], + 'pbcopy': [ + ['pbcopy'], + ], + } + ordered_managers = ['pbcopy', 'xclip', 'xsel'] + executables = get_executables() + for manager in ordered_managers: + if manager in executables: + return clipboard_managers[manager] + return [] + + clipboard_commands = clipboards() + + selection = self.get_selection_attr(self.modes[self.arg(1)]) + new_clipboard_contents = "\n".join(selection) + for command in clipboard_commands: + process = subprocess.Popen(command, universal_newlines=True, + stdin=subprocess.PIPE) + process.communicate(input=new_clipboard_contents) + + def get_selection_attr(self, attr): + return [getattr(item, attr) for item in + self.fm.thistab.get_selection()] + + def tab(self, tabnum): + return ( + self.start(1) + mode for mode + in sorted(self.modes.keys()) + if mode + ) diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf index 9af5a953..d7f61550 100644 --- a/ranger/config/rc.conf +++ b/ranger/config/rc.conf @@ -167,6 +167,10 @@ set padding_right true # When false, bookmarks are saved when ranger is exited. set autosave_bookmarks true +# Save the "`" bookmark to disk. This can be used to switch to the last +# directory by typing "``". +set save_backtick_bookmark 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 @@ -221,6 +225,9 @@ set clear_filters_on_dir_change false # Disable displaying line numbers in main column set line_numbers false +# Start line numbers from 1 instead of 0 +set one_indexed false + # Save tabs on exit set save_tabs_on_exit false @@ -228,6 +235,10 @@ set save_tabs_on_exit false # the top and vice versa. set wrap_scroll false +# Set the global_inode_type_filter to nothing. Possible options: d, f and l for +# directories, files and symlinks respectively. +set global_inode_type_filter + # =================================================================== # == Local Options # =================================================================== @@ -262,11 +273,12 @@ alias travel scout -aefklst # =================================================================== # Basic -map Q quit! +map Q quitall map q quit copymap q ZZ ZQ map R reload_cwd +map F set freeze_files! map <C-r> reset map <C-l> redraw_window map <C-c> abort @@ -309,7 +321,7 @@ map uV toggle_visual_mode reverse=True # For the nostalgics: Midnight Commander bindings map <F1> help -map <F2> console rename%space +map <F2> rename_append map <F3> display_file map <F4> edit map <F5> copy @@ -375,10 +387,9 @@ map g? cd /usr/share/doc/ranger map E edit map du shell -p du --max-depth=1 -h --apparent-size map dU shell -p du --max-depth=1 -h --apparent-size | sort -rh -map yp shell -f echo -n %d/%f | xsel -i && xsel -o | xsel -i -b -map yd shell -f echo -n %d | xsel -i && xsel -o | xsel -i -b -map yn shell -f echo -n %f | xsel -i && xsel -o | xsel -i -b -map ys shell -f printf '%%s\n' %s | xsel -i && xsel -o | xsel -i -b +map yp yank path +map yd yank dir +map yn yank name # Filesystem Operations map = chmod diff --git a/ranger/config/rifle.conf b/ranger/config/rifle.conf index 39dee7e9..8bd5f565 100644 --- a/ranger/config/rifle.conf +++ b/ranger/config/rifle.conf @@ -149,6 +149,7 @@ ext pdf, has atril, X, flag f = atril -- "$@" ext pdf, has okular, X, flag f = okular -- "$@" ext pdf, has epdfview, X, flag f = epdfview -- "$@" ext pdf, has qpdfview, X, flag f = qpdfview "$@" +ext pdf, has open, X, flat f = open "$@" ext docx?, has catdoc, terminal = catdoc -- "$@" | "$PAGER" @@ -162,6 +163,9 @@ ext djvu, has zathura,X, flag f = zathura -- "$@" ext djvu, has evince, X, flag f = evince -- "$@" ext djvu, has atril, X, flag f = atril -- "$@" +ext epub, has ebook-viewer, X, flag f = ebook-viewer -- "$@" +ext mobi, has ebook-viewer, X, flag f = ebook-viewer -- "$@" + #------------------------------------------- # Image Viewing: #------------------------------------------- @@ -175,6 +179,7 @@ mime ^image, has mirage, X, flag f = mirage -- "$@" mime ^image, has ristretto, X, flag f = ristretto "$@" mime ^image, has eog, X, flag f = eog -- "$@" mime ^image, has eom, X, flag f = eom -- "$@" +mime ^image, has nomacs, X, flag f = nomacs -- "$@" mime ^image, has gimp, X, flag f = gimp -- "$@" ext xcf, X, flag f = gimp -- "$@" diff --git a/ranger/container/bookmarks.py b/ranger/container/bookmarks.py index cba07367..59838c00 100644 --- a/ranger/container/bookmarks.py +++ b/ranger/container/bookmarks.py @@ -28,7 +28,8 @@ class Bookmarks(FileManagerAware): autosave = True load_pattern = re.compile(r"^[\d\w']:.") - def __init__(self, bookmarkfile, bookmarktype=str, autosave=False): + def __init__(self, bookmarkfile, bookmarktype=str, autosave=False, + nonpersistent_bookmarks=()): """Initializes Bookmarks. <bookmarkfile> specifies the path to the file where @@ -39,6 +40,7 @@ class Bookmarks(FileManagerAware): self.original_dict = {} self.path = bookmarkfile self.bookmarktype = bookmarktype + self.nonpersistent_bookmarks = set(nonpersistent_bookmarks) def load(self): """Load the bookmarks from path/bookmarks""" @@ -174,7 +176,8 @@ class Bookmarks(FileManagerAware): self.fm.notify('Bookmarks error: {0}'.format(str(ex)), bad=True) return for key, value in self.dct.items(): - if isinstance(key, str) and key in ALLOWED_KEYS: + if isinstance(key, str) and key in ALLOWED_KEYS \ + and key not in self.nonpersistent_bookmarks: fobj.write("{0}:{1}\n".format(str(key), str(value))) fobj.close() @@ -189,6 +192,16 @@ class Bookmarks(FileManagerAware): self._update_mtime() + def enable_saving_backtick_bookmark(self, boolean): + """ + Adds or removes the ' from the list of nonpersitent bookmarks + """ + if boolean: + if "'" in self.nonpersistent_bookmarks: + self.nonpersistent_bookmarks.remove("'") # enable + else: + self.nonpersistent_bookmarks.add("'") # disable + def _load_dict(self): if self.path is None: return {} diff --git a/ranger/container/directory.py b/ranger/container/directory.py index 168a46c7..18b1687c 100644 --- a/ranger/container/directory.py +++ b/ranger/container/directory.py @@ -93,6 +93,12 @@ def mtimelevel(path, level): return mtime +class InodeFilterConstants(object): # pylint: disable=too-few-public-methods + DIRS = 'd' + FILES = 'f' + LINKS = 'l' + + class Directory( # pylint: disable=too-many-instance-attributes,too-many-public-methods FileSystemObject, Accumulator, Loadable): is_directory = True @@ -108,6 +114,7 @@ class Directory( # pylint: disable=too-many-instance-attributes,too-many-public files_all = None filter = None temporary_filter = None + narrow_filter = None inode_type_filter = None marked_items = None scroll_begin = 0 @@ -122,8 +129,8 @@ class Directory( # pylint: disable=too-many-instance-attributes,too-many-public content_outdated = False content_loaded = False - vcs = None has_vcschild = False + _vcs_signal_handler_installed = False cumulative_size_calculated = False @@ -160,11 +167,20 @@ class Directory( # pylint: disable=too-many-instance-attributes,too-many-public self.settings = LocalSettings(path, self.settings) - if self.settings.vcs_aware: - self.vcs = Vcs(self) - self.use() + @lazy_property + def vcs(self): + if not self._vcs_signal_handler_installed: + self.settings.signal_bind( + 'setopt.vcs_aware', self.vcs__reset, # pylint: disable=no-member + weak=True, autosort=False, + ) + self._vcs_signal_handler_installed = True + if self.settings.vcs_aware: + return Vcs(self) + return None + def signal_function_factory(self, function): def signal_function(): self.load_if_outdated() @@ -252,11 +268,32 @@ class Directory( # pylint: disable=too-many-instance-attributes,too-many-public return False return True filters.append(hidden_filter_func) + if self.narrow_filter: + # pylint: disable=unsupported-membership-test + + # Pylint complains that self.narrow_filter is by default + # None but the execution won't reach this line if it is + # still None. + filters.append(lambda fobj: fobj.basename in self.narrow_filter) + if self.settings.global_inode_type_filter or self.inode_type_filter: + def inode_filter_func(obj): + # Use local inode_type_filter if present, global otherwise + inode_filter = self.inode_type_filter or self.settings.global_inode_type_filter + # Apply filter + if InodeFilterConstants.DIRS in inode_filter and \ + obj.is_directory: + return True + elif InodeFilterConstants.FILES in inode_filter and \ + obj.is_file and not obj.is_link: + return True + elif InodeFilterConstants.LINKS in inode_filter and \ + obj.is_link: + return True + return False + filters.append(inode_filter_func) if self.filter: filter_search = self.filter.search filters.append(lambda fobj: filter_search(fobj.basename)) - if self.inode_type_filter: - filters.append(self.inode_type_filter) if self.temporary_filter: temporary_filter_search = self.temporary_filter.search filters.append(lambda fobj: temporary_filter_search(fobj.basename)) @@ -436,6 +473,8 @@ class Directory( # pylint: disable=too-many-instance-attributes,too-many-public Use this sparingly since it takes rather long. """ self.content_outdated = False + if self.settings.freeze_files: + return if not self.loading: if not self.loaded: diff --git a/ranger/container/file.py b/ranger/container/file.py index 17ca577c..d2daa169 100644 --- a/ranger/container/file.py +++ b/ranger/container/file.py @@ -48,7 +48,6 @@ class File(FileSystemObject): preview_data = None preview_known = False preview_loading = False - linemode = "filename" _firstbytes = None @property diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py index d24a01ce..0c9f70f6 100644 --- a/ranger/container/fsobject.py +++ b/ranger/container/fsobject.py @@ -6,7 +6,7 @@ from __future__ import (absolute_import, division, print_function) import re from grp import getgrgid from os import lstat, stat -from os.path import abspath, basename, dirname, realpath, splitext, extsep, relpath +from os.path import abspath, basename, dirname, realpath, relpath from pwd import getpwuid from time import time @@ -47,42 +47,39 @@ def safe_path(path): return path.translate(_SAFE_STRING_TABLE) -class FileSystemObject( # pylint: disable=too-many-instance-attributes +class FileSystemObject( # pylint: disable=too-many-instance-attributes,too-many-public-methods FileManagerAware, SettingsAware): - (basename, - relative_path, - relative_path_lower, - dirname, - extension, - infostring, - path, - permissions, - stat) = (None,) * 9 - - (content_loaded, - force_load, - - is_device, - is_directory, - is_file, - is_fifo, - is_link, - is_socket, - - accessible, - exists, # "exists" currently means "link_target_exists" - loaded, - marked, - runnable, - stopped, - tagged, - - audio, - container, - document, - image, - media, - video) = (False,) * 21 + basename = None + relative_path = None + infostring = None + path = None + permissions = None + stat = None + + content_loaded = False + force_load = False + + is_device = False + is_directory = False + is_file = False + is_fifo = False + is_link = False + is_socket = False + + accessible = False + exists = False # "exists" currently means "link_target_exists" + loaded = False + marked = False + runnable = False + stopped = False + tagged = False + + audio = False + container = False + document = False + image = False + media = False + video = False size = 0 @@ -91,7 +88,6 @@ class FileSystemObject( # pylint: disable=too-many-instance-attributes vcsstatus = None vcsremotestatus = None - linemode = DEFAULT_LINEMODE linemode_dict = dict( (linemode.name, linemode()) for linemode in [DefaultLinemode, TitleLinemode, PermissionsLinemode, FileInfoLinemode, @@ -107,34 +103,41 @@ class FileSystemObject( # pylint: disable=too-many-instance-attributes self.relative_path = self.basename else: self.relative_path = relpath(path, basename_is_rel_to) - self.relative_path_lower = self.relative_path.lower() - self.extension = splitext(self.basename)[1].lstrip(extsep) or None - self.dirname = dirname(path) self.preload = preload self.display_data = {} + def __repr__(self): + return "<{0} {1}>".format(self.__class__.__name__, self.path) + + @lazy_property + def extension(self): try: lastdot = self.basename.rindex('.') + 1 - self.extension = self.basename[lastdot:].lower() + return self.basename[lastdot:].lower() except ValueError: - self.extension = None + return None + @lazy_property + def relative_path_lower(self): + return self.relative_path.lower() + + @lazy_property + def linemode(self): # pylint: disable=method-hidden # Set the line mode from fm.default_linemodes for method, argument, linemode in self.fm.default_linemodes: if linemode in self.linemode_dict: if method == "always": - self.linemode = linemode - break - if method == "path" and argument.search(path): - self.linemode = linemode - break + return linemode + if method == "path" and argument.search(self.path): + return linemode if method == "tag" and self.realpath in self.fm.tags and \ self.fm.tags.marker(self.realpath) in argument: - self.linemode = linemode - break + return linemode + return DEFAULT_LINEMODE - def __repr__(self): - return "<{0} {1}>".format(self.__class__.__name__, self.path) + @lazy_property + def dirname(self): + return dirname(self.path) @lazy_property def shell_escaped_basename(self): @@ -260,16 +263,19 @@ class FileSystemObject( # pylint: disable=too-many-instance-attributes return None # it is impossible to get the link destination return self.path - def load(self): + def load(self): # pylint: disable=too-many-statements """Loads information about the directory itself. reads useful information about the filesystem-object from the filesystem and caches it for later use """ + self.loaded = True + if self.settings.freeze_files: + return + self.display_data = {} self.fm.update_preview(self.path) - self.loaded = True # Get the stat object, either from preload or from [l]stat self.permissions = None diff --git a/ranger/container/settings.py b/ranger/container/settings.py index 70e299c7..bb902f9d 100644 --- a/ranger/container/settings.py +++ b/ranger/container/settings.py @@ -30,6 +30,7 @@ ALLOWED_SETTINGS = { 'cd_bookmarks': bool, 'cd_tab_case': str, 'cd_tab_smart': bool, + 'clear_filters_on_dir_change': bool, 'collapse_preview': bool, 'colorscheme': str, 'column_ratios': (tuple, list), @@ -41,13 +42,17 @@ ALLOWED_SETTINGS = { 'draw_borders': bool, 'draw_progress_bar_in_status_bar': bool, 'flushinput': bool, + 'freeze_files': bool, + 'global_inode_type_filter': str, 'hidden_filter': str, + 'hostname_in_titlebar': bool, 'idle_delay': int, 'line_numbers': str, 'max_console_history_size': (int, type(None)), 'max_history_size': (int, type(None)), 'metadata_deep_search': bool, 'mouse_enabled': bool, + 'one_indexed': bool, 'open_all_images': bool, 'padding_right': bool, 'preview_directories': bool, @@ -56,41 +61,41 @@ ALLOWED_SETTINGS = { 'preview_images_method': str, 'preview_max_size': int, 'preview_script': (str, type(None)), + 'save_backtick_bookmark': bool, 'save_console_history': bool, + 'save_tabs_on_exit': bool, 'scroll_offset': int, 'shorten_title': int, 'show_cursor': bool, # TODO: not working? - 'show_selection_in_titlebar': bool, 'show_hidden_bookmarks': bool, 'show_hidden': bool, + 'show_selection_in_titlebar': bool, 'sort_case_insensitive': bool, 'sort_directories_first': bool, 'sort_reverse': bool, - 'sort_unicode': bool, 'sort': str, + 'sort_unicode': bool, 'status_bar_on_top': bool, - 'hostname_in_titlebar': bool, 'tilde_in_titlebar': bool, 'unicode_ellipsis': bool, 'update_title': bool, 'update_tmux_title': bool, 'use_preview_script': bool, - 'viewmode': str, 'vcs_aware': bool, 'vcs_backend_bzr': str, 'vcs_backend_git': str, 'vcs_backend_hg': str, 'vcs_backend_svn': str, + 'viewmode': str, 'wrap_scroll': bool, 'xterm_alt_key': bool, - 'clear_filters_on_dir_change': bool, - 'save_tabs_on_exit': bool, } ALLOWED_VALUES = { 'cd_tab_case': ['sensitive', 'insensitive', 'smart'], 'confirm_on_delete': ['multiple', 'always', 'never'], 'line_numbers': ['false', 'absolute', 'relative'], + 'one_indexed': [False, True], 'preview_images_method': ['w3m', 'iterm2', 'urxvt', 'urxvt-full'], 'vcs_backend_bzr': ['disabled', 'local', 'enabled'], 'vcs_backend_git': ['enabled', 'disabled', 'local'], @@ -298,6 +303,8 @@ class LocalSettings(object): # pylint: disable=too-few-public-methods def __getattr__(self, name): if name.startswith('_'): return self.__dict__[name] + if name.startswith('signal_'): + return getattr(self._parent, name) return self._parent.get(name, self._path) def __iter__(self): diff --git a/ranger/container/tags.py b/ranger/container/tags.py index 7aed5131..dabaf6a8 100644 --- a/ranger/container/tags.py +++ b/ranger/container/tags.py @@ -102,7 +102,7 @@ class Tags(object): def _parse(self, fobj): result = dict() for line in fobj: - line = line.strip() + line = line.rstrip('\n') if len(line) > 2 and line[1] == ':': tag, path = line[0], line[2:] if tag in ALLOWED_KEYS: diff --git a/ranger/core/actions.py b/ranger/core/actions.py index 3b30fe6b..0298af19 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -64,7 +64,7 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m def reset(self): """:reset - Reset the filemanager, clearing the directory buffer. + Reset the filemanager, clearing the directory buffer, reload rifle config """ old_path = self.thisdir.path self.previews = {} @@ -73,6 +73,7 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m self.change_mode('normal') if self.metadata: self.metadata.reset() + self.rifle.reload_config() def change_mode(self, mode=None): """:change_mode <mode> @@ -228,7 +229,7 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m cmd_class = self.commands.get_command(command_name) if cmd_class is None: self.notify("Command not found: `%s'" % command_name, bad=True) - return + return None cmd = cmd_class(string, quantifier=quantifier) if cmd.resolve_macros and _MacroTemplate.delimiter in cmd.line: @@ -251,6 +252,7 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m if ranger.args.debug: raise self.notify(ex) + return None def substitute_macros(self, string, # pylint: disable=redefined-outer-name additional=None, escape=False): @@ -461,6 +463,7 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m """ cwd = self.thisdir kw.setdefault('cycle', self.fm.settings['wrap_scroll']) + kw.setdefault('one_indexed', self.fm.settings['one_indexed']) direction = Direction(kw) if 'left' in direction or direction.left() > 0: steps = direction.left() @@ -737,6 +740,7 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m # -- Searching # -------------------------- + # TODO: Do we still use this method? Should we remove it? def search_file(self, text, offset=1, regexp=True): if isinstance(text, str) and regexp: try: @@ -745,6 +749,7 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m return False self.thistab.last_search = text self.search_next(order='search', offset=offset) + return None def search_next(self, order=None, offset=1, forward=True): original_order = order @@ -795,6 +800,7 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m return cwd.cycle(forward=None) return cwd.cycle(forward=forward) + return None def set_search_method(self, order, forward=True): # pylint: disable=unused-argument if order in ('search', 'tag', 'size', 'mimetype', 'ctime', 'mtime', 'atime'): @@ -1025,7 +1031,8 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m return path cacheimg = os.path.join(ranger.args.cachedir, self.sha1_encode(path)) - if os.path.isfile(cacheimg) and \ + if self.settings.preview_images and \ + os.path.isfile(cacheimg) and \ os.path.getmtime(cacheimg) > os.path.getmtime(path): data['foundpreview'] = True data['imagepreview'] = True @@ -1081,6 +1088,7 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m else: pager.set_source(self.thisfile.get_preview_source( pager.wid, pager.hei)) + return None def on_destroy(signal): # pylint: disable=unused-argument try: @@ -1171,6 +1179,7 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m newtab = tablist[(current_index + offset) % len(tablist)] if newtab != self.current_tab: self.tab_open(newtab) + return None def tab_new(self, path=None, narg=None): if narg: @@ -1178,6 +1187,7 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m for i in range(1, 10): if i not in self.tabs: return self.tab_open(i, path) + return None def tab_switch(self, path, create_directory=False): """Switches to tab of given path, opening a new tab as necessary. diff --git a/ranger/core/fm.py b/ranger/core/fm.py index b8d38397..c55a3922 100644 --- a/ranger/core/fm.py +++ b/ranger/core/fm.py @@ -105,6 +105,11 @@ class FM(Actions, # pylint: disable=too-many-instance-attributes self.settings.signal_bind('setopt.preview_images_method', set_image_displayer, priority=settings.SIGNAL_PRIORITY_AFTER_SYNC) + self.settings.signal_bind( + 'setopt.preview_images', + lambda signal: signal.fm.previews.clear(), + ) + if ranger.args.clean: self.tags = TagsDummy("") elif self.tags is None: @@ -120,6 +125,8 @@ class FM(Actions, # pylint: disable=too-many-instance-attributes bookmarktype=Directory, autosave=self.settings.autosave_bookmarks) self.bookmarks.load() + self.bookmarks.enable_saving_backtick_bookmark( + self.settings.save_backtick_bookmark) self.ui.setup_curses() self.ui.initialize() @@ -186,6 +193,10 @@ class FM(Actions, # pylint: disable=too-many-instance-attributes 'setopt.metadata_deep_search', lambda signal: setattr(signal.fm.metadata, 'deep_search', signal.value) ) + self.settings.signal_bind( + 'setopt.save_backtick_bookmark', + lambda signal: signal.fm.bookmarks.enable_saving_backtick_bookmark(signal.value) + ) def destroy(self): debug = ranger.args.debug diff --git a/ranger/core/main.py b/ranger/core/main.py index 38513970..0148e2b5 100644 --- a/ranger/core/main.py +++ b/ranger/core/main.py @@ -119,6 +119,10 @@ def main( FileManagerAware.fm_set(fm) load_settings(fm, args.clean) + if args.show_only_dirs: + from ranger.container.directory import InodeFilterConstants + fm.settings.global_inode_type_filter = InodeFilterConstants.DIRS + if args.list_unused_keys: from ranger.ext.keybinding_parser import (special_keys, reversed_special_keys) @@ -274,6 +278,8 @@ def parse_arguments(): parser.add_option('--choosedir', type='string', metavar='PATH', help="Makes ranger act like a directory chooser. When ranger quits" ", it will write the name of the last visited directory to PATH") + parser.add_option('--show-only-dirs', action='store_true', + help="Show only directories, no files or links") parser.add_option('--selectfile', type='string', metavar='filepath', help="Open ranger with supplied file selected.") parser.add_option('--list-unused-keys', action='store_true', @@ -350,19 +356,6 @@ def load_settings( # pylint: disable=too-many-locals,too-many-branches,too-many LOG.debug("Loaded custom commands from '%s'", custom_comm_path) sys.dont_write_bytecode = old_bytecode_setting - allow_access_to_confdir(ranger.args.confdir, False) - - # Load rc.conf - custom_conf = fm.confpath('rc.conf') - default_conf = fm.relpath('config', 'rc.conf') - - if os.environ.get('RANGER_LOAD_DEFAULT_RC', 'TRUE').upper() != 'FALSE': - fm.source(default_conf) - if os.access(custom_conf, os.R_OK): - fm.source(custom_conf) - - allow_access_to_confdir(ranger.args.confdir, True) - # XXX Load plugins (experimental) plugindir = fm.confpath('plugins') try: @@ -399,6 +392,17 @@ def load_settings( # pylint: disable=too-many-locals,too-many-branches,too-many ranger.fm = None allow_access_to_confdir(ranger.args.confdir, False) + # Load rc.conf + custom_conf = fm.confpath('rc.conf') + default_conf = fm.relpath('config', 'rc.conf') + + custom_conf_is_readable = os.access(custom_conf, os.R_OK) + if (os.environ.get('RANGER_LOAD_DEFAULT_RC', 'TRUE').upper() != 'FALSE' or + not custom_conf_is_readable): + fm.source(default_conf) + if custom_conf_is_readable: + fm.source(custom_conf) + else: fm.source(fm.relpath('config', 'rc.conf')) diff --git a/ranger/core/metadata.py b/ranger/core/metadata.py index 28b1f5b7..75f7ba3c 100644 --- a/ranger/core/metadata.py +++ b/ranger/core/metadata.py @@ -90,26 +90,25 @@ class MetadataManager(object): def _get_entry(self, filename): if filename in self.metadata_cache: return self.metadata_cache[filename] - else: - - # Try to find an entry for this file in any of - # the applicable .metadata.json files - for metafile in self._get_metafile_names(filename): - entries = self._get_metafile_content(metafile) - # Check for a direct match: - if filename in entries: - entry = entries[filename] - # Check for a match of the base name: - elif basename(filename) in entries: - entry = entries[basename(filename)] - else: - # No match found, try another entry - continue - - self.metadata_cache[filename] = entry - return entry - - raise KeyError + + # Try to find an entry for this file in any of + # the applicable .metadata.json files + for metafile in self._get_metafile_names(filename): + entries = self._get_metafile_content(metafile) + # Check for a direct match: + if filename in entries: + entry = entries[filename] + # Check for a match of the base name: + elif basename(filename) in entries: + entry = entries[basename(filename)] + else: + # No match found, try another entry + continue + + self.metadata_cache[filename] = entry + return entry + + raise KeyError def _get_metafile_content(self, metafile): import json diff --git a/ranger/core/runner.py b/ranger/core/runner.py index 8c3e3162..bb4e512a 100644 --- a/ranger/core/runner.py +++ b/ranger/core/runner.py @@ -27,7 +27,7 @@ from __future__ import (absolute_import, division, print_function) import logging import os import sys -from subprocess import Popen, PIPE +from subprocess import Popen, PIPE, STDOUT from ranger.ext.get_executables import get_executables, get_term from ranger.ext.popen_forked import Popen_forked @@ -185,7 +185,7 @@ class Runner(object): # pylint: disable=too-few-public-methods if 'p' in context.flags: popen_kws['stdout'] = PIPE - popen_kws['stderr'] = PIPE + popen_kws['stderr'] = STDOUT toggle_ui = False pipe_output = True context.wait = False diff --git a/ranger/core/tab.py b/ranger/core/tab.py index 160f590f..1d5e69d4 100644 --- a/ranger/core/tab.py +++ b/ranger/core/tab.py @@ -111,7 +111,7 @@ class Tab(FileManagerAware, SettingsAware): # pylint: disable=too-many-instance """Enter given path""" # TODO: Ensure that there is always a self.thisdir if path is None: - return + return None path = str(path) # clear filter in the folder we're leaving diff --git a/ranger/data/mime.types b/ranger/data/mime.types index f338ab84..c1124271 100644 --- a/ranger/data/mime.types +++ b/ranger/data/mime.types @@ -14,6 +14,7 @@ audio/flac flac audio/musepack mpc mpp mp+ audio/ogg oga ogg spx audio/wavpack wv wvc +audio/webm weba video/mkv mkv video/webm webm diff --git a/ranger/data/scope.sh b/ranger/data/scope.sh index 8386eac2..560e27da 100755 --- a/ranger/data/scope.sh +++ b/ranger/data/scope.sh @@ -95,6 +95,15 @@ handle_image() { # Image image/*) + local orientation + orientation="$( identify -format '%[EXIF:Orientation]\n' -- "${FILE_PATH}" )" + # If orientation data is present and the image actually + # needs rotating ("1" means no rotation)... + if [[ -n "$orientation" && "$orientation" != 1 ]]; then + # ...auto-rotate the image according to the EXIF data. + convert -- "${FILE_PATH}" -auto-orient "${IMAGE_CACHE_PATH}" && exit 6 + fi + # `w3mimgdisplay` will be called for all images (unless overriden as above), # but might fail for unsupported types. exit 7;; @@ -104,6 +113,15 @@ handle_image() { # # Thumbnail # ffmpegthumbnailer -i "${FILE_PATH}" -o "${IMAGE_CACHE_PATH}" -s 0 && exit 6 # exit 1;; + # PDF + # application/pdf) + # pdftoppm -f 1 -l 1 \ + # -scale-to-x 1920 \ + # -scale-to-y -1 \ + # -singlefile \ + # -jpeg -tiffcompression jpeg \ + # -- "${FILE_PATH}" "${IMAGE_CACHE_PATH%.*}" \ + # && exit 6 || exit 1;; esac } @@ -149,11 +167,11 @@ handle_fallback() { } -handle_extension MIMETYPE="$( file --dereference --brief --mime-type -- "${FILE_PATH}" )" if [[ "${PV_IMAGE_ENABLED}" == 'True' ]]; then handle_image "${MIMETYPE}" fi +handle_extension handle_mime "${MIMETYPE}" handle_fallback diff --git a/ranger/ext/accumulator.py b/ranger/ext/accumulator.py index b0e4a1c5..c2e33b59 100644 --- a/ranger/ext/accumulator.py +++ b/ranger/ext/accumulator.py @@ -29,12 +29,12 @@ class Accumulator(object): def move_to_obj(self, arg, attr=None): if not arg: - return + return None lst = self.get_list() if not lst: - return + return None do_get_attr = isinstance(attr, str) diff --git a/ranger/ext/cached_function.py b/ranger/ext/cached_function.py index 4255443c..7fdd579e 100644 --- a/ranger/ext/cached_function.py +++ b/ranger/ext/cached_function.py @@ -4,6 +4,7 @@ from __future__ import (absolute_import, division, print_function) +# Similar to functools.lru_cache of python3 def cached_function(fnc): cache = {} diff --git a/ranger/ext/direction.py b/ranger/ext/direction.py index bbb69c9b..7df45556 100644 --- a/ranger/ext/direction.py +++ b/ranger/ext/direction.py @@ -20,6 +20,8 @@ False from __future__ import (absolute_import, division, print_function) +import math + class Direction(dict): @@ -94,6 +96,10 @@ class Direction(dict): def cycle(self): return self.get('cycle') in (True, 'true', 'on', 'yes') + def one_indexed(self): + return ('one_indexed' in self and + self.get('one_indexed') in (True, 'true', 'on', 'yes')) + def multiply(self, n): for key in ('up', 'right', 'down', 'left'): try: @@ -127,7 +133,10 @@ class Direction(dict): pos = direction if override is not None: if self.absolute(): - pos = override + if self.one_indexed(): + pos = override - 1 + else: + pos = override else: pos *= override if self.pages(): @@ -142,8 +151,16 @@ class Direction(dict): if self.cycle(): cycles, pos = divmod(pos, (maximum + offset - minimum)) self['_move_cycles'] = int(cycles) - return int(minimum + pos) - return int(max(min(pos, maximum + offset - 1), minimum)) + ret = minimum + pos + else: + ret = max(min(pos, maximum + offset - 1), minimum) + # Round towards the direction we're moving from. + # From the UI point of view, round down. See: #912. + if direction < 0: + ret = int(math.ceil(ret)) + else: + ret = int(ret) + return ret def move_cycles(self): return self.get('_move_cycles', 0) diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py index 15eecd1b..abcdd7b1 100644 --- a/ranger/ext/img_display.py +++ b/ranger/ext/img_display.py @@ -32,6 +32,7 @@ W3MIMGDISPLAY_PATHS = [ '/usr/libexec/w3m/w3mimgdisplay', '/usr/lib64/w3m/w3mimgdisplay', '/usr/libexec64/w3m/w3mimgdisplay', + '/usr/local/libexec/w3m/w3mimgdisplay', ] @@ -121,6 +122,8 @@ class W3MImageDisplayer(ImageDisplayer): self.process.stdin.write(input_gen) self.process.stdin.flush() self.process.stdout.readline() + self.quit() + self.is_initialized = False def clear(self, start_x, start_y, width, height): if not self.is_initialized or self.process.poll() is not None: diff --git a/ranger/ext/lazy_property.py b/ranger/ext/lazy_property.py index 92dc309d..bb54bd5e 100644 --- a/ranger/ext/lazy_property.py +++ b/ranger/ext/lazy_property.py @@ -1,4 +1,6 @@ -# From http://blog.pythonisito.com/2008/08/lazy-descriptors.html +# This file is part of ranger, the console file manager. +# License: GNU GPL version 3, see the file "AUTHORS" for details. +# Based on http://blog.pythonisito.com/2008/08/lazy-descriptors.html from __future__ import (absolute_import, division, print_function) @@ -7,16 +9,30 @@ class lazy_property(object): # pylint: disable=invalid-name,too-few-public-meth """A @property-like decorator with lazy evaluation >>> class Foo: + ... counter = 0 ... @lazy_property ... def answer(self): - ... print("calculating answer...") - ... return 2*3*7 + ... Foo.counter += 1 + ... return Foo.counter >>> foo = Foo() >>> foo.answer - calculating answer... - 42 + 1 >>> foo.answer - 42 + 1 + >>> foo.answer__reset() + >>> foo.answer + 2 + >>> foo.answer + 2 + + Avoid interaction between multiple objects: + + >>> bar = Foo() + >>> bar.answer + 3 + >>> foo.answer__reset() + >>> bar.answer + 3 """ def __init__(self, method): @@ -27,6 +43,15 @@ class lazy_property(object): # pylint: disable=invalid-name,too-few-public-meth def __get__(self, obj, cls=None): if obj is None: # to fix issues with pydoc return None + + reset_function_name = self.__name__ + "__reset" + + if not hasattr(obj, reset_function_name): + def reset_function(): + setattr(obj, self.__name__, self) + del obj.__dict__[self.__name__] # force "__get__" being called + obj.__dict__[reset_function_name] = reset_function + result = self._method(obj) obj.__dict__[self.__name__] = result return result diff --git a/ranger/ext/next_available_filename.py b/ranger/ext/next_available_filename.py index 91d48631..0a5e0856 100644 --- a/ranger/ext/next_available_filename.py +++ b/ranger/ext/next_available_filename.py @@ -19,3 +19,4 @@ def next_available_filename(fname, directory="."): for i in range(1, len(existing_files) + 1): if fname + str(i) not in existing_files: return fname + str(i) + return None diff --git a/ranger/ext/rifle.py b/ranger/ext/rifle.py index cfb07f5f..a6c0b9f0 100755 --- a/ranger/ext/rifle.py +++ b/ranger/ext/rifle.py @@ -244,6 +244,7 @@ class Rifle(object): # pylint: disable=too-many-instance-attributes return bool(os.environ.get(argument)) elif function == 'else': return True + return None def get_mimetype(self, fname): # Spawn "file" to determine the mime-type of the given file. @@ -295,7 +296,7 @@ class Rifle(object): # pylint: disable=too-many-instance-attributes count = self._skip yield (count, cmd, self._app_label, self._app_flags) - def execute(self, files, # pylint: disable=too-many-branches,too-many-statements + def execute(self, files, # noqa: E501 pylint: disable=too-many-branches,too-many-statements,too-many-locals number=0, label=None, flags="", mimetype=None): """Executes the given list of files. @@ -379,6 +380,8 @@ class Rifle(object): # pylint: disable=too-many-instance-attributes finally: self.hook_after_executing(command, self._mimetype, self._app_flags) + return None + def find_conf_path(): # Find configuration file path diff --git a/ranger/ext/signals.py b/ranger/ext/signals.py index cbd35643..67c8960d 100644 --- a/ranger/ext/signals.py +++ b/ranger/ext/signals.py @@ -155,6 +155,7 @@ class SignalDispatcher(object): key=lambda handler: -handler.priority) return handler + # TODO: Do we still use this method? Should we remove it? def signal_force_sort(self, signal_name=None): """Forces a sorting of signal handlers by priority. @@ -165,11 +166,12 @@ class SignalDispatcher(object): for handlers in self._signals.values(): handlers.sort( key=lambda handler: -handler.priority) + return None elif signal_name in self._signals: self._signals[signal_name].sort( key=lambda handler: -handler.priority) - else: - return False + return None + return False def signal_unbind(self, signal_handler): """Removes a signal binding. diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index ba28d141..e2838f8d 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -112,7 +112,11 @@ class Vcs(object): # pylint: disable=too-many-instance-attributes def _run(self, args, path=None, # pylint: disable=too-many-arguments catchout=True, retbytes=False, rstrip_newline=True): """Run a command""" - cmd = [self.repotype] + args + if self.repotype == 'hg': + # use "chg", a faster built-in client + cmd = ['chg'] + args + else: + cmd = [self.repotype] + args if path is None: path = self.path @@ -125,6 +129,7 @@ class Vcs(object): # pylint: disable=too-many-instance-attributes else: with open(os.devnull, mode='w') as fd_devnull: subprocess.check_call(cmd, cwd=path, stdout=fd_devnull, stderr=fd_devnull) + return None except (subprocess.CalledProcessError, OSError): raise VcsError('{0:s}: {1:s}'.format(str(cmd), path)) diff --git a/ranger/ext/widestring.py b/ranger/ext/widestring.py index ed66f3c7..2721643c 100644 --- a/ranger/ext/widestring.py +++ b/ranger/ext/widestring.py @@ -1,4 +1,4 @@ -# -*- encoding: utf8 -*- +# -*- encoding: utf-8 -*- # This file is part of ranger, the console file manager. # License: GNU GPL version 3, see the file "AUTHORS" for details. @@ -81,6 +81,7 @@ class WideString(object): # pylint: disable=too-few-public-methods return WideString(self.string + string) elif isinstance(string, WideString): return WideString(self.string + string.string, self.chars + string.chars) + return None def __radd__(self, string): """ @@ -91,6 +92,7 @@ class WideString(object): # pylint: disable=too-few-public-methods return WideString(string + self.string) elif isinstance(string, WideString): return WideString(string.string + self.string, string.chars + self.chars) + return None def __str__(self): return self.string diff --git a/ranger/gui/bar.py b/ranger/gui/bar.py index 64bd6ce1..3f4b891c 100644 --- a/ranger/gui/bar.py +++ b/ranger/gui/bar.py @@ -77,6 +77,7 @@ class Bar(object): else: item.cut_off(oversize) break + return None def fill_gap(self, char, wid, gapwidth=False): del self.gap[:] diff --git a/ranger/gui/context.py b/ranger/gui/context.py index 7c7f1727..d8d1957c 100644 --- a/ranger/gui/context.py +++ b/ranger/gui/context.py @@ -16,7 +16,7 @@ CONTEXT_KEYS = [ 'good', 'bad', 'space', 'permissions', 'owner', 'group', 'mtime', 'nlink', 'scroll', 'all', 'bot', 'top', 'percentage', 'filter', - 'flat', 'marked', 'tagged', 'tag_marker', 'cut', 'copied', + 'flat', 'marked', 'tagged', 'tag_marker', 'cut', 'copied', 'frozen', 'help_markup', # COMPAT 'seperator', 'key', 'special', 'border', # COMPAT 'title', 'text', 'highlight', 'bars', 'quotes', 'tab', 'loaded', diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py index 5d53603f..990db0ad 100644 --- a/ranger/gui/ui.py +++ b/ranger/gui/ui.py @@ -102,6 +102,7 @@ class UI( # pylint: disable=too-many-instance-attributes,too-many-public-method pass self.settings.signal_bind('setopt.mouse_enabled', _setup_mouse) + self.settings.signal_bind('setopt.freeze_files', self.redraw_statusbar) _setup_mouse(dict(value=self.settings.mouse_enabled)) if not self.is_set_up: @@ -452,6 +453,9 @@ class UI( # pylint: disable=too-many-instance-attributes,too-many-public-method def redraw_main_column(self): self.browser.main_column.need_redraw = True + def redraw_statusbar(self): + self.status.need_redraw = True + def close_taskview(self): self.taskview.visible = False self.browser.visible = True @@ -505,6 +509,7 @@ class UI( # pylint: disable=too-many-instance-attributes,too-many-public-method if viewmode == 'miller': from ranger.gui.widgets.view_miller import ViewMiller return ViewMiller - if viewmode == 'multipane': + elif viewmode == 'multipane': from ranger.gui.widgets.view_multipane import ViewMultipane return ViewMultipane + return None diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index 14fde110..b3272cbc 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -17,6 +17,10 @@ from . import Widget from .pager import Pager +def hook_before_drawing(fsobject, color_list): + return fsobject, color_list + + class BrowserColumn(Pager): # pylint: disable=too-many-instance-attributes main_column = False display_infostring = False @@ -203,7 +207,12 @@ class BrowserColumn(Pager): # pylint: disable=too-many-instance-attributes if self.settings.line_numbers == 'relative': line_number = abs(selected_i - i) if line_number == 0: - line_number = selected_i + if self.settings.one_indexed: + line_number = selected_i + 1 + else: + line_number = selected_i + elif self.settings.one_indexed: + line_number += 1 return linum_format.format(line_number) @@ -382,6 +391,8 @@ class BrowserColumn(Pager): # pylint: disable=too-many-instance-attributes display_data = [] drawn.display_data[key] = display_data + drawn, this_color = hook_before_drawing(drawn, this_color) + predisplay = predisplay_left + predisplay_right for txt, color in predisplay: attr = self.settings.colorscheme.get_attr(*(this_color + color)) diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py index 2d905f62..13201e34 100644 --- a/ranger/gui/widgets/console.py +++ b/ranger/gui/widgets/console.py @@ -87,21 +87,28 @@ class Console(Widget): # pylint: disable=too-many-instance-attributes,too-many- fobj.close() Widget.destroy(self) + def _calculate_offset(self): + wid = self.wid - 2 + whalf = wid // 2 + if self.pos < whalf or len(self.line) < wid: + return 0 + if self.pos > len(self.line) - (wid - whalf): + return len(self.line) - wid + return self.pos - whalf + def draw(self): self.win.erase() if self.question_queue: assert isinstance(self.question_queue[0], tuple) assert len(self.question_queue[0]) == 3 - self.addstr(0, 0, self.question_queue[0][0]) + self.addstr(0, 0, self.question_queue[0][0][self.pos:]) return self.addstr(0, 0, self.prompt) line = WideString(self.line) - overflow = -self.wid + len(self.prompt) + len(line) + 1 - if overflow > 0: - self.addstr(0, len(self.prompt), str(line[overflow:])) - else: - self.addstr(0, len(self.prompt), self.line) + if line: + x = self._calculate_offset() + self.addstr(0, len(self.prompt), str(line[x:])) def finalize(self): move = self.fm.ui.win.move @@ -112,7 +119,8 @@ class Console(Widget): # pylint: disable=too-many-instance-attributes,too-many- pass else: try: - pos = uwid(self.line[0:self.pos]) + len(self.prompt) + x = self._calculate_offset() + pos = uwid(self.line[x:self.pos]) + len(self.prompt) move(self.y, self.x + min(self.wid - 1, pos)) except curses.error: pass @@ -202,7 +210,7 @@ class Console(Widget): # pylint: disable=too-many-instance-attributes,too-many- return if self.question_queue: - self.unicode_buffer, answer, self.pos = result + self.unicode_buffer, answer, _ = result self._answer_question(answer) else: self.unicode_buffer, self.line, self.pos = result @@ -273,22 +281,28 @@ class Console(Widget): # pylint: disable=too-many-instance-attributes,too-many- if direction.horizontal(): # Ensure that the pointer is moved utf-char-wise if self.fm.py3: + if self.question_queue: + umax = len(self.question_queue[0][0]) + 1 - self.wid + else: + umax = len(self.line) + 1 self.pos = direction.move( direction=direction.right(), minimum=0, - maximum=len(self.line) + 1, + maximum=umax, current=self.pos) else: - if self.fm.py3: - uchar = list(self.line) - upos = len(self.line[:self.pos]) + if self.question_queue: + uchar = list(self.question_queue[0][0].decode('utf-8', 'ignore')) + upos = len(self.question_queue[0][0][:self.pos].decode('utf-8', 'ignore')) + umax = len(uchar) + 1 - self.wid else: uchar = list(self.line.decode('utf-8', 'ignore')) upos = len(self.line[:self.pos].decode('utf-8', 'ignore')) + umax = len(uchar) + 1 newupos = direction.move( direction=direction.right(), minimum=0, - maximum=len(uchar) + 1, + maximum=umax, current=upos) self.pos = len(''.join(uchar[:newupos]).encode('utf-8', 'ignore')) diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py index eb2250ae..266d48ca 100644 --- a/ranger/gui/widgets/statusbar.py +++ b/ranger/gui/widgets/statusbar.py @@ -234,7 +234,7 @@ class StatusBar(Widget): # pylint: disable=too-many-instance-attributes except KeyError: return str(gid) - def _get_right_part(self, bar): # pylint: disable=too-many-branches + def _get_right_part(self, bar): # pylint: disable=too-many-branches,too-many-statements right = bar.right if self.column is None: return @@ -256,6 +256,10 @@ class StatusBar(Widget): # pylint: disable=too-many-instance-attributes right.add(str(self.fm.thisdir.flat), base, 'flat') right.add(", ", "space") + if self.fm.thisdir.narrow_filter: + right.add("narrowed") + right.add(", ", "space") + if self.fm.thisdir.filter: right.add("f=`", base, 'filter') right.add(self.fm.thisdir.filter.pattern, base, 'filter') @@ -298,6 +302,11 @@ class StatusBar(Widget): # pylint: disable=too-many-instance-attributes else: right.add('0/0 All', base, 'all') + if self.settings.freeze_files: + # Indicate that files are frozen and will not be loaded + right.add(" ", "space") + right.add('FROZEN', base, 'frozen') + def _print_result(self, result): self.win.move(0, 0) for part in result: diff --git a/ranger/gui/widgets/taskview.py b/ranger/gui/widgets/taskview.py index 2c0cafb0..a4633e15 100644 --- a/ranger/gui/widgets/taskview.py +++ b/ranger/gui/widgets/taskview.py @@ -84,7 +84,7 @@ class TaskView(Widget, Accumulator): if i is None: i = self.pointer - self.fm.loader.move(_from=i, to=to) + self.fm.loader.move(pos_src=i, pos_dest=to) def press(self, key): self.fm.ui.keymaps.use_keymap('taskview') diff --git a/ranger/gui/widgets/view_base.py b/ranger/gui/widgets/view_base.py index 5cdb2615..cb205d92 100644 --- a/ranger/gui/widgets/view_base.py +++ b/ranger/gui/widgets/view_base.py @@ -109,6 +109,7 @@ class ViewBase(Widget, DisplayableContainer): # pylint: disable=too-many-instan def _draw_hints(self): self.columns[-1].clear_image(force=True) + self.color_reset() self.need_clear = True hints = [] for key, value in self.fm.ui.keybuffer.pointer.items(): |