diff options
author | Wojciech Siewierski <wojciech.siewierski@onet.pl> | 2019-12-28 01:07:49 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-12-28 01:07:49 +0100 |
commit | 814ba418732267d13bdbe01342d14b1c85171133 (patch) | |
tree | fc476dc0e5ebda3b09caa9ecd859f973ef3e7b49 /ranger | |
parent | 3417dc99a39f860b2a91fcc43e165c7b1707fb7a (diff) | |
parent | 45f58365d6b0ef475407571c77cb98cd7ae1717e (diff) | |
download | ranger-814ba418732267d13bdbe01342d14b1c85171133.tar.gz |
Merge branch 'master' into chain-macros
Diffstat (limited to 'ranger')
-rwxr-xr-x | ranger/config/commands.py | 150 | ||||
-rw-r--r-- | ranger/config/rc.conf | 12 | ||||
-rw-r--r-- | ranger/config/rifle.conf | 26 | ||||
-rw-r--r-- | ranger/container/fsobject.py | 6 | ||||
-rw-r--r-- | ranger/container/settings.py | 2 | ||||
-rw-r--r-- | ranger/core/actions.py | 15 | ||||
-rw-r--r-- | ranger/core/fm.py | 25 | ||||
-rw-r--r-- | ranger/core/loader.py | 12 | ||||
-rw-r--r-- | ranger/core/main.py | 3 | ||||
-rw-r--r-- | ranger/core/runner.py | 4 | ||||
-rw-r--r-- | ranger/data/mime.types | 27 | ||||
-rwxr-xr-x | ranger/data/scope.sh | 11 | ||||
-rw-r--r-- | ranger/ext/img_display.py | 37 | ||||
-rwxr-xr-x | ranger/ext/rifle.py | 7 | ||||
-rw-r--r-- | ranger/ext/safe_path.py | 22 | ||||
-rw-r--r-- | ranger/ext/shutil_generatorized.py | 42 | ||||
-rw-r--r-- | ranger/gui/displayable.py | 6 | ||||
-rw-r--r-- | ranger/gui/ui.py | 25 | ||||
-rw-r--r-- | ranger/gui/widgets/browsercolumn.py | 3 | ||||
-rw-r--r-- | ranger/gui/widgets/pager.py | 17 | ||||
-rw-r--r-- | ranger/gui/widgets/statusbar.py | 6 |
21 files changed, 350 insertions, 108 deletions
diff --git a/ranger/config/commands.py b/ranger/config/commands.py index b7f48eb2..17a0fa9c 100755 --- a/ranger/config/commands.py +++ b/ranger/config/commands.py @@ -243,8 +243,9 @@ class cd(Command): paths = self._tab_fuzzy_match(basepath, tokens) if not os.path.isabs(dest): - paths_rel = basepath - paths = [os.path.relpath(path, paths_rel) for path in paths] + paths_rel = self.fm.thisdir.path + paths = [os.path.relpath(os.path.join(basepath, path), paths_rel) + for path in paths] else: paths_rel = '' return paths, paths_rel @@ -700,6 +701,64 @@ class delete(Command): self.fm.delete(files) +class trash(Command): + """:trash + + Tries to move the selection or the files passed in arguments (if any) to + the trash, using rifle rules with label "trash". + The arguments use a shell-like escaping. + + "Selection" is defined as all the "marked files" (by default, you + can mark files with space or v). If there are no marked files, + use the "current file" (where the cursor is) + + When attempting to trash non-empty directories or multiple + marked files, it will require a confirmation. + """ + + allow_abbrev = False + escape_macros_for_shell = True + + def execute(self): + import shlex + from functools import partial + + def is_directory_with_files(path): + return os.path.isdir(path) and not os.path.islink(path) and len(os.listdir(path)) > 0 + + if self.rest(1): + files = shlex.split(self.rest(1)) + many_files = (len(files) > 1 or is_directory_with_files(files[0])) + else: + cwd = self.fm.thisdir + tfile = self.fm.thisfile + if not cwd or not tfile: + self.fm.notify("Error: no file selected for deletion!", bad=True) + return + + # relative_path used for a user-friendly output in the confirmation. + files = [f.relative_path for f in self.fm.thistab.get_selection()] + many_files = (cwd.marked_items or is_directory_with_files(tfile.path)) + + confirm = self.fm.settings.confirm_on_delete + if confirm != 'never' and (confirm != 'multiple' or many_files): + self.fm.ui.console.ask( + "Confirm deletion of: %s (y/N)" % ', '.join(files), + partial(self._question_callback, files), + ('n', 'N', 'y', 'Y'), + ) + else: + # no need for a confirmation, just delete + self.fm.execute_file(files, label='trash') + + def tab(self, tabnum): + return self._tab_directory_content() + + def _question_callback(self, files, answer): + if answer == 'y' or answer == 'Y': + self.fm.execute_file(files, label='trash') + + class jump_non(Command): """:jump_non [-FLAGS...] @@ -1264,30 +1323,69 @@ class unmap(Command): self.fm.ui.keymaps.unbind(self.context, arg) -class cunmap(unmap): +class uncmap(unmap): + """:uncmap <keys> [<keys2>, ...] + + Remove the given "console" mappings + """ + context = 'console' + + +class cunmap(uncmap): """:cunmap <keys> [<keys2>, ...] Remove the given "console" mappings + + DEPRECATED in favor of uncmap. """ - context = 'browser' + def execute(self): + self.fm.notify("cunmap is deprecated in favor of uncmap!") + super(cunmap, self).execute() -class punmap(unmap): - """:punmap <keys> [<keys2>, ...] + +class unpmap(unmap): + """:unpmap <keys> [<keys2>, ...] Remove the given "pager" mappings """ context = 'pager' -class tunmap(unmap): - """:tunmap <keys> [<keys2>, ...] +class punmap(unpmap): + """:punmap <keys> [<keys2>, ...] + + Remove the given "pager" mappings + + DEPRECATED in favor of unpmap. + """ + + def execute(self): + self.fm.notify("punmap is deprecated in favor of unpmap!") + super(punmap, self).execute() + + +class untmap(unmap): + """:untmap <keys> [<keys2>, ...] Remove the given "taskview" mappings """ context = 'taskview' +class tunmap(untmap): + """:tunmap <keys> [<keys2>, ...] + + Remove the given "taskview" mappings + + DEPRECATED in favor of untmap. + """ + + def execute(self): + self.fm.notify("tunmap is deprecated in favor of untmap!") + super(tunmap, self).execute() + + class map_(Command): """:map <keysequence> <command> @@ -1828,11 +1926,14 @@ class yank(Command): ['xsel'], ['xsel', '-b'], ], + 'wl-copy': [ + ['wl-copy'], + ], 'pbcopy': [ ['pbcopy'], ], } - ordered_managers = ['pbcopy', 'xclip', 'xsel'] + ordered_managers = ['pbcopy', 'wl-copy', 'xclip', 'xsel'] executables = get_executables() for manager in ordered_managers: if manager in executables: @@ -1860,3 +1961,34 @@ class yank(Command): in sorted(self.modes.keys()) if mode ) + + +class paste_ext(Command): + """ + :paste_ext + + Like paste but tries to rename conflicting files so that the + file extension stays intact (e.g. file_.ext). + """ + + @staticmethod + def make_safe_path(dst): + if not os.path.exists(dst): + return dst + + dst_name, dst_ext = os.path.splitext(dst) + + if not dst_name.endswith("_"): + dst_name += "_" + if not os.path.exists(dst_name + dst_ext): + return dst_name + dst_ext + n = 0 + test_dst = dst_name + str(n) + while os.path.exists(test_dst + dst_ext): + n += 1 + test_dst = dst_name + str(n) + + return test_dst + dst_ext + + def execute(self): + return self.fm.paste(make_safe_path=paste_ext.make_safe_path) diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf index 2ca32e3c..7226130d 100644 --- a/ranger/config/rc.conf +++ b/ranger/config/rc.conf @@ -67,6 +67,9 @@ set vcs_backend_hg disabled set vcs_backend_bzr disabled set vcs_backend_svn disabled +# Truncate the long commit messages to this length when shown in the statusbar. +set vcs_msg_length 50 + # Use one of the supported image preview protocols set preview_images false @@ -144,6 +147,9 @@ set preview_files true set preview_directories true set collapse_preview true +# Wrap long lines in plain text previews? +set wrap_plaintext_previews false + # Save the console history on exit? set save_console_history true @@ -176,10 +182,10 @@ set display_free_space_in_status_bar true # Display files tags in all columns or only in main column? set display_tags_in_all_columns true -# Set a title for the window? +# Set a title for the window? Updates both `WM_NAME` and `WM_ICON_NAME` set update_title false -# Set the title to "ranger" in the tmux program? +# Set the tmux window-name to "ranger"? set update_tmux_title true # Shorten the title if it gets long? The number defines how many @@ -401,6 +407,7 @@ map <F5> copy map <F6> cut map <F7> console mkdir%space map <F8> console delete +#map <F8> console trash map <F10> exit # In case you work on a keyboard with dvorak layout @@ -488,6 +495,7 @@ map p`<any> paste dest=%any_path map p'<any> paste dest=%any_path map dD console delete +map dT console trash map dd cut map ud uncut diff --git a/ranger/config/rifle.conf b/ranger/config/rifle.conf index a90646e2..86f53fd1 100644 --- a/ranger/config/rifle.conf +++ b/ranger/config/rifle.conf @@ -26,7 +26,7 @@ # directory | $1 is a directory # number <n> | change the number of this command to n # terminal | stdin, stderr and stdout are connected to a terminal -# X | $DISPLAY is not empty (i.e. Xorg runs) +# X | A graphical environment is available (darwin, Xorg, or Wayland) # # There are also pseudo-conditions which have a "side effect": # flag <flags> | Change how the program is run. See below. @@ -66,13 +66,13 @@ ext x?html?, has uzbl-tabbed, X, flag f = uzbl-tabbed -- "$@" ext x?html?, has uzbl-browser, X, flag f = uzbl-browser -- "$@" ext x?html?, has uzbl-core, X, flag f = uzbl-core -- "$@" ext x?html?, has midori, X, flag f = midori -- "$@" -ext x?html?, has chromium-browser, X, flag f = chromium-browser -- "$@" -ext x?html?, has chromium, X, flag f = chromium -- "$@" -ext x?html?, has google-chrome, X, flag f = google-chrome -- "$@" ext x?html?, has opera, X, flag f = opera -- "$@" ext x?html?, has firefox, X, flag f = firefox -- "$@" ext x?html?, has seamonkey, X, flag f = seamonkey -- "$@" ext x?html?, has iceweasel, X, flag f = iceweasel -- "$@" +ext x?html?, has chromium-browser, X, flag f = chromium-browser -- "$@" +ext x?html?, has chromium, X, flag f = chromium -- "$@" +ext x?html?, has google-chrome, X, flag f = google-chrome -- "$@" ext x?html?, has epiphany, X, flag f = epiphany -- "$@" ext x?html?, has konqueror, X, flag f = konqueror -- "$@" ext x?html?, has elinks, terminal = elinks "$@" @@ -259,10 +259,26 @@ label wallpaper, number 12, mime ^image, has feh, X = feh --bg-tile "$1" label wallpaper, number 13, mime ^image, has feh, X = feh --bg-center "$1" label wallpaper, number 14, mime ^image, has feh, X = feh --bg-fill "$1" +#------------------------------------------- +# Generic file openers +#------------------------------------------- +label open, has xdg-open = xdg-open -- "$@" +label open, has open = open -- "$@" + # Define the editor for non-text files + pager as last action !mime ^text, !ext xml|json|csv|tex|py|pl|rb|js|sh|php = ask label editor, !mime ^text, !ext xml|json|csv|tex|py|pl|rb|js|sh|php = ${VISUAL:-$EDITOR} -- "$@" label pager, !mime ^text, !ext xml|json|csv|tex|py|pl|rb|js|sh|php = "$PAGER" -- "$@" -# The very last action, so that it's never triggered accidentally, is to execute a program: + +###################################################################### +# The actions below are left so low down in this file on purpose, so # +# they are never triggered accidentally. # +###################################################################### + +# Execute a file as program/script. mime application/x-executable = "$1" + +# Move the file to trash using trash-cli. +label trash, has trash-put = trash-put -- "$@" +label trash = mkdir -p -- ${XDG_DATA_DIR:-$HOME/.ranger}/ranger-trash; mv -- "$@" ${XDG_DATA_DIR:-$HOME/.ranger}/ranger-trash diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py index 4fb47354..7de889bf 100644 --- a/ranger/container/fsobject.py +++ b/ranger/container/fsobject.py @@ -342,10 +342,10 @@ class FileSystemObject( # pylint: disable=too-many-instance-attributes,too-many if self.permissions is not None: return self.permissions - if self.is_directory: - perms = ['d'] - elif self.is_link: + if self.is_link: perms = ['l'] + elif self.is_directory: + perms = ['d'] else: perms = ['-'] diff --git a/ranger/container/settings.py b/ranger/container/settings.py index 82901ac0..7549325a 100644 --- a/ranger/container/settings.py +++ b/ranger/container/settings.py @@ -94,9 +94,11 @@ ALLOWED_SETTINGS = { 'vcs_backend_git': str, 'vcs_backend_hg': str, 'vcs_backend_svn': str, + 'vcs_msg_length': int, 'viewmode': str, 'w3m_delay': float, 'w3m_offset': int, + 'wrap_plaintext_previews': bool, 'wrap_scroll': bool, 'xterm_alt_key': bool, } diff --git a/ranger/core/actions.py b/ranger/core/actions.py index 25b01e2c..c3d7de86 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -24,6 +24,7 @@ import ranger from ranger.ext.direction import Direction from ranger.ext.relative_symlink import relative_symlink from ranger.ext.keybinding_parser import key_to_string, construct_keybinding +from ranger.ext.safe_path import get_safe_path from ranger.ext.shell_escape import shell_quote from ranger.ext.next_available_filename import next_available_filename from ranger.ext.rifle import squash_flags, ASK_COMMAND @@ -1118,10 +1119,6 @@ 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)) @@ -1591,19 +1588,21 @@ 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, dest=None): + def paste(self, overwrite=False, append=False, dest=None, make_safe_path=get_safe_path): """:paste Paste the selected items into the current directory or to dest if provided. """ - if dest is None or isdir(dest): + if dest is None: + dest = self.thistab.path + if isdir(dest): loadable = CopyLoader(self.copy_buffer, self.do_cut, overwrite, - dest) + dest, make_safe_path) self.loader.add(loadable, append=append) self.do_cut = False else: - self.notify('Failed to paste. The given path is invalid.', bad=True) + self.notify('Failed to paste. The destination is invalid.', bad=True) def delete(self, files=None): # XXX: warn when deleting mount points/unseen marked files? diff --git a/ranger/core/fm.py b/ranger/core/fm.py index 43001e8b..7d23c9b6 100644 --- a/ranger/core/fm.py +++ b/ranger/core/fm.py @@ -22,11 +22,7 @@ from ranger.container.tags import Tags, TagsDummy from ranger.gui.ui import UI from ranger.container.bookmarks import Bookmarks from ranger.core.runner import Runner -from ranger.ext.img_display import (W3MImageDisplayer, ITerm2ImageDisplayer, - TerminologyImageDisplayer, - URXVTImageDisplayer, URXVTImageFSDisplayer, - KittyImageDisplayer, UeberzugImageDisplayer, - ImageDisplayer) +from ranger.ext.img_display import get_image_displayer from ranger.core.metadata import MetadataManager from ranger.ext.rifle import Rifle from ranger.container.directory import Directory @@ -104,7 +100,7 @@ class FM(Actions, # pylint: disable=too-many-instance-attributes def set_image_displayer(): if self.image_displayer: self.image_displayer.quit() - self.image_displayer = self._get_image_displayer() + self.image_displayer = get_image_displayer(self.settings.preview_images_method) set_image_displayer() self.settings.signal_bind('setopt.preview_images_method', set_image_displayer, priority=settings.SIGNAL_PRIORITY_AFTER_SYNC) @@ -227,23 +223,6 @@ class FM(Actions, # pylint: disable=too-many-instance-attributes for line in entry.splitlines(): yield line - def _get_image_displayer(self): # pylint: disable=too-many-return-statements - if self.settings.preview_images_method == "w3m": - return W3MImageDisplayer() - elif self.settings.preview_images_method == "iterm2": - return ITerm2ImageDisplayer() - elif self.settings.preview_images_method == "terminology": - return TerminologyImageDisplayer() - elif self.settings.preview_images_method == "urxvt": - return URXVTImageDisplayer() - elif self.settings.preview_images_method == "urxvt-full": - return URXVTImageFSDisplayer() - elif self.settings.preview_images_method == "kitty": - return KittyImageDisplayer() - elif self.settings.preview_images_method == "ueberzug": - return UeberzugImageDisplayer() - return ImageDisplayer() - def _get_thisfile(self): return self.thistab.thisfile diff --git a/ranger/core/loader.py b/ranger/core/loader.py index 26b729b6..7d4b21e8 100644 --- a/ranger/core/loader.py +++ b/ranger/core/loader.py @@ -19,6 +19,7 @@ except ImportError: HAVE_CHARDET = False from ranger.core.shared import FileManagerAware +from ranger.ext.safe_path import get_safe_path from ranger.ext.signals import SignalDispatcher from ranger.ext.human_readable import human_readable @@ -51,12 +52,14 @@ 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, dest=None): + def __init__(self, copy_buffer, do_cut=False, overwrite=False, dest=None, + make_safe_path=get_safe_path): self.copy_buffer = tuple(copy_buffer) self.do_cut = do_cut self.original_copy_buffer = copy_buffer self.original_path = dest if dest is not None else self.fm.thistab.path self.overwrite = overwrite + self.make_safe_path = make_safe_path self.percent = 0 if self.copy_buffer: self.one_file = self.copy_buffer[0] @@ -108,7 +111,8 @@ class CopyLoader(Loadable, FileManagerAware): # pylint: disable=too-many-instan self.fm.tags.dump() n = 0 for n in shutil_g.move(src=fobj.path, dst=self.original_path, - overwrite=self.overwrite): + overwrite=self.overwrite, + make_safe_path=self.make_safe_path): self.percent = ((done + n) / size) * 100. yield done += n @@ -125,6 +129,7 @@ class CopyLoader(Loadable, FileManagerAware): # pylint: disable=too-many-instan dst=os.path.join(self.original_path, fobj.basename), symlinks=True, overwrite=self.overwrite, + make_safe_path=self.make_safe_path, ): self.percent = ((done + n) / size) * 100. yield @@ -132,7 +137,8 @@ class CopyLoader(Loadable, FileManagerAware): # pylint: disable=too-many-instan else: n = 0 for n in shutil_g.copy2(fobj.path, self.original_path, - symlinks=True, overwrite=self.overwrite): + symlinks=True, overwrite=self.overwrite, + make_safe_path=self.make_safe_path): self.percent = ((done + n) / size) * 100. yield done += n diff --git a/ranger/core/main.py b/ranger/core/main.py index aefaacbc..7322a501 100644 --- a/ranger/core/main.py +++ b/ranger/core/main.py @@ -333,7 +333,8 @@ def parse_arguments(): return path if args.clean: - args.cachedir = None + from tempfile import mkdtemp + args.cachedir = mkdtemp(suffix='.ranger-cache') args.confdir = None args.datadir = None else: diff --git a/ranger/core/runner.py b/ranger/core/runner.py index f38b026a..d465f070 100644 --- a/ranger/core/runner.py +++ b/ranger/core/runner.py @@ -214,7 +214,9 @@ class Runner(object): # pylint: disable=too-few-public-methods toggle_ui = True context.wait = True if 't' in context.flags: - if 'DISPLAY' not in os.environ: + if not ('WAYLAND_DISPLAY' in os.environ + or sys.platform == 'darwin' + or 'DISPLAY' in os.environ): return self._log("Can not run with 't' flag, no display found!") term = get_term() if isinstance(action, str): diff --git a/ranger/data/mime.types b/ranger/data/mime.types index 900cb7d9..c20003d3 100644 --- a/ranger/data/mime.types +++ b/ranger/data/mime.types @@ -8,22 +8,31 @@ # Mimetypes are used for colorschemes and the builtin filetype detection # to execute files with the right program. # +# You can find the official list of Media Types assigned by IANA here, +# https://www.iana.org/assignments/media-types/media-types.xhtml +# We deviate from these in certain cases when formats lack an official type +# or the type is too generic, application/* for a video format for example. +# In such cases we try to adhere to what file(1) returns. +# ############################################################################### -audio/flac flac +application/javascript js + audio/musepack mpc mpp mp+ -audio/ogg oga ogg spx +audio/ogg oga ogg spx opus audio/wavpack wv wvc audio/webm weba audio/x-ape ape +audio/x-dsdiff dsf +audio/x-flac flac -video/mkv mkv -video/webm webm -video/flash flv -video/ogg ogv ogm -video/divx div divx +image/vnd.djvu djvu +image/webp webp text/x-ruby rb -application/javascript js -application/djvu djvu +video/ogg ogv ogm +video/webm webm +video/x-flv flv +video/x-matroska mkv +video/x-msvideo divx diff --git a/ranger/data/scope.sh b/ranger/data/scope.sh index eb09b2bf..caa9475f 100755 --- a/ranger/data/scope.sh +++ b/ranger/data/scope.sh @@ -87,12 +87,19 @@ handle_extension() { w3m -dump "${FILE_PATH}" && exit 5 lynx -dump -- "${FILE_PATH}" && exit 5 elinks -dump "${FILE_PATH}" && exit 5 - ;; # Continue with next handler on failure + ;; + ## JSON json) jq --color-output . "${FILE_PATH}" && exit 5 python -m json.tool -- "${FILE_PATH}" && exit 5 ;; + + ## Direct Stream Digital/Transfer (DSDIFF) + dsf) + mediainfo "${FILE_PATH}" && exit 5 + exiftool "${FILE_PATH}" && exit 5 + ;; # Continue with next handler on failure esac } @@ -237,6 +244,8 @@ handle_mime() { env HIGHLIGHT_OPTIONS="${HIGHLIGHT_OPTIONS}" highlight \ --out-format="${highlight_format}" \ --force -- "${FILE_PATH}" && exit 5 + env COLORTERM=8bit bat --color=always --style="plain" \ + -- "${FILE_PATH}" && exit 5 pygmentize -f "${pygmentize_format}" -O "style=${PYGMENTIZE_STYLE}"\ -- "${FILE_PATH}" && exit 5 exit 2;; diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py index 2cce5c7a..8d1d8168 100644 --- a/ranger/ext/img_display.py +++ b/ranger/ext/img_display.py @@ -23,6 +23,7 @@ import warnings import json import threading from subprocess import Popen, PIPE +from collections import defaultdict import termios from contextlib import contextmanager @@ -72,6 +73,33 @@ class ImgDisplayUnsupportedException(Exception): pass +def fallback_image_displayer(): + """Simply makes some noise when chosen. Temporary fallback behavior.""" + + raise ImgDisplayUnsupportedException + + +IMAGE_DISPLAYER_REGISTRY = defaultdict(fallback_image_displayer) + + +def register_image_displayer(nickname=None): + """Register an ImageDisplayer by nickname if available.""" + + def decorator(image_displayer_class): + if nickname: + registry_key = nickname + else: + registry_key = image_displayer_class.__name__ + IMAGE_DISPLAYER_REGISTRY[registry_key] = image_displayer_class + return image_displayer_class + return decorator + + +def get_image_displayer(registry_key): + image_displayer_class = IMAGE_DISPLAYER_REGISTRY[registry_key] + return image_displayer_class() + + class ImageDisplayer(object): """Image display provider functions for drawing images in the terminal""" @@ -90,6 +118,7 @@ class ImageDisplayer(object): pass +@register_image_displayer("w3m") class W3MImageDisplayer(ImageDisplayer, FileManagerAware): """Implementation of ImageDisplayer using w3mimgdisplay, an utilitary program from w3m (a text-based web browser). w3mimgdisplay can display @@ -243,6 +272,7 @@ class W3MImageDisplayer(ImageDisplayer, FileManagerAware): # ranger-independent libraries. +@register_image_displayer("iterm2") class ITerm2ImageDisplayer(ImageDisplayer, FileManagerAware): """Implementation of ImageDisplayer using iTerm2 image display support (http://iterm2.com/images.html). @@ -308,7 +338,7 @@ class ITerm2ImageDisplayer(ImageDisplayer, FileManagerAware): def _encode_image_content(path): """Read and encode the contents of path""" with open(path, 'rb') as fobj: - return base64.b64encode(fobj.read()) + return base64.b64encode(fobj.read()).decode('utf-8') @staticmethod def _get_image_dimensions(path): @@ -351,6 +381,7 @@ class ITerm2ImageDisplayer(ImageDisplayer, FileManagerAware): return width, height +@register_image_displayer("terminology") class TerminologyImageDisplayer(ImageDisplayer, FileManagerAware): """Implementation of ImageDisplayer using terminology image display support (https://github.com/billiob/terminology). @@ -390,6 +421,7 @@ class TerminologyImageDisplayer(ImageDisplayer, FileManagerAware): self.clear(0, 0, 0, 0) +@register_image_displayer("urxvt") class URXVTImageDisplayer(ImageDisplayer, FileManagerAware): """Implementation of ImageDisplayer working by setting the urxvt background image "under" the preview pane. @@ -474,6 +506,7 @@ class URXVTImageDisplayer(ImageDisplayer, FileManagerAware): self.clear(0, 0, 0, 0) # dummy assignments +@register_image_displayer("urxvt-full") class URXVTImageFSDisplayer(URXVTImageDisplayer): """URXVTImageDisplayer that utilizes the whole terminal.""" @@ -486,6 +519,7 @@ class URXVTImageFSDisplayer(URXVTImageDisplayer): return self._get_centered_offsets() +@register_image_displayer("kitty") 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, @@ -681,6 +715,7 @@ class KittyImageDisplayer(ImageDisplayer, FileManagerAware): # continue +@register_image_displayer("ueberzug") class UeberzugImageDisplayer(ImageDisplayer): """Implementation of ImageDisplayer using ueberzug. Ueberzug can display images in a Xorg session. diff --git a/ranger/ext/rifle.py b/ranger/ext/rifle.py index a55e14c7..a73a188b 100755 --- a/ranger/ext/rifle.py +++ b/ranger/ext/rifle.py @@ -237,7 +237,9 @@ class Rifle(object): # pylint: disable=too-many-instance-attributes self._app_flags = argument return True elif function == 'X': - return sys.platform == 'darwin' or 'DISPLAY' in os.environ + return ('WAYLAND_DISPLAY' in os.environ + or sys.platform == 'darwin' + or 'DISPLAY' in os.environ) elif function == 'env': return bool(os.environ.get(argument)) elif function == 'else': @@ -293,6 +295,9 @@ class Rifle(object): # pylint: disable=too-many-instance-attributes self._app_flags = '' self._app_label = None if skip_ask and cmd == ASK_COMMAND: + # TODO(vifon): Fix properly, see + # https://github.com/ranger/ranger/pull/1341#issuecomment-537264495 + count += 1 continue for test in tests: if not self._eval_condition(test, files, None): diff --git a/ranger/ext/safe_path.py b/ranger/ext/safe_path.py new file mode 100644 index 00000000..b172b577 --- /dev/null +++ b/ranger/ext/safe_path.py @@ -0,0 +1,22 @@ +# This file is part of ranger, the console file manager. +# License: GNU GPL version 3, see the file "AUTHORS" for details. + +import os + +SUFFIX = '_' + + +def get_safe_path(dst): + if not os.path.exists(dst): + return dst + if not dst.endswith(SUFFIX): + dst += SUFFIX + if not os.path.exists(dst): + return dst + n = 0 + test_dst = dst + str(n) + while os.path.exists(test_dst): + n += 1 + test_dst = dst + str(n) + + return test_dst diff --git a/ranger/ext/shutil_generatorized.py b/ranger/ext/shutil_generatorized.py index fe3cf068..a97a4d2d 100644 --- a/ranger/ext/shutil_generatorized.py +++ b/ranger/ext/shutil_generatorized.py @@ -7,11 +7,11 @@ import os import stat import sys from shutil import (_samefile, rmtree, _basename, _destinsrc, Error, SpecialFileError) +from ranger.ext.safe_path import get_safe_path __all__ = ["copyfileobj", "copyfile", "copystat", "copy2", "BLOCK_SIZE", "copytree", "move", "rmtree", "Error", "SpecialFileError"] -APPENDIX = '_' BLOCK_SIZE = 16 * 1024 @@ -103,22 +103,6 @@ else: pass -def get_safe_path(dst): - if not os.path.exists(dst): - return dst - if not dst.endswith(APPENDIX): - dst += APPENDIX - if not os.path.exists(dst): - return dst - n = 0 - test_dst = dst + str(n) - while os.path.exists(test_dst): - n += 1 - test_dst = dst + str(n) - - return test_dst - - def copyfileobj(fsrc, fdst, length=BLOCK_SIZE): """copy data from file-like object fsrc to file-like object fdst""" done = 0 @@ -153,7 +137,7 @@ def copyfile(src, dst): yield done -def copy2(src, dst, overwrite=False, symlinks=False): +def copy2(src, dst, overwrite=False, symlinks=False, make_safe_path=get_safe_path): """Copy data and all stat info ("cp -p src dst"). The destination may be a directory. @@ -162,7 +146,7 @@ def copy2(src, dst, overwrite=False, symlinks=False): if os.path.isdir(dst): dst = os.path.join(dst, os.path.basename(src)) if not overwrite: - dst = get_safe_path(dst) + dst = make_safe_path(dst) if symlinks and os.path.islink(src): linkto = os.readlink(src) if overwrite and os.path.lexists(dst): @@ -175,7 +159,7 @@ def copy2(src, dst, overwrite=False, symlinks=False): def copytree(src, dst, # pylint: disable=too-many-locals,too-many-branches - symlinks=False, ignore=None, overwrite=False): + symlinks=False, ignore=None, overwrite=False, make_safe_path=get_safe_path): """Recursively copy a directory tree using copy2(). The destination directory must not already exist. @@ -211,7 +195,7 @@ def copytree(src, dst, # pylint: disable=too-many-locals,too-many-branches os.makedirs(dst) except OSError: if not overwrite: - dst = get_safe_path(dst) + dst = make_safe_path(dst) os.makedirs(dst) errors = [] done = 0 @@ -229,13 +213,15 @@ def copytree(src, dst, # pylint: disable=too-many-locals,too-many-branches copystat(srcname, dstname) elif os.path.isdir(srcname): n = 0 - for n in copytree(srcname, dstname, symlinks, ignore, overwrite): + for n in copytree(srcname, dstname, symlinks, ignore, overwrite, + make_safe_path): yield done + n done += n else: # Will raise a SpecialFileError for unsupported file types n = 0 - for n in copy2(srcname, dstname, overwrite=overwrite, symlinks=symlinks): + for n in copy2(srcname, dstname, overwrite=overwrite, symlinks=symlinks, + make_safe_path=make_safe_path): yield done + n done += n # catch the Error from the recursive copytree so that we can @@ -256,7 +242,7 @@ def copytree(src, dst, # pylint: disable=too-many-locals,too-many-branches raise Error(errors) -def move(src, dst, overwrite=False): +def move(src, dst, overwrite=False, make_safe_path=get_safe_path): """Recursively move a file or directory to another location. This is similar to the Unix "mv" command. @@ -283,17 +269,19 @@ def move(src, dst, overwrite=False): real_dst = os.path.join(dst, _basename(src)) if not overwrite: - real_dst = get_safe_path(real_dst) + real_dst = make_safe_path(real_dst) try: os.rename(src, real_dst) except OSError: if os.path.isdir(src): if _destinsrc(src, dst): raise Error("Cannot move a directory '%s' into itself '%s'." % (src, dst)) - for done in copytree(src, real_dst, symlinks=True, overwrite=overwrite): + for done in copytree(src, real_dst, symlinks=True, overwrite=overwrite, + make_safe_path=make_safe_path): yield done rmtree(src) else: - for done in copy2(src, real_dst, symlinks=True, overwrite=overwrite): + for done in copy2(src, real_dst, symlinks=True, overwrite=overwrite, + make_safe_path=make_safe_path): yield done os.unlink(src) diff --git a/ranger/gui/displayable.py b/ranger/gui/displayable.py index 16cb275f..1c3fb3e4 100644 --- a/ranger/gui/displayable.py +++ b/ranger/gui/displayable.py @@ -197,7 +197,11 @@ class Displayable( # pylint: disable=too-many-instance-attributes try: self.win.mvderwin(y, x) except curses.error: - pass + try: + self.win.resize(hei, wid) + self.win.mvderwin(y, x) + except curses.error: + pass self.paryx = self.win.getparyx() self.y, self.x = self.paryx diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py index 2874ee97..b6ea0886 100644 --- a/ranger/gui/ui.py +++ b/ranger/gui/ui.py @@ -20,6 +20,12 @@ from .mouse_event import MouseEvent MOUSEMASK = curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION +# This escape is not available with a capname from terminfo unlike +# tsl (to_status_line), so it's hardcoded here. It's used just like tsl, +# but it sets the icon title (WM_ICON_NAME) instead of the window title +# (WM_NAME). +ESCAPE_ICON_TITLE = '\033]1;' + _ASCII = ''.join(chr(c) for c in range(32, 127)) @@ -238,7 +244,7 @@ class UI( # pylint: disable=too-many-instance-attributes,too-many-public-method for key in keys: self.handle_key(key) - def handle_input(self): + def handle_input(self): # pylint: disable=too-many-branches key = self.win.getch() if key == curses.KEY_ENTER: key = ord('\n') @@ -277,6 +283,9 @@ class UI( # pylint: disable=too-many-instance-attributes,too-many-public-method else: if not self.fm.input_is_blocked(): self.handle_key(key) + elif key == -1 and not os.isatty(sys.stdin.fileno()): + # STDIN has been closed + self.fm.exit() def setup(self): """Build up the UI by initializing widgets.""" @@ -381,16 +390,18 @@ class UI( # pylint: disable=too-many-instance-attributes,too-many-public-method try: fixed_cwd = cwd.encode('utf-8', 'surrogateescape'). \ decode('utf-8', 'replace') - fmt_tup = ( + escapes = [ curses.tigetstr('tsl').decode('latin-1'), - fixed_cwd, - curses.tigetstr('fsl').decode('latin-1'), - ) + ESCAPE_ICON_TITLE + ] + bel = curses.tigetstr('fsl').decode('latin-1') + fmt_tups = [(e, fixed_cwd, bel) for e in escapes] except UnicodeError: pass else: - sys.stdout.write("%sranger:%s%s" % fmt_tup) - sys.stdout.flush() + for fmt_tup in fmt_tups: + sys.stdout.write("%sranger:%s%s" % fmt_tup) + sys.stdout.flush() self.win.refresh() diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index ecc66f44..1412ef6a 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -48,6 +48,9 @@ class BrowserColumn(Pager): # pylint: disable=too-many-instance-attributes level 0 => current file/directory level <0 => parent directories """ + self.need_redraw = False + self.image = None + self.need_clear_image = True Pager.__init__(self, win) Widget.__init__(self, win) # pylint: disable=non-parent-init-called self.level = level diff --git a/ranger/gui/widgets/pager.py b/ranger/gui/widgets/pager.py index c1aa2765..064f28ca 100644 --- a/ranger/gui/widgets/pager.py +++ b/ranger/gui/widgets/pager.py @@ -246,11 +246,18 @@ class Pager(Widget): # pylint: disable=too-many-instance-attributes while True: try: line = self._get_line(i).expandtabs(4) - if self.markup == 'ansi': - line = ansi.char_slice(line, startx, self.wid) + ansi.reset - else: - line = line[startx:self.wid + startx] - yield line.rstrip().replace('\r\n', '\n') + for part in ((0,) if not + self.fm.settings.wrap_plaintext_previews else + range(max(1, ((len(line) - 1) // self.wid) + 1))): + shift = part * self.wid + if self.markup == 'ansi': + line_bit = (ansi.char_slice(line, startx + shift, + self.wid + shift) + + ansi.reset) + else: + line_bit = line[startx + shift:self.wid + startx + + shift] + yield line_bit.rstrip().replace('\r\n', '\n') except IndexError: return i += 1 diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py index 19113012..fd44613e 100644 --- a/ranger/gui/widgets/statusbar.py +++ b/ranger/gui/widgets/statusbar.py @@ -212,7 +212,11 @@ class StatusBar(Widget): # pylint: disable=too-many-instance-attributes left.add_space() left.add(directory.vcs.rootvcs.head['date'].strftime(self.timeformat), 'vcsdate') left.add_space() - left.add(directory.vcs.rootvcs.head['summary'][:50], 'vcscommit') + summary_length = self.settings.vcs_msg_length or 50 + left.add( + directory.vcs.rootvcs.head['summary'][:summary_length], + 'vcscommit' + ) def _get_owner(self, target): uid = target.stat.st_uid |