diff options
31 files changed, 325 insertions, 120 deletions
diff --git a/HACKING.md b/HACKING.md index 57ac90e6..6e200a43 100644 --- a/HACKING.md +++ b/HACKING.md @@ -50,7 +50,7 @@ Good places to read about ranger internals are: About the UI: * `ranger/gui/widgets/browsercolumn.py` -* `ranger/gui/widgets/browserview.py` +* `ranger/gui/widgets/view_miller.py` * `ranger/gui/ui.py` diff --git a/Makefile b/Makefile index 79b4f9d4..c6b3b35d 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,7 @@ endif SETUPOPTS ?= '--record=install_log.txt' DOCDIR ?= doc/pydoc DESTDIR ?= / +PREFIX ?= /usr/local PYOPTIMIZE ?= 1 FILTER ?= . @@ -55,7 +56,8 @@ help: install: $(PYTHON) setup.py install $(SETUPOPTS) \ - '--root=$(DESTDIR)' --optimize=$(PYOPTIMIZE) + '--prefix=$(PREFIX)' '--root=$(DESTDIR)' \ + --optimize=$(PYOPTIMIZE) compile: clean PYTHONOPTIMIZE=$(PYOPTIMIZE) $(PYTHON) -m compileall -q ranger @@ -96,7 +98,8 @@ test_flake8: test_doctest: @echo "Running doctests..." - @for FILE in $(shell grep -IHm 1 doctest -r ranger | grep $(FILTER) | cut -d: -f1); do \ + @set -e; \ + for FILE in $(shell grep -IHm 1 doctest -r ranger | grep $(FILTER) | cut -d: -f1); do \ echo "Testing $$FILE..."; \ RANGER_DOCTEST=1 PYTHONPATH=".:"$$PYTHONPATH ${PYTHON} $$FILE; \ done diff --git a/README.md b/README.md index d7ca0493..6ef246a8 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ ranger 1.9.2 ============ [![Build Status](https://travis-ci.org/ranger/ranger.svg?branch=master)](https://travis-ci.org/ranger/ranger) +<a href="https://repology.org/metapackage/ranger/versions"> + <img src="https://repology.org/badge/latest-versions/ranger.svg" alt="latest packaged version(s)"> +</a> ranger is a console file manager with VI key bindings. It provides a minimalistic and nice curses interface with a view on the directory hierarchy. @@ -11,11 +14,12 @@ out which program to use for what file type. ![screenshot](https://raw.githubusercontent.com/ranger/ranger-assets/master/screenshots/screenshot.png) This file describes ranger and how to get it to run. For instructions on the -usage, please read the man page. See `HACKING.md` for development specific -information. +usage, please read the man page (`man ranger` in a terminal). See `HACKING.md` +for development-specific information. For configuration, check the files in `ranger/config/` or copy the -default config to `~/.config/ranger` with `ranger --copy-config`. +default config to `~/.config/ranger` with `ranger --copy-config` +(see [instructions](#getting-started)). The `examples/` directory contains several scripts and plugins that demonstrate how ranger can be extended or combined with other programs. These files can be @@ -40,12 +44,12 @@ Design Goals * An easily maintainable file manager in a high level language * A quick way to switch directories and browse the file system * Keep it small but useful, do one thing and do it well -* Console based, with smooth integration into the unix shell +* Console-based, with smooth integration into the unix shell Features -------- -* UTF-8 Support (if your python copy supports it) +* UTF-8 Support (if your Python copy supports it) * Multi-column display * Preview of the selected file/directory * Common file operations (create/chmod/copy/delete/...) @@ -53,20 +57,20 @@ Features * VIM-like console and hotkeys * Automatically determine file types and run them with correct programs * Change the directory of your shell after exiting ranger -* Tabs, bookmarks, mouse support +* Tabs, bookmarks, mouse support... Dependencies ------------ * Python (`>=2.6` or `>=3.1`) with the `curses` module - and (optionally) wide-unicode support. + and (optionally) wide-unicode support * A pager (`less` by default) Optional: * The `file` program for determining file types -* The python module `chardet`, in case of encoding detection problems -* `sudo` to use the "run as root"-feature +* The Python module `chardet`, in case of encoding detection problems +* `sudo` to use the "run as root" feature * `w3m` for the `w3mimgdisplay` program to preview images * `python-bidi` for correct display of RTL file names (Hebrew, Arabic) @@ -76,8 +80,8 @@ Optional, for enhanced file previews (with `scope.sh`): * `highlight` or `pygmentize` for syntax highlighting of code * `atool`, `bsdtar` and/or `unrar` for previews of archives * `lynx`, `w3m` or `elinks` for previews of html pages -* `pdftotext` or `mutool` for pdf previews -* `transmission-show` for viewing bit-torrent information +* `pdftotext` or `mutool` for `pdf` previews +* `transmission-show` for viewing BitTorrent information * `mediainfo` or `exiftool` for viewing information about media files * `odt2txt` for OpenDocument text files (`odt`, `ods`, `odp` and `sxw`) * `chardet` (Python package) for improved encoding detection of text files @@ -86,7 +90,24 @@ Optional, for enhanced file previews (with `scope.sh`): Installing ---------- Use the package manager of your operating system to install ranger. -Note that ranger can be started without installing by simply running `ranger.py`. +You can also install ranger through PyPI: ```pip install ranger-fm```. + +<details> + <summary> + Check current version: + <sub> + <a href="https://repology.org/metapackage/ranger/versions"> + <img src="https://repology.org/badge/tiny-repos/ranger.svg" alt="Packaging status"> + </a> + </sub> + </summary> + <a href="https://repology.org/metapackage/ranger/versions"> + <img src="https://repology.org/badge/vertical-allrepos/ranger.svg" alt="Packaging status"> + </a> +</details> + +### Installing from a clone +Note that you don't *have* to install ranger; you can simply run `ranger.py`. To install ranger manually: ``` @@ -104,10 +125,10 @@ use to uninstall ranger. Getting Started --------------- -After starting ranger, you can use the Arrow Keys or `h` `j` `k` `l` to navigate, `Enter` -to open a file or type `Q` to quit. The third column shows a preview of the -current file. The second is the main column and the first shows the parent -directory. +After starting ranger, you can use the Arrow Keys or `h` `j` `k` `l` to +navigate, `Enter` to open a file or `q` to quit. The third column shows a +preview of the current file. The second is the main column and the first shows +the parent directory. Ranger can automatically copy default configuration files to `~/.config/ranger` if you run it with the switch `--copy-config=( rc | scope | ... | all )`. diff --git a/doc/ranger.1 b/doc/ranger.1 index 38926f87..43a13659 100644 --- a/doc/ranger.1 +++ b/doc/ranger.1 @@ -1,4 +1,4 @@ -.\" Automatically generated by Pod::Man 4.10 (Pod::Simple 3.35) +.\" Automatically generated by Pod::Man 4.09 (Pod::Simple 3.35) .\" .\" Standard preamble: .\" ======================================================================== @@ -54,20 +54,16 @@ .\" Avoid warning from groff about undefined register 'F'. .de IX .. -.nr rF 0 -.if \n(.g .if rF .nr rF 1 -.if (\n(rF:(\n(.g==0)) \{\ -. if \nF \{\ -. de IX -. tm Index:\\$1\t\\n%\t"\\$2" +.if !\nF .nr F 0 +.if \nF>0 \{\ +. de IX +. tm Index:\\$1\t\\n%\t"\\$2" .. -. if !\nF==2 \{\ -. nr % 0 -. nr F 2 -. \} +. if !\nF==2 \{\ +. nr % 0 +. nr F 2 . \} .\} -.rr rF .\" .\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2). .\" Fear. Run. Save yourself. No user-serviceable parts. @@ -173,7 +169,7 @@ plugins, sample configuration files and some programs for integrating ranger with other software. They are usually installed to \&\fI/usr/share/doc/ranger/examples\fR. .PP -The man page of \fBrifle\fR\|(1) describes the functions of the file opener +The man page of \fIrifle\fR\|(1) describes the functions of the file opener .PP The section \fI\s-1LINKS\s0\fR of this man page contains further resources. .SH "POSITIONAL ARGUMENTS" @@ -415,7 +411,7 @@ Note: The bookmarks ' (Apostrophe) and ` (Backtick) are the same. .SS "\s-1RIFLE\s0" .IX Subsection "RIFLE" Rifle is the file opener of ranger. It can be used as a standalone program or -a python module. It is located at \fIranger/ext/rifle.py\fR. In contrast to +a python module. It is located at \fI\f(CI$repo\fI/ranger/ext/rifle.py\fR. In contrast to other, more simple file openers, rifle can automatically find installed programs so it can be used effectively out of the box on a variety of systems. .PP @@ -427,14 +423,16 @@ by typing \*(L"<rulenumber><enter>\*(R". If you use rifle standalone, you can l rules with the \*(L"\-l\*(R" option and pick a rule with \*(L"\-p <number>\*(R". .PP The rules, along with further documentation, are contained in -\&\fIranger/config/rifle.conf\fR. +\&\fI\f(CI$repo\fI/ranger/config/rifle.conf\fR. .SS "\s-1FLAGS\s0" .IX Subsection "FLAGS" Flags give you a way to modify the behavior of the spawned process. They are used in the commands \f(CW\*(C`:open_with\*(C'\fR (key \*(L"r\*(R") and \f(CW\*(C`:shell\*(C'\fR (key \*(L"!\*(R"). .PP -.Vb 4 -\& f Fork the process. (Run in background) +.Vb 6 +\& f Fork the process, i.e. run in background. Please use this flag instead of +\& calling "disown" or "nohup", to avoid killing the background command when +\& pressing Ctrl+C in ranger. \& c Run the current file only, instead of the selection \& r Run application with root privilege (requires sudo) \& t Run application in a new terminal window @@ -945,6 +943,9 @@ Start line numbers from 1. Possible values are: .IX Item "open_all_images [bool]" Open all images in this directory when running certain image viewers like feh or sxiv? You can still open selected files by marking them. +.Sp +If there would be too many files for the system to handle, this option +will be temporarily disabled automatically. .IP "padding_right [bool]" 4 .IX Item "padding_right [bool]" When collapse_preview is on and there is no preview, should there remain a @@ -1090,7 +1091,7 @@ Enable this if key combinations with the Alt Key don't work for you. .IX Header "COMMANDS" You can enter the commands in the console which is opened by pressing \*(L":\*(R". .PP -You can always get a list of the currently existing commands by typing \*(L"2?\*(R" in +You can always get a list of the currently existing commands by typing \*(L"?c\*(R" in ranger. For your convenience, this is a list of the \*(L"public\*(R" commands including their parameters, excluding descriptions: .PP .Vb 10 @@ -1611,7 +1612,7 @@ by checking for this variable. .IP "\s-1RANGER_LOAD_DEFAULT_RC\s0" 8 .IX Item "RANGER_LOAD_DEFAULT_RC" If this variable is set to \s-1FALSE,\s0 ranger will not load the default rc.conf. -This can save time if you copied the whole rc.conf to ~/.config/ranger/ and +This can save time if you copied the whole rc.conf to \fI~/.config/ranger/\fR and don't need the default one at all. .IP "\s-1VISUAL\s0" 8 .IX Item "VISUAL" @@ -1675,7 +1676,7 @@ copy, run: .Ve .SH "SEE ALSO" .IX Header "SEE ALSO" -\&\fBrifle\fR\|(1) +\&\fIrifle\fR\|(1) .SH "BUGS" .IX Header "BUGS" Report bugs here: <https://github.com/ranger/ranger/issues> diff --git a/doc/ranger.pod b/doc/ranger.pod index b67ded7f..d182f3af 100644 --- a/doc/ranger.pod +++ b/doc/ranger.pod @@ -320,7 +320,7 @@ Note: The bookmarks ' (Apostrophe) and ` (Backtick) are the same. =head2 RIFLE Rifle is the file opener of ranger. It can be used as a standalone program or -a python module. It is located at F<ranger/ext/rifle.py>. In contrast to +a python module. It is located at F<$repo/ranger/ext/rifle.py>. In contrast to other, more simple file openers, rifle can automatically find installed programs so it can be used effectively out of the box on a variety of systems. @@ -332,14 +332,16 @@ by typing "<rulenumber><enter>". If you use rifle standalone, you can list all rules with the "-l" option and pick a rule with "-p <number>". The rules, along with further documentation, are contained in -F<ranger/config/rifle.conf>. +F<$repo/ranger/config/rifle.conf>. =head2 FLAGS Flags give you a way to modify the behavior of the spawned process. They are used in the commands C<:open_with> (key "r") and C<:shell> (key "!"). - f Fork the process. (Run in background) + f Fork the process, i.e. run in background. Please use this flag instead of + calling "disown" or "nohup", to avoid killing the background command when + pressing Ctrl+C in ranger. c Run the current file only, instead of the selection r Run application with root privilege (requires sudo) t Run application in a new terminal window @@ -967,6 +969,9 @@ Start line numbers from 1. Possible values are: Open all images in this directory when running certain image viewers like feh or sxiv? You can still open selected files by marking them. +If there would be too many files for the system to handle, this option +will be temporarily disabled automatically. + =item padding_right [bool] When collapse_preview is on and there is no preview, should there remain a @@ -1149,7 +1154,7 @@ Enable this if key combinations with the Alt Key don't work for you. You can enter the commands in the console which is opened by pressing ":". -You can always get a list of the currently existing commands by typing "2?" in +You can always get a list of the currently existing commands by typing "?c" in ranger. For your convenience, this is a list of the "public" commands including their parameters, excluding descriptions: alias [newcommand] [oldcommand] @@ -1753,7 +1758,7 @@ by checking for this variable. =item RANGER_LOAD_DEFAULT_RC If this variable is set to FALSE, ranger will not load the default rc.conf. -This can save time if you copied the whole rc.conf to ~/.config/ranger/ and +This can save time if you copied the whole rc.conf to I<~/.config/ranger/> and don't need the default one at all. =item VISUAL diff --git a/doc/rifle.1 b/doc/rifle.1 index 9ed1a145..a42734d2 100644 --- a/doc/rifle.1 +++ b/doc/rifle.1 @@ -1,4 +1,4 @@ -.\" Automatically generated by Pod::Man 4.10 (Pod::Simple 3.35) +.\" Automatically generated by Pod::Man 4.09 (Pod::Simple 3.35) .\" .\" Standard preamble: .\" ======================================================================== @@ -54,20 +54,16 @@ .\" Avoid warning from groff about undefined register 'F'. .de IX .. -.nr rF 0 -.if \n(.g .if rF .nr rF 1 -.if (\n(rF:(\n(.g==0)) \{\ -. if \nF \{\ -. de IX -. tm Index:\\$1\t\\n%\t"\\$2" +.if !\nF .nr F 0 +.if \nF>0 \{\ +. de IX +. tm Index:\\$1\t\\n%\t"\\$2" .. -. if !\nF==2 \{\ -. nr % 0 -. nr F 2 -. \} +. if !\nF==2 \{\ +. nr % 0 +. nr F 2 . \} .\} -.rr rF .\" .\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2). .\" Fear. Run. Save yourself. No user-serviceable parts. @@ -133,7 +129,7 @@ .\" ======================================================================== .\" .IX Title "RIFLE 1" -.TH RIFLE 1 "rifle-1.9.2" "2018-09-09" "rifle manual" +.TH RIFLE 1 "rifle-1.9.2" "2019-04-03" "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/ranger/api/commands.py b/ranger/api/commands.py index 9c687927..90a79488 100644 --- a/ranger/api/commands.py +++ b/ranger/api/commands.py @@ -448,4 +448,5 @@ def command_function_factory(func): if __name__ == '__main__': import doctest - doctest.testmod() + import sys + sys.exit(doctest.testmod()[0]) diff --git a/ranger/config/commands.py b/ranger/config/commands.py index d177203a..36a6070e 100755 --- a/ranger/config/commands.py +++ b/ranger/config/commands.py @@ -996,7 +996,7 @@ class rename_append(Command): relpath = tfile.relative_path.replace(MACRO_DELIMITER, MACRO_DELIMITER_ESC) basename = tfile.basename.replace(MACRO_DELIMITER, MACRO_DELIMITER_ESC) - if basename.find('.') <= 0: + if basename.find('.') <= 0 or os.path.isdir(relpath): self.fm.open_console('rename ' + relpath) return @@ -1639,6 +1639,17 @@ class flat(Command): self.fm.thisdir.flat = level self.fm.thisdir.load_content() + +class reset_previews(Command): + """:reset_previews + + Reset the file previews. + """ + def execute(self): + self.fm.previews = {} + self.fm.ui.need_redraw = True + + # Version control commands # -------------------------------- diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf index 8913213c..f559290d 100644 --- a/ranger/config/rc.conf +++ b/ranger/config/rc.conf @@ -478,6 +478,9 @@ map pl paste_symlink relative=False map pL paste_symlink relative=True map phl paste_hardlink map pht paste_hardlinked_subtree +map pd console paste dest= +map p`<any> paste dest=%any_path +map p'<any> paste dest=%any_path map dD console delete diff --git a/ranger/config/rifle.conf b/ranger/config/rifle.conf index babdcda7..fb9b70f0 100644 --- a/ranger/config/rifle.conf +++ b/ranger/config/rifle.conf @@ -98,7 +98,7 @@ ext exe = wine "$1" name ^[mM]akefile$ = make #-------------------------------------------- -# Code +# Scripts #------------------------------------------- ext py = python -- "$1" ext pl = perl -- "$1" @@ -131,7 +131,7 @@ mime ^video|audio, has totem, X, flag f = totem -- "$@" mime ^video|audio, has totem, X, flag f = totem --fullscreen -- "$@" #-------------------------------------------- -# Video without X: +# Video without X #------------------------------------------- mime ^video, terminal, !X, has mpv = mpv -- "$@" mime ^video, terminal, !X, has mplayer2 = mplayer2 -- "$@" @@ -170,11 +170,12 @@ ext epub, has ebook-viewer, X, flag f = ebook-viewer -- "$@" ext mobi, has ebook-viewer, X, flag f = ebook-viewer -- "$@" #------------------------------------------- -# Image Viewing: +# Images #------------------------------------------- mime ^image/svg, has inkscape, X, flag f = inkscape -- "$@" mime ^image/svg, has display, X, flag f = display -- "$@" +mime ^image, has imv, X, flag f = imv -- "$@" mime ^image, has pqiv, X, flag f = pqiv -- "$@" mime ^image, has sxiv, X, flag f = sxiv -- "$@" mime ^image, has feh, X, flag f = feh -- "$@" @@ -212,6 +213,11 @@ ext rar, has unrar = unrar l "$1" | less ext rar, has unrar = for file in "$@"; do unrar x "$file"; done #------------------------------------------- +# Fonts +#------------------------------------------- +mime ^font, has fontforge, X, flag f = fontforge "$@" + +#------------------------------------------- # Flag t fallback terminals #------------------------------------------- # Rarely installed terminal emulators get higher priority; It is assumed that diff --git a/ranger/container/bookmarks.py b/ranger/container/bookmarks.py index 59838c00..7b5e56b7 100644 --- a/ranger/container/bookmarks.py +++ b/ranger/container/bookmarks.py @@ -88,7 +88,11 @@ class Bookmarks(FileManagerAware): if key == '`': key = "'" if key in self.dct: - return self.dct[key] + value = self.dct[key] + if self._validate(value): + return value + else: + raise KeyError("Cannot open bookmark: `%s'!" % key) else: raise KeyError("Nonexistant Bookmark: `%s'!" % key) @@ -185,7 +189,13 @@ class Bookmarks(FileManagerAware): old_perms = os.stat(self.path) os.chown(path_new, old_perms.st_uid, old_perms.st_gid) os.chmod(path_new, old_perms.st_mode) - os.rename(path_new, self.path) + + if os.path.islink(self.path): + target_path = os.path.realpath(self.path) + os.rename(path_new, target_path) + else: + os.rename(path_new, self.path) + except OSError as ex: self.fm.notify('Bookmarks error: {0}'.format(str(ex)), bad=True) return @@ -223,7 +233,7 @@ class Bookmarks(FileManagerAware): for line in fobj: if self.load_pattern.match(line): key, value = line[0], line[2:-1] - if key in ALLOWED_KEYS and not os.path.isfile(value): + if key in ALLOWED_KEYS: dct[key] = self.bookmarktype(value) fobj.close() return dct @@ -247,3 +257,6 @@ class Bookmarks(FileManagerAware): def _update_mtime(self): self.last_mtime = self._get_mtime() + + def _validate(self, value): # pylint: disable=no-self-use + return os.path.isdir(str(value)) diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py index a82532c9..4fb47354 100644 --- a/ranger/container/fsobject.py +++ b/ranger/container/fsobject.py @@ -29,11 +29,13 @@ except AttributeError: CONTAINER_EXTENSIONS = ('7z', 'ace', 'ar', 'arc', 'bz', 'bz2', 'cab', 'cpio', - 'cpt', 'deb', 'dgc', 'dmg', 'gz', 'iso', 'jar', 'msi', 'pkg', 'rar', - 'shar', 'tar', 'tbz', 'tgz', 'xar', 'xpi', 'xz', 'zip') -DOCUMENT_EXTENSIONS = ('cfg', 'css', 'cvs', 'djvu', 'doc', 'docx', 'gnm', - 'gnumeric', 'htm', 'html', 'md', 'odf', 'odg', 'odp', 'ods', 'odt', 'pdf', - 'pod', 'ps', 'rtf', 'sxc', 'txt', 'xls', 'xlw', 'xml', 'xslx') + 'cpt', 'deb', 'dgc', 'dmg', 'gz', 'iso', 'jar', 'msi', + 'pkg', 'rar', 'shar', 'tar', 'tbz', 'tgz', 'txz', + 'xar', 'xpi', 'xz', 'zip') +DOCUMENT_EXTENSIONS = ('cbr', 'cbz', 'cfg', 'css', 'cvs', 'djvu', 'doc', + 'docx', 'gnm', 'gnumeric', 'htm', 'html', 'md', 'odf', + 'odg', 'odp', 'ods', 'odt', 'pdf', 'pod', 'ps', 'rtf', + 'sxc', 'txt', 'xls', 'xlw', 'xml', 'xslx') DOCUMENT_BASENAMES = ('bugs', 'bugs', 'changelog', 'copying', 'credits', 'hacking', 'help', 'install', 'license', 'readme', 'todo') diff --git a/ranger/core/actions.py b/ranger/core/actions.py index 6cf376eb..e7be0c65 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -247,8 +247,6 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m try: val = self.fm.bookmarks[key_to_string(char)] except KeyError: - self.notify('No bookmark defined for `{}`'.format( - key_to_string(char)), bad=True) val = MACRO_FAIL return ('any_path{:d}'.format(i), val) @@ -412,7 +410,7 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m raise self.notify('Error in line `%s\':\n %s' % (line, str(ex)), bad=True) - def execute_file(self, files, **kw): + def execute_file(self, files, **kw): # pylint: disable=too-many-branches """Uses the "rifle" module to open/execute a file Arguments are the same as for ranger.ext.rifle.Rifle.execute: @@ -459,8 +457,23 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m self.signal_emit('execute.before', keywords=kw) filenames = [f.path for f in files] label = kw.get('label', kw.get('app', None)) - try: + + def execute(): return self.rifle.execute(filenames, mode, label, flags, None) + try: + return execute() + except OSError as err: + # Argument list too long. + if err.errno == 7 and self.settings.open_all_images: + old_value = self.settings.open_all_images + try: + self.notify("Too many files: Disabling open_all_images temporarily.") + self.settings.open_all_images = False + return execute() + finally: + self.settings.open_all_images = old_value + else: + raise finally: self.signal_emit('execute.after') @@ -508,6 +521,8 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m selection = self.thistab.get_selection() else: selection = [tfile] + if tfile is None: + return if tfile.is_directory: self.thistab.enter_dir(tfile) elif selection: @@ -857,10 +872,16 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m # Tags are saved in ~/.config/ranger/tagged and simply mark if a # file is important to you in any context. - def tag_toggle(self, paths=None, value=None, movedown=None, tag=None): + def tag_toggle(self, tag=None, paths=None, value=None, movedown=None): """:tag_toggle <character> Toggle a tag <character>. + + Keyword arguments: + tag=<character> + paths=<paths to tag> + value=<True: add/False: remove/anything else: toggle> + movedown=<boolean> """ if not self.tags: return @@ -882,11 +903,19 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m self.ui.redraw_main_column() - def tag_remove(self, paths=None, movedown=None, tag=None): - self.tag_toggle(paths=paths, value=False, movedown=movedown, tag=tag) + def tag_remove(self, tag=None, paths=None, movedown=None): + """:tag_remove <character> - def tag_add(self, paths=None, movedown=None, tag=None): - self.tag_toggle(paths=paths, value=True, movedown=movedown, tag=tag) + Remove a tag <character>. See :tag_toggle for keyword arguments. + """ + self.tag_toggle(tag=tag, paths=paths, value=False, movedown=movedown) + + def tag_add(self, tag=None, paths=None, movedown=None): + """:tag_add <character> + + Add a tag <character>. See :tag_toggle for keyword arguments. + """ + self.tag_toggle(tag=tag, paths=paths, value=True, movedown=movedown) # -------------------------- # -- Bookmarks @@ -1088,6 +1117,12 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m data['loading'] = False return path + if ranger.args.clean: + # Don't access args.cachedir in clean mode + return None + + if not os.path.exists(ranger.args.cachedir): + os.makedirs(ranger.args.cachedir) cacheimg = os.path.join(ranger.args.cachedir, self.sha1_encode(path)) if self.settings.preview_images and \ os.path.isfile(cacheimg) and \ @@ -1555,14 +1590,19 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m link(source_path, next_available_filename(target_path)) - def paste(self, overwrite=False, append=False): + def paste(self, overwrite=False, append=False, dest=None): """:paste - Paste the selected items into the current directory. + Paste the selected items into the current directory or to dest + if provided. """ - loadable = CopyLoader(self.copy_buffer, self.do_cut, overwrite) - self.loader.add(loadable, append=append) - self.do_cut = False + if dest is None or isdir(dest): + loadable = CopyLoader(self.copy_buffer, self.do_cut, overwrite, + dest) + self.loader.add(loadable, append=append) + self.do_cut = False + else: + self.notify('Failed to paste. The given path is invalid.', bad=True) def delete(self, files=None): # XXX: warn when deleting mount points/unseen marked files? diff --git a/ranger/core/loader.py b/ranger/core/loader.py index 9f32535f..96d000ac 100644 --- a/ranger/core/loader.py +++ b/ranger/core/loader.py @@ -51,11 +51,11 @@ class Loadable(object): class CopyLoader(Loadable, FileManagerAware): # pylint: disable=too-many-instance-attributes progressbar_supported = True - def __init__(self, copy_buffer, do_cut=False, overwrite=False): + def __init__(self, copy_buffer, do_cut=False, overwrite=False, dest=None): self.copy_buffer = tuple(copy_buffer) self.do_cut = do_cut self.original_copy_buffer = copy_buffer - self.original_path = self.fm.thistab.path + self.original_path = dest if dest is not None else self.fm.thistab.path self.overwrite = overwrite self.percent = 0 if self.copy_buffer: @@ -102,7 +102,7 @@ class CopyLoader(Loadable, FileManagerAware): # pylint: disable=too-many-instan tag = self.fm.tags.tags[path] self.fm.tags.remove(path) self.fm.tags.tags[ - path.replace(fobj.path, self.original_path + '/' + fobj.basename) + path.replace(fobj.path, path.join(self.original_path, fobj.basename)) ] = tag self.fm.tags.dump() n = 0 diff --git a/ranger/core/main.py b/ranger/core/main.py index 598ce243..23648677 100644 --- a/ranger/core/main.py +++ b/ranger/core/main.py @@ -105,7 +105,8 @@ def main( if not os.access(path_abs, os.F_OK): paths_inaccessible += [path] if paths_inaccessible: - print('Inaccessible paths: {0}'.format(paths), file=sys.stderr) + print('Inaccessible paths: {0}'.format(', '.join(paths_inaccessible)), + file=sys.stderr) return 1 profile = None @@ -145,11 +146,6 @@ def main( from ranger.ext import curses_interrupt_handler curses_interrupt_handler.install_interrupt_handler() - # Create cache directory - if fm.settings.preview_images and fm.settings.use_preview_script: - if not os.path.exists(args.cachedir): - os.makedirs(args.cachedir) - if not args.clean: # Create data directory if not os.path.exists(args.datadir): @@ -326,9 +322,14 @@ def parse_arguments(): sys.exit(1) return path - args.cachedir = path_init('cachedir') - args.confdir = path_init('confdir') - args.datadir = path_init('datadir') + if args.clean: + args.cachedir = None + args.confdir = None + args.datadir = None + else: + args.cachedir = path_init('cachedir') + args.confdir = path_init('confdir') + args.datadir = path_init('datadir') if args.choosefile: args.choosefile = path_init('choosefile') if args.choosefiles: @@ -407,8 +408,15 @@ def load_settings( # pylint: disable=too-many-locals,too-many-branches,too-many except OSError: LOG.debug('Unable to access plugin directory: %s', plugindir) else: - plugins = [p[:-3] for p in plugin_files - if p.endswith('.py') and not p.startswith('_')] + plugins = [] + for path in plugin_files: + if not path.startswith('_'): + if path.endswith('.py'): + # remove trailing '.py' + plugins.append(path[:-3]) + elif os.path.isdir(os.path.join(plugindir, path)): + plugins.append(path) + if not os.path.exists(fm.confpath('plugins', '__init__.py')): LOG.debug("Creating missing '__init__.py' file in plugin folder") fobj = open(fm.confpath('plugins', '__init__.py'), 'w') diff --git a/ranger/data/scope.sh b/ranger/data/scope.sh index 8e0a0f6d..33666fcb 100755 --- a/ranger/data/scope.sh +++ b/ranger/data/scope.sh @@ -31,7 +31,7 @@ IMAGE_CACHE_PATH="${4}" # Full path that should be used to cache image preview PV_IMAGE_ENABLED="${5}" # 'True' if image previews are enabled, 'False' otherwise. FILE_EXTENSION="${FILE_PATH##*.}" -FILE_EXTENSION_LOWER=$(echo ${FILE_EXTENSION} | tr '[:upper:]' '[:lower:]') +FILE_EXTENSION_LOWER="$(printf "%s" "${FILE_EXTENSION}" | tr '[:upper:]' '[:lower:]')" # Settings HIGHLIGHT_SIZE_MAX=262143 # 256KiB @@ -92,13 +92,24 @@ handle_extension() { } handle_image() { + # Size of the preview if there are multiple options or it has to be rendered + # from vector graphics. If the conversion program allows specifying only one + # dimension while keeping the aspect ratio, the width will be used. + local DEFAULT_SIZE="1920x1080" + local mimetype="${1}" case "${mimetype}" in # SVG # image/svg+xml) - # convert "${FILE_PATH}" "${IMAGE_CACHE_PATH}" && exit 6 + # convert -- "${FILE_PATH}" "${IMAGE_CACHE_PATH}" && exit 6 # exit 1;; + # DjVu + # image/vnd.djvu) + # ddjvu -format=tiff -quality=90 -page=1 -size="${DEFAULT_SIZE}" \ + # - "${IMAGE_CACHE_PATH}" < "${FILE_PATH}" \ + # && exit 6 || exit 1;; + # Image image/*) local orientation @@ -110,7 +121,7 @@ handle_image() { convert -- "${FILE_PATH}" -auto-orient "${IMAGE_CACHE_PATH}" && exit 6 fi - # `w3mimgdisplay` will be called for all images (unless overriden as above), + # `w3mimgdisplay` will be called for all images (unless overridden as above), # but might fail for unsupported types. exit 7;; @@ -119,16 +130,49 @@ 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-x "${DEFAULT_SIZE%x*}" \ # -scale-to-y -1 \ # -singlefile \ # -jpeg -tiffcompression jpeg \ # -- "${FILE_PATH}" "${IMAGE_CACHE_PATH%.*}" \ # && exit 6 || exit 1;; + # ePub, MOBI, FB2 (using Calibre) + # application/epub+zip|application/x-mobipocket-ebook|application/x-fictionbook+xml) + # ebook-meta --get-cover="${IMAGE_CACHE_PATH}" -- "${FILE_PATH}" > /dev/null \ + # && exit 6 || exit 1;; + + # ePub (using <https://github.com/marianosimone/epub-thumbnailer>) + # application/epub+zip) + # epub-thumbnailer \ + # "${FILE_PATH}" "${IMAGE_CACHE_PATH}" "${DEFAULT_SIZE%x*}" \ + # && exit 6 || exit 1;; + + # Font + application/font*|application/*opentype) + preview_png="/tmp/$(basename "${IMAGE_CACHE_PATH%.*}").png" + if fontimage -o "${preview_png}" \ + --pixelsize "120" \ + --fontname \ + --pixelsize "80" \ + --text " ABCDEFGHIJKLMNOPQRSTUVWXYZ " \ + --text " abcdefghijklmnopqrstuvwxyz " \ + --text " 0123456789.:,;(*!?') ff fl fi ffi ffl " \ + --text " The quick brown fox jumps over the lazy dog. " \ + "${FILE_PATH}"; + then + convert -- "${preview_png}" "${IMAGE_CACHE_PATH}" \ + && rm "${preview_png}" \ + && exit 6 + else + exit 1 + fi + ;; + # Preview archives using the first image inside. # (Very useful for comic book collections for example.) # application/zip|application/x-rar|application/x-7z-compressed|\ @@ -189,6 +233,13 @@ handle_mime() { # pygmentize -f "${pygmentize_format}" -O "style=${PYGMENTIZE_STYLE}" -- "${FILE_PATH}" && exit 5 exit 2;; + # DjVu + image/vnd.djvu) + # Preview as text conversion (requires djvulibre) + djvutxt "${FILE_PATH}" | fmt -w ${PV_WIDTH} && exit 5 + exiftool "${FILE_PATH}" && exit 5 + exit 1;; + # Image image/*) # Preview as text conversion diff --git a/ranger/ext/direction.py b/ranger/ext/direction.py index 33ebb604..32b69954 100644 --- a/ranger/ext/direction.py +++ b/ranger/ext/direction.py @@ -174,4 +174,5 @@ class Direction(dict): if __name__ == '__main__': import doctest - doctest.testmod() + import sys + sys.exit(doctest.testmod()[0]) diff --git a/ranger/ext/human_readable.py b/ranger/ext/human_readable.py index 2a5bc81b..44f23079 100644 --- a/ranger/ext/human_readable.py +++ b/ranger/ext/human_readable.py @@ -71,5 +71,13 @@ def human_readable_time(timestamp): if __name__ == '__main__': + + # XXX: This mock class is a temporary (as of 2019-01-27) hack. + class SettingsAwareMock(object): # pylint: disable=too-few-public-methods + class settings(object): # pylint: disable=invalid-name,too-few-public-methods + size_in_bytes = False + SettingsAware = SettingsAwareMock # noqa: F811 + import doctest - doctest.testmod() + import sys + sys.exit(doctest.testmod()[0]) diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py index 9c84ce5e..7c9f35a7 100644 --- a/ranger/ext/img_display.py +++ b/ranger/ext/img_display.py @@ -484,7 +484,7 @@ class URXVTImageFSDisplayer(URXVTImageDisplayer): return self._get_centered_offsets() -class KittyImageDisplayer(ImageDisplayer): +class KittyImageDisplayer(ImageDisplayer, FileManagerAware): """Implementation of ImageDisplayer for kitty (https://github.com/kovidgoyal/kitty/) terminal. It uses the built APC to send commands and data to kitty, which in turn renders the image. The APC takes the form @@ -649,6 +649,8 @@ class KittyImageDisplayer(ImageDisplayer): # kitty doesn't seem to reply on deletes, checking like we do in draw() # will slows down scrolling with timeouts from select self.image_id -= 1 + self.fm.ui.win.redrawwin() + self.fm.ui.win.refresh() def _format_cmd_str(self, cmd, payload=None, max_slice_len=2048): central_blk = ','.join(["{}={}".format(k, v) for k, v in cmd.items()]).encode('ascii') diff --git a/ranger/ext/iter_tools.py b/ranger/ext/iter_tools.py index f321aee0..c710fbba 100644 --- a/ranger/ext/iter_tools.py +++ b/ranger/ext/iter_tools.py @@ -47,4 +47,5 @@ def unique(iterable): if __name__ == '__main__': import doctest - doctest.testmod() + import sys + sys.exit(doctest.testmod()[0]) diff --git a/ranger/ext/keybinding_parser.py b/ranger/ext/keybinding_parser.py index e5fb6a86..8703d458 100644 --- a/ranger/ext/keybinding_parser.py +++ b/ranger/ext/keybinding_parser.py @@ -278,4 +278,4 @@ class KeyBuffer(object): # pylint: disable=too-many-instance-attributes if __name__ == '__main__': import doctest - doctest.testmod() + sys.exit(doctest.testmod()[0]) diff --git a/ranger/ext/lazy_property.py b/ranger/ext/lazy_property.py index bb54bd5e..42b61979 100644 --- a/ranger/ext/lazy_property.py +++ b/ranger/ext/lazy_property.py @@ -59,4 +59,5 @@ class lazy_property(object): # pylint: disable=invalid-name,too-few-public-meth if __name__ == '__main__': import doctest - doctest.testmod() + import sys + sys.exit(doctest.testmod()[0]) diff --git a/ranger/ext/rifle.py b/ranger/ext/rifle.py index 377f9b8a..ad7d0049 100755 --- a/ranger/ext/rifle.py +++ b/ranger/ext/rifle.py @@ -534,6 +534,6 @@ def main(): # pylint: disable=too-many-locals if __name__ == '__main__': if 'RANGER_DOCTEST' in os.environ: import doctest - doctest.testmod() + sys.exit(doctest.testmod()[0]) else: main() diff --git a/ranger/ext/signals.py b/ranger/ext/signals.py index 67c8960d..0973249c 100644 --- a/ranger/ext/signals.py +++ b/ranger/ext/signals.py @@ -278,4 +278,5 @@ class SignalDispatcher(object): if __name__ == '__main__': import doctest - doctest.testmod() + import sys + sys.exit(doctest.testmod()[0]) diff --git a/ranger/ext/vcs/svn.py b/ranger/ext/vcs/svn.py index ef958a24..27216a43 100644 --- a/ranger/ext/vcs/svn.py +++ b/ranger/ext/vcs/svn.py @@ -98,6 +98,7 @@ class SVN(Vcs): # Paths with status lines = self._run(['status']).split('\n') + lines = list(filter(None, lines)) if not lines: return 'sync' for line in lines: @@ -116,6 +117,7 @@ class SVN(Vcs): # Paths with status lines = self._run(['status']).split('\n') + lines = list(filter(None, lines)) for line in lines: code, path = line[0], line[8:] if code == ' ': diff --git a/ranger/ext/widestring.py b/ranger/ext/widestring.py index 2721643c..390da639 100644 --- a/ranger/ext/widestring.py +++ b/ranger/ext/widestring.py @@ -164,4 +164,4 @@ class WideString(object): # pylint: disable=too-few-public-methods if __name__ == '__main__': import doctest - doctest.testmod() + sys.exit(doctest.testmod()[0]) diff --git a/ranger/gui/ansi.py b/ranger/gui/ansi.py index ff8b2fd9..ce735317 100644 --- a/ranger/gui/ansi.py +++ b/ranger/gui/ansi.py @@ -175,4 +175,5 @@ def char_slice(ansi_text, start, length): if __name__ == '__main__': import doctest - doctest.testmod() + import sys + sys.exit(doctest.testmod()[0]) diff --git a/ranger/gui/curses_shortcuts.py b/ranger/gui/curses_shortcuts.py index 14f1e0e4..fcd2be30 100644 --- a/ranger/gui/curses_shortcuts.py +++ b/ranger/gui/curses_shortcuts.py @@ -43,7 +43,7 @@ class CursesShortcuts(SettingsAware): try: self.win.addstr(*_fix_surrogates(args)) - except (curses.error, UnicodeError): + except (curses.error, UnicodeError, ValueError, TypeError): pass def addnstr(self, *args): @@ -51,13 +51,13 @@ class CursesShortcuts(SettingsAware): try: self.win.addnstr(*args) - except (curses.error, TypeError): + except (curses.error, TypeError, ValueError, TypeError): if len(args) > 2: self.win.move(y, x) try: self.win.addnstr(*_fix_surrogates(args)) - except (curses.error, UnicodeError): + except (curses.error, UnicodeError, ValueError, TypeError): pass def addch(self, *args): diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py index 13201e34..d796058c 100644 --- a/ranger/gui/widgets/console.py +++ b/ranger/gui/widgets/console.py @@ -530,4 +530,5 @@ class Console(Widget): # pylint: disable=too-many-instance-attributes,too-many- if __name__ == '__main__': import doctest - doctest.testmod() + import sys + sys.exit(doctest.testmod()[0]) diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py index 3457955e..71064ed4 100644 --- a/ranger/gui/widgets/statusbar.py +++ b/ranger/gui/widgets/statusbar.py @@ -183,7 +183,11 @@ class StatusBar(Widget): # pylint: disable=too-many-instance-attributes left.add(target.infostring.replace(" ", "")) left.add_space() - left.add(strftime(self.timeformat, localtime(stat.st_mtime)), 'mtime') + try: + date = strftime(self.timeformat, localtime(stat.st_mtime)) + except OSError: + date = '?' + left.add(date, 'mtime') directory = target if target.is_directory else \ target.fm.get_directory(os.path.dirname(target.path)) @@ -277,7 +281,7 @@ class StatusBar(Widget): # pylint: disable=too-many-instance-attributes right.add(human_readable(target.disk_usage, separator='') + " sum") if self.settings.display_free_space_in_status_bar: try: - free = get_free_space(target.mount_path) + free = get_free_space(target.path) except OSError: pass else: diff --git a/tests/ranger/container/test_bookmarks.py b/tests/ranger/container/test_bookmarks.py index 6fba2a3d..84ac42c2 100644 --- a/tests/ranger/container/test_bookmarks.py +++ b/tests/ranger/container/test_bookmarks.py @@ -8,11 +8,16 @@ import pytest from ranger.container.bookmarks import Bookmarks +class NotValidatedBookmarks(Bookmarks): + def _validate(self, value): + return True + + def testbookmarks(tmpdir): # Bookmarks point to directory location and allow fast access to # 'favorite' directories. They are persisted to a bookmark file, plain text. bookmarkfile = tmpdir.join("bookmarkfile") - bmstore = Bookmarks(str(bookmarkfile)) + bmstore = NotValidatedBookmarks(str(bookmarkfile)) # loading an empty bookmark file doesnot crash bmstore.load() @@ -33,7 +38,7 @@ def testbookmarks(tmpdir): # We can persist bookmarks to disk and restore them from disk bmstore.save() - secondstore = Bookmarks(str(bookmarkfile)) + secondstore = NotValidatedBookmarks(str(bookmarkfile)) secondstore.load() assert "'" in secondstore assert secondstore["'"] == "the milk" @@ -56,3 +61,20 @@ def testbookmarks(tmpdir): secondstore.update_if_outdated() secondstore.update = origupdate secondstore.update_if_outdated() + + +def test_bookmark_symlink(tmpdir): + # Initialize plain file and symlink paths + bookmarkfile_link = tmpdir.join("bookmarkfile") + bookmarkfile_orig = tmpdir.join("bookmarkfile.orig") + + # Create symlink pointing towards the original plain file. + os.symlink(str(bookmarkfile_orig), str(bookmarkfile_link)) + + # Initialize the bookmark file and save the file. + bmstore = Bookmarks(str(bookmarkfile_link)) + bmstore.save() + + # Once saved, the bookmark file should still be a symlink pointing towards the plain file. + assert os.path.islink(str(bookmarkfile_link)) + assert not os.path.islink(str(bookmarkfile_orig)) |