diff options
34 files changed, 503 insertions, 218 deletions
diff --git a/README.md b/README.md index b5fc03ea..5174f1c1 100644 --- a/README.md +++ b/README.md @@ -69,12 +69,13 @@ Optional: Optional, for enhanced file previews (with "scope.sh"): * img2txt (from caca-utils) for ASCII-art image previews -* highlight for syntax highlighting of code -* atool for previews of archives +* highlight or pygmentize for syntax highlighting of code +* atool, acat, bsdtar and/or unrar for previews of archives * lynx, w3m or elinks for previews of html pages * pdftotext for pdf previews * transmission-show for viewing bit-torrent information * mediainfo or exiftool for viewing information about media files +* odt2txt for OpenDocument text files (odt, ods, odp and sxw) Installing diff --git a/doc/ranger.1 b/doc/ranger.1 index b2ba1aac..26a78a49 100644 --- a/doc/ranger.1 +++ b/doc/ranger.1 @@ -1,4 +1,4 @@ -.\" Automatically generated by Pod::Man 2.28 (Pod::Simple 3.29) +.\" Automatically generated by Pod::Man 2.28 (Pod::Simple 3.28) .\" .\" Standard preamble: .\" ======================================================================== @@ -133,7 +133,7 @@ .\" ======================================================================== .\" .IX Title "RANGER 1" -.TH RANGER 1 "ranger-1.7.2" "16/05/16" "ranger manual" +.TH RANGER 1 "ranger-1.7.2" "10/08/2016" "ranger manual" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l @@ -287,6 +287,24 @@ This only works in iTerm2 compiled with image preview support, but works over ssh. .PP To enable this feature, set the option \f(CW\*(C`preview_images_method\*(C'\fR to iterm2. +.PP +\fIurxvt\fR +.IX Subsection "urxvt" +.PP +This only works in urxvt compiled with pixbuf support. Does not work over ssh. +.PP +Essentially this mode sets an image as a terminal background temporarily, so it +will break any previously set image background. +.PP +To enable this feature, set the option \f(CW\*(C`preview_images_method\*(C'\fR to urxvt. +.PP +\fIurxvt-full\fR +.IX Subsection "urxvt-full" +.PP +The same as urxvt but utilizing not only the preview pane but the whole terminal +window. +.PP +To enable this feature, set the option \f(CW\*(C`preview_images_method\*(C'\fR to urxvt-full. .SS "\s-1SELECTION\s0" .IX Subsection "SELECTION" The \fIselection\fR is defined as \*(L"All marked files \s-1IF THERE ARE ANY,\s0 otherwise @@ -337,6 +355,8 @@ can use it for something like this command: The macro \f(CW%space\fR expands to a space character. You can use it to add spaces to the end of a command when needed, while preventing editors to strip spaces off the end of the line automatically. +.PP +To write a literal %, you need to escape it by writing %%. .SS "\s-1BOOKMARKS\s0" .IX Subsection "BOOKMARKS" Type \fBm<key>\fR to bookmark the current directory. You can re-enter this @@ -1029,7 +1049,7 @@ Examples: .IP "filter [\fIstring\fR]" 2 .IX Item "filter [string]" Displays only the files which contain the \fIstring\fR in their basename. Running -this command without any parameter will reset the fitler. +this command without any parameter will reset the filter. .Sp This command is based on the \fIscout\fR command and supports all of its options. .IP "filter_inode_type [dfl]" 2 @@ -1168,6 +1188,7 @@ influence its behaviour: \& \-m = mark the matching files after pressing enter \& \-M = unmark the matching files after pressing enter \& \-p = permanent filter: hide non\-matching files after pressing enter +\& \-r = interpret pattern as a regular expression pattern \& \-s = smart case; like \-i unless pattern contains upper case letters \& \-t = apply filter and search pattern as you type \& \-v = inverts the match diff --git a/doc/ranger.pod b/doc/ranger.pod index 7c5a7768..fe2b45eb 100644 --- a/doc/ranger.pod +++ b/doc/ranger.pod @@ -186,6 +186,22 @@ ssh. To enable this feature, set the option C<preview_images_method> to iterm2. +=head3 urxvt + +This only works in urxvt compiled with pixbuf support. Does not work over ssh. + +Essentially this mode sets an image as a terminal background temporarily, so it +will break any previously set image background. + +To enable this feature, set the option C<preview_images_method> to urxvt. + +=head3 urxvt-full + +The same as urxvt but utilizing not only the preview pane but the whole terminal +window. + +To enable this feature, set the option C<preview_images_method> to urxvt-full. + =head2 SELECTION The I<selection> is defined as "All marked files IF THERE ARE ANY, otherwise @@ -234,6 +250,8 @@ The macro %space expands to a space character. You can use it to add spaces to the end of a command when needed, while preventing editors to strip spaces off the end of the line automatically. +To write a literal %, you need to escape it by writing %%. + =head2 BOOKMARKS Type B<m<keyE<gt>> to bookmark the current directory. You can re-enter this @@ -1063,7 +1081,7 @@ Examples: =item filter [I<string>] Displays only the files which contain the I<string> in their basename. Running -this command without any parameter will reset the fitler. +this command without any parameter will reset the filter. This command is based on the I<scout> command and supports all of its options. diff --git a/doc/rifle.1 b/doc/rifle.1 index 0b67edcc..f90bb6e1 100644 --- a/doc/rifle.1 +++ b/doc/rifle.1 @@ -1,4 +1,4 @@ -.\" Automatically generated by Pod::Man 2.28 (Pod::Simple 3.29) +.\" Automatically generated by Pod::Man 4.07 (Pod::Simple 3.32) .\" .\" Standard preamble: .\" ======================================================================== @@ -46,7 +46,7 @@ .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\" -.\" If the F register is turned on, we'll generate index entries on stderr for +.\" If the F register is >0, we'll generate index entries on stderr for .\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index .\" entries marked with X<> in POD. Of course, you'll have to process the .\" output yourself in some meaningful fashion. @@ -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.7.2" "02/24/2016" "rifle manual" +.TH RIFLE 1 "rifle-1.7.2" "09/03/2016" "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/examples/bash_automatic_cd.sh b/examples/bash_automatic_cd.sh index 040bf21a..bdac5757 100644 --- a/examples/bash_automatic_cd.sh +++ b/examples/bash_automatic_cd.sh @@ -6,6 +6,8 @@ # the last visited one after ranger quits. # To undo the effect of this function, you can type "cd -" to return to the # original directory. +# +# On OS X 10 or later, replace `usr/bin/ranger` with `/usr/local/bin/ranger`. function ranger-cd { tempfile="$(mktemp -t tmp.XXXXXX)" diff --git a/examples/rc_emacs.conf b/examples/rc_emacs.conf index 39a6d654..694496c2 100644 --- a/examples/rc_emacs.conf +++ b/examples/rc_emacs.conf @@ -73,6 +73,14 @@ set preview_images false # Preview images in full color using iTerm2 image previews # (http://iterm2.com/images.html). This requires using iTerm2 compiled # with image preview support. +# +# * urxvt: +# Preview images in full color using urxvt image backgrounds. This +# requires using urxvt compiled with pixbuf support. +# +# * urxvt-full: +# The same as urxvt but utilizing not only the preview pane but the +# whole terminal window. set preview_images_method w3m # Use a unicode "..." character to mark cut-off filenames? diff --git a/ranger/__init__.py b/ranger/__init__.py index 4d8e4afe..9c4f8922 100644 --- a/ranger/__init__.py +++ b/ranger/__init__.py @@ -29,44 +29,9 @@ CACHEDIR = os.path.expanduser("~/.cache/ranger") USAGE = '%prog [options] [path]' VERSION = 'ranger-master %s\n\nPython %s' % (__version__, sys.version) -try: - ExceptionClass = FileNotFoundError -except NameError: - ExceptionClass = IOError -try: - LOGFILE = tempfile.gettempdir() + '/ranger_errorlog' -except ExceptionClass: - LOGFILE = '/dev/null' -del ExceptionClass # If the environment variable XDG_CONFIG_HOME is non-empty, CONFDIR is ignored # and the configuration directory will be $XDG_CONFIG_HOME/ranger instead. CONFDIR = '~/.config/ranger' -# Debugging functions. These will be activated when run with --debug. -# Example usage in the code: -# import ranger; ranger.log("hello world") - - -def log(*objects, **keywords): - """Writes objects to a logfile (for the purpose of debugging only.) - Has the same arguments as print() in python3. - """ - from ranger import arg - if LOGFILE is None or not arg.debug or arg.clean: - return - start = keywords.get('start', 'ranger:') - sep = keywords.get('sep', ' ') - end = keywords.get('end', '\n') - _file = keywords['file'] if 'file' in keywords else open(LOGFILE, 'a') - _file.write(sep.join(map(str, (start, ) + objects)) + end) - - -def log_traceback(): - from ranger import arg - if LOGFILE is None or not arg.debug or arg.clean: - return - import traceback - traceback.print_stack(file=open(LOGFILE, 'a')) - from ranger.core.main import main diff --git a/ranger/api/commands.py b/ranger/api/commands.py index 9b8ec7d5..93c50adb 100644 --- a/ranger/api/commands.py +++ b/ranger/api/commands.py @@ -236,6 +236,11 @@ class Command(FileManagerAware): break return flags, rest + @lazy_property + def log(self): + import logging + return logging.getLogger('ranger.commands.' + self.__class__.__name__) + # XXX: Lazy properties? Not so smart? self.line can change after all! @lazy_property def _tabinsert_left(self): diff --git a/ranger/config/commands.py b/ranger/config/commands.py index a5117696..18efbfc1 100755 --- a/ranger/config/commands.py +++ b/ranger/config/commands.py @@ -493,14 +493,8 @@ class terminal(Command): Spawns an "x-terminal-emulator" starting in the current directory. """ def execute(self): - import os - from ranger.ext.get_executables import get_executables - command = os.environ.get('TERMCMD', os.environ.get('TERM')) - if command not in get_executables(): - command = 'x-terminal-emulator' - if command not in get_executables(): - command = 'xterm' - self.fm.run(command, flags='f') + from ranger.ext.get_executables import get_term + self.fm.run(get_term(), flags='f') class delete(Command): diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf index cc7f5007..f25d3a0c 100644 --- a/ranger/config/rc.conf +++ b/ranger/config/rc.conf @@ -79,6 +79,14 @@ set preview_images false # Preview images in full color using iTerm2 image previews # (http://iterm2.com/images.html). This requires using iTerm2 compiled # with image preview support. +# +# * urxvt: +# Preview images in full color using urxvt image backgrounds. This +# requires using urxvt compiled with pixbuf support. +# +# * urxvt-full: +# The same as urxvt but utilizing not only the preview pane but the +# whole terminal window. set preview_images_method w3m # Use a unicode "..." character to mark cut-off filenames? diff --git a/ranger/config/rifle.conf b/ranger/config/rifle.conf index d6165f21..aadc2f2c 100644 --- a/ranger/config/rifle.conf +++ b/ranger/config/rifle.conf @@ -181,8 +181,11 @@ ext xcf, X, flag f = gimp -- "$@" #------------------------------------------- # Archives #------------------------------------------- + +# avoid password prompt by providing empty password +ext 7z, has 7z = 7z -p l "$@" | "$PAGER" # This requires atool -ext 7z|ace|ar|arc|bz2?|cab|cpio|cpt|deb|dgc|dmg|gz, has als = als -- "$@" | "$PAGER" +ext ace|ar|arc|bz2?|cab|cpio|cpt|deb|dgc|dmg|gz, has als = als -- "$@" | "$PAGER" ext iso|jar|msi|pkg|rar|shar|tar|tgz|xar|xpi|xz|zip, has als = als -- "$@" | "$PAGER" ext 7z|ace|ar|arc|bz2?|cab|cpio|cpt|deb|dgc|dmg|gz, has aunpack = aunpack -- "$@" ext iso|jar|msi|pkg|rar|shar|tar|tgz|xar|xpi|xz|zip, has aunpack = aunpack -- "$@" diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py index d73fa76c..9b312e6d 100644 --- a/ranger/container/fsobject.py +++ b/ranger/container/fsobject.py @@ -20,7 +20,7 @@ from pwd import getpwuid from ranger.core.linemode import * from ranger.core.shared import FileManagerAware, SettingsAware from ranger.ext.shell_escape import shell_escape -from ranger.ext.spawn import spawn +from ranger.ext import spawn from ranger.ext.lazy_property import lazy_property from ranger.ext.human_readable import human_readable @@ -131,7 +131,7 @@ class FileSystemObject(FileManagerAware, SettingsAware): @lazy_property def filetype(self): try: - return spawn(["file", '-Lb', '--mime-type', self.path]) + return spawn.check_output(["file", '-Lb', '--mime-type', self.path]) except OSError: return "" diff --git a/ranger/container/settings.py b/ranger/container/settings.py index a5d71874..1faf5860 100644 --- a/ranger/container/settings.py +++ b/ranger/container/settings.py @@ -79,7 +79,7 @@ ALLOWED_SETTINGS = { ALLOWED_VALUES = { 'confirm_on_delete': ['always', 'multiple', 'never'], 'line_numbers': ['false', 'absolute', 'relative'], - 'preview_images_method': ['w3m', 'iterm2'], + 'preview_images_method': ['w3m', 'iterm2', 'urxvt', 'urxvt-full'], 'vcs_backend_bzr': ['enabled', 'local', 'disabled'], 'vcs_backend_git': ['enabled', 'local', 'disabled'], 'vcs_backend_hg': ['enabled', 'local', 'disabled'], diff --git a/ranger/core/actions.py b/ranger/core/actions.py index c119c501..1b35e57b 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -13,6 +13,7 @@ from inspect import cleandoc from stat import S_IEXEC from hashlib import sha1 from sys import version_info +from logging import getLogger import ranger from ranger.ext.direction import Direction @@ -31,6 +32,8 @@ from ranger.core.linemode import DEFAULT_LINEMODE MACRO_FAIL = "<\x01\x01MACRO_HAS_NO_VALUE\x01\01>" +log = getLogger(__name__) + class _MacroTemplate(string.Template): """A template for substituting macros in commands""" @@ -152,7 +155,7 @@ class Actions(FileManagerAware, SettingsAware): elif bad is True and ranger.arg.debug: raise Exception(str(text)) text = str(text) - self.log.appendleft(text) + log.debug("Command notify invoked: [Bad: {0}, Text: '{1}']".format(bad, text)) if self.ui and self.ui.is_on: self.ui.status.notify(" ".join(text.split("\n")), duration=duration, bad=bad) @@ -344,9 +347,10 @@ class Actions(FileManagerAware, SettingsAware): Load a config file. """ filename = os.path.expanduser(filename) + log.debug("Sourcing config file '{0}'".format(filename)) with open(filename, 'r') as f: for line in f: - line = line.lstrip().rstrip("\r\n") + line = line.strip(" \r\n") if line.startswith("#") or not line.strip(): continue try: @@ -419,7 +423,7 @@ class Actions(FileManagerAware, SettingsAware): Example: self.move(down=4, pages=True) # moves down by 4 pages. self.move(to=2, pages=True) # moves to page 2. - self.move(to=1, percentage=True) # moves to 80% + self.move(to=80, percentage=True) # moves to 80% """ cwd = self.thisdir direction = Direction(kw) @@ -869,11 +873,13 @@ class Actions(FileManagerAware, SettingsAware): self.notify("Could not find manpage.", bad=True) def display_log(self): + logs = list(self.get_log()) pager = self.ui.open_pager() - if self.log: - pager.set_source(["Message Log:"] + list(self.log)) + if logs: + pager.set_source(["Message Log:"] + logs) else: pager.set_source(["Message Log:", "No messages!"]) + pager.move(to=100, percentage=True) def display_file(self): if not self.thisfile or not self.thisfile.is_file: diff --git a/ranger/core/fm.py b/ranger/core/fm.py index 523239c1..5d630464 100644 --- a/ranger/core/fm.py +++ b/ranger/core/fm.py @@ -5,6 +5,7 @@ from time import time from collections import deque +import logging import mimetypes import os.path import pwd @@ -15,6 +16,7 @@ import sys import ranger.api from ranger.core.actions import Actions from ranger.core.tab import Tab +from ranger.container import settings from ranger.container.tags import Tags, TagsDummy from ranger.gui.ui import UI from ranger.container.bookmarks import Bookmarks @@ -24,8 +26,10 @@ from ranger.core.metadata import MetadataManager from ranger.ext.rifle import Rifle from ranger.container.directory import Directory from ranger.ext.signals import SignalDispatcher -from ranger import __version__ from ranger.core.loader import Loader +from ranger.ext import logutils + +log = logging.getLogger(__name__) class FM(Actions, SignalDispatcher): @@ -49,7 +53,6 @@ class FM(Actions, SignalDispatcher): self.ui = ui self.start_paths = paths self.directories = dict() - self.log = deque(maxlen=20) self.bookmarks = bookmarks self.current_tab = 1 self.tabs = {} @@ -70,10 +73,6 @@ class FM(Actions, SignalDispatcher): self.hostname = socket.gethostname() self.home_path = os.path.expanduser('~') - self.log.append('ranger {0} started! Process ID is {1}.' - .format(__version__, os.getpid())) - self.log.append('Running on Python ' + sys.version.replace('\n', '')) - mimetypes.knownfiles.append(os.path.expanduser('~/.mime.types')) mimetypes.knownfiles.append(self.relpath('data/mime.types')) self.mimetypes = mimetypes.MimeTypes() @@ -97,7 +96,13 @@ class FM(Actions, SignalDispatcher): rifleconf = self.relpath('config/rifle.conf') self.rifle = Rifle(rifleconf) self.rifle.reload_config() - self.image_displayer = self._get_image_displayer() + + def set_image_displayer(): + self.image_displayer = self._get_image_displayer() + set_image_displayer() + self.settings.signal_bind('setopt.preview_images_method', + set_image_displayer, + priority=settings.SIGNAL_PRIORITY_AFTER_SYNC) if not ranger.arg.clean and self.tags is None: self.tags = Tags(self.confpath('tagged')) @@ -198,11 +203,24 @@ class FM(Actions, SignalDispatcher): if debug: raise + def get_log(self): + """Return the current log + + The log is returned as a list of string + """ + for log in logutils.log_queue: + for line in log.split('\n'): + yield line + def _get_image_displayer(self): if self.settings.preview_images_method == "w3m": return W3MImageDisplayer() elif self.settings.preview_images_method == "iterm2": return ITerm2ImageDisplayer() + elif self.settings.preview_images_method == "urxvt": + return URXVTImageDisplayer() + elif self.settings.preview_images_method == "urxvt-full": + return URXVTImageFSDisplayer() else: return ImageDisplayer() diff --git a/ranger/core/linemode.py b/ranger/core/linemode.py index 96557515..c7839723 100644 --- a/ranger/core/linemode.py +++ b/ranger/core/linemode.py @@ -7,6 +7,7 @@ import sys from abc import * from datetime import datetime from ranger.ext.human_readable import human_readable +from ranger.ext import spawn DEFAULT_LINEMODE = "filename" @@ -97,9 +98,9 @@ class FileInfoLinemode(LinemodeBase): def infostring(self, file, metadata): if not file.is_directory: - from subprocess import check_output, CalledProcessError + from subprocess import Popen, PIPE, CalledProcessError try: - fileinfo = check_output(["file", "-bL", file.path]).strip() + fileinfo = spawn.check_output(["file", "-bL", file.path]).strip() except CalledProcessError: return "unknown" if sys.version_info[0] >= 3: diff --git a/ranger/core/main.py b/ranger/core/main.py index 118a7480..b0e3b600 100644 --- a/ranger/core/main.py +++ b/ranger/core/main.py @@ -6,6 +6,10 @@ import os.path import sys import tempfile +from ranger import __version__ +from logging import getLogger + +log = getLogger(__name__) def main(): @@ -15,6 +19,14 @@ def main(): from ranger.container.settings import Settings from ranger.core.shared import FileManagerAware, SettingsAware from ranger.core.fm import FM + from ranger.ext.logutils import setup_logging + + ranger.arg = arg = parse_arguments() + setup_logging(debug=arg.debug, logfile=arg.logfile) + + log.info("Ranger version {0}".format(__version__)) + log.info('Running on Python ' + sys.version.replace('\n', '')) + log.info("Process ID is {0}".format(os.getpid())) try: locale.setlocale(locale.LC_ALL, '') @@ -31,7 +43,9 @@ def main(): if 'SHELL' not in os.environ: os.environ['SHELL'] = 'sh' - ranger.arg = arg = parse_arguments() + log.debug("config dir: '{0}'".format(arg.confdir)) + log.debug("cache dir: '{0}'".format(arg.cachedir)) + if arg.copy_config is not None: fm = FM() fm.copy_config_files(arg.copy_config) @@ -73,8 +87,6 @@ def main(): "deprecated.\nPlease use the standalone file launcher " "'rifle' instead.\n") - def print_function(string): - print(string) from ranger.ext.rifle import Rifle fm = FM() if not arg.clean and os.path.isfile(fm.confpath('rifle.conf')): @@ -112,7 +124,7 @@ def main(): if fm.username == 'root': fm.settings.preview_files = False fm.settings.use_preview_script = False - fm.log.append("Running as root, disabling the file previews.") + log.info("Running as root, disabling the file previews.") if not arg.debug: from ranger.ext import curses_interrupt_handler curses_interrupt_handler.install_interrupt_handler() @@ -199,6 +211,8 @@ def parse_arguments(): help="activate debug mode") parser.add_option('-c', '--clean', action='store_true', help="don't touch/require any config files. ") + parser.add_option('--logfile', type='string', metavar='file', + help="log file to use, '-' for stderr") parser.add_option('-r', '--confdir', type='string', metavar='dir', default=default_confdir, help="change the configuration directory. (%default)") @@ -265,14 +279,18 @@ def load_settings(fm, clean): allow_access_to_confdir(ranger.arg.confdir, True) # Load custom commands - if os.path.exists(fm.confpath('commands.py')): + custom_comm_path = fm.confpath('commands.py') + if os.path.exists(custom_comm_path): old_bytecode_setting = sys.dont_write_bytecode sys.dont_write_bytecode = True try: import commands fm.commands.load_commands_from_module(commands) - except ImportError: - pass + except ImportError as e: + log.debug("Failed to import custom commands from '{0}'".format(custom_comm_path)) + log.exception(e) + else: + log.debug("Loaded custom commands from '{0}'".format(custom_comm_path)) sys.dont_write_bytecode = old_bytecode_setting allow_access_to_confdir(ranger.arg.confdir, False) @@ -297,6 +315,7 @@ def load_settings(fm, clean): pass else: if not os.path.exists(fm.confpath('plugins', '__init__.py')): + log.debug("Creating missing '__init__.py' file in plugin folder") f = open(fm.confpath('plugins', '__init__.py'), 'w') f.close() @@ -313,12 +332,12 @@ def load_settings(fm, clean): else: module = importlib.import_module('plugins.' + plugin) fm.commands.load_commands_from_module(module) - fm.log.append("Loaded plugin '%s'." % plugin) + log.debug("Loaded plugin '{0}'".format(plugin)) except Exception as e: - fm.log.append("Error in plugin '%s'" % plugin) - import traceback - for line in traceback.format_exception_only(type(e), e): - fm.log.append(line) + mex = "Error while loading plugin '{0}'".format(plugin) + log.error(mex) + log.exception(e) + fm.notify(mex, bad=True) ranger.fm = None # COMPAT: Load the outdated options.py @@ -361,6 +380,8 @@ def allow_access_to_confdir(confdir, allow): print("To run ranger without the need for configuration") print("files, use the --clean option.") raise SystemExit() + else: + log.debug("Created config directory '{0}'".format(confdir)) if confdir not in sys.path: sys.path[0:0] = [confdir] else: diff --git a/ranger/core/runner.py b/ranger/core/runner.py index 6d03f8fb..78d52764 100644 --- a/ranger/core/runner.py +++ b/ranger/core/runner.py @@ -25,7 +25,7 @@ t: run application in a new terminal window import os import sys from subprocess import Popen, PIPE -from ranger.ext.get_executables import get_executables +from ranger.ext.get_executables import get_executables, get_term from ranger.ext.popen_forked import Popen_forked @@ -194,11 +194,7 @@ class Runner(object): if 't' in context.flags: if 'DISPLAY' not in os.environ: return self._log("Can not run with 't' flag, no display found!") - term = os.environ.get('TERMCMD', os.environ.get('TERM')) - if term not in get_executables(): - term = 'x-terminal-emulator' - if term not in get_executables(): - term = 'xterm' + term = get_term() if isinstance(action, str): action = term + ' -e ' + action else: diff --git a/ranger/data/scope.sh b/ranger/data/scope.sh index 669d1e34..a0fb2ec0 100755 --- a/ranger/data/scope.sh +++ b/ranger/data/scope.sh @@ -65,14 +65,18 @@ fi case "$extension" in # Archive extensions: - 7z|a|ace|alz|arc|arj|bz|bz2|cab|cpio|deb|gz|jar|lha|lz|lzh|lzma|lzo|\ + a|ace|alz|arc|arj|bz|bz2|cab|cpio|deb|gz|jar|lha|lz|lzh|lzma|lzo|\ rpm|rz|t7z|tar|tbz|tbz2|tgz|tlz|txz|tZ|tzo|war|xpi|xz|Z|zip) try als "$path" && { dump | trim; exit 0; } try acat "$path" && { dump | trim; exit 3; } try bsdtar -lf "$path" && { dump | trim; exit 0; } exit 1;; rar) + # avoid password prompt by providing empty password try unrar -p- lt "$path" && { dump | trim; exit 0; } || exit 1;; + 7z) + # avoid password prompt by providing empty password + try 7z -p l "$path" && { dump | trim; exit 0; } || exit 1;; # PDF documents: pdf) try pdftotext -l 10 -nopgbrk -q "$path" - && \ @@ -80,6 +84,9 @@ case "$extension" in # BitTorrent Files torrent) try transmission-show "$path" && { dump | trim; exit 5; } || exit 1;; + # ODT Files + odt|ods|odp|sxw) + try odt2txt "$path" && { dump | trim; exit 5; } || exit 1;; # HTML Pages: htm|html|xhtml) try w3m -dump "$path" && { dump | trim | fmt -s -w $width; exit 4; } diff --git a/ranger/ext/get_executables.py b/ranger/ext/get_executables.py index ee988e49..f2a31345 100644 --- a/ranger/ext/get_executables.py +++ b/ranger/ext/get_executables.py @@ -4,6 +4,7 @@ from stat import S_IXOTH, S_IFREG from ranger.ext.iter_tools import unique from os import listdir, environ, stat +import shlex _cached_executables = None @@ -44,3 +45,16 @@ def get_executables_uncached(*paths): if filestat.st_mode & (S_IXOTH | S_IFREG): executables.add(item) return executables + + +def get_term(): + """Get the user terminal executable name. + + Either $TERMCMD, $TERM, "x-terminal-emulator" or "xterm", in this order. + """ + command = environ.get('TERMCMD', environ.get('TERM')) + if shlex.split(command)[0] not in get_executables(): + command = 'x-terminal-emulator' + if command not in get_executables(): + command = 'xterm' + return command diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py index ada7b31c..0e6b08fe 100644 --- a/ranger/ext/img_display.py +++ b/ranger/ext/img_display.py @@ -6,7 +6,7 @@ """Interface for drawing images into the console This module provides functions to draw images in the terminal using supported -implementations, which are currently w3m and iTerm2. +implementations, which are currently w3m, iTerm2 and urxvt. """ import base64 @@ -295,3 +295,79 @@ class ITerm2ImageDisplayer(ImageDisplayer, FileManagerAware): return 0, 0 file_handle.close() return width, height + + +class URXVTImageDisplayer(ImageDisplayer, FileManagerAware): + """Implementation of ImageDisplayer working by setting the urxvt + background image "under" the preview pane. + + Ranger must be running in urxvt for this to work. + + """ + + def _get_max_sizes(self): + """Use the whole terminal.""" + w = 100 + h = 100 + return w, h + + def _get_centered_offsets(self): + """Center the image.""" + x = 50 + y = 50 + return x, y + + def _get_sizes(self): + """Return the width and height of the preview pane in relation to the + whole terminal window. + + """ + if self.fm.ui.pager.visible: + return self._get_max_sizes() + + total_columns_ratio = sum(self.fm.settings.column_ratios) + preview_column_ratio = self.fm.settings.column_ratios[-1] + w = int((100 * preview_column_ratio) / total_columns_ratio) + h = 100 # As much as possible while preserving the aspect ratio. + return w, h + + def _get_offsets(self): + """Return the offsets of the image center.""" + if self.fm.ui.pager.visible: + return self._get_centered_offsets() + + x = 100 # Right-aligned. + y = 2 # TODO: Use the font size to calculate this offset. + return x, y + + def draw(self, path, start_x, start_y, width, height): + # The coordinates in the arguments are ignored as urxvt takes + # the coordinates in a non-standard way: the position of the + # image center as a percentage of the terminal size. As a + # result all values below are in percents. + + x, y = self._get_offsets() + w, h = self._get_sizes() + + sys.stdout.write("\033]20;{path};{w}x{h}+{x}+{y}:op=keep-aspect\a".format(**vars())) + sys.stdout.flush() + + def clear(self, start_x, start_y, width, height): + sys.stdout.write("\033]20;;100x100+1000+1000\a") + sys.stdout.flush() + + def quit(self): + sys.stdout.write("\033]20;;100x100+1000+1000\a") + sys.stdout.flush() + + +class URXVTImageFSDisplayer(URXVTImageDisplayer): + """URXVTImageDisplayer that utilizes the whole terminal.""" + + def _get_sizes(self): + """Use the whole terminal.""" + return self._get_max_sizes() + + def _get_offsets(self): + """Center the image.""" + return self._get_centered_offsets() diff --git a/ranger/ext/logutils.py b/ranger/ext/logutils.py new file mode 100644 index 00000000..0de6c333 --- /dev/null +++ b/ranger/ext/logutils.py @@ -0,0 +1,78 @@ +import logging +from collections import deque + +LOG_FORMAT = "[%(levelname)s] %(message)s" +LOG_FORMAT_EXT = "%(asctime)s,%(msecs)d [%(name)s] |%(levelname)s| %(message)s" +LOG_DATA_FORMAT = "%H:%M:%S" + + +class QueueHandler(logging.Handler): + """ + This handler store logs events into a queue. + """ + + def __init__(self, queue): + """ + Initialize an instance, using the passed queue. + """ + logging.Handler.__init__(self) + self.queue = queue + + def enqueue(self, record): + """ + Enqueue a log record. + """ + self.queue.append(record) + + def emit(self, record): + self.enqueue(self.format(record)) + + +log_queue = deque(maxlen=1000) +concise_formatter = logging.Formatter(fmt=LOG_FORMAT, datefmt=LOG_DATA_FORMAT) +extended_formatter = logging.Formatter(fmt=LOG_FORMAT_EXT, datefmt=LOG_DATA_FORMAT) + + +def setup_logging(debug=True, logfile=None): + """ + All the produced logs using the standard logging function + will be collected in a queue by the `queue_handler` as well + as outputted on the standard error `stderr_handler`. + + The verbosity and the format of the log message is + controlled by the `debug` parameter + + - debug=False: + a concise log format will be used, debug messsages will be discarded + and only important message will be passed to the `stderr_handler` + + - debug=True: + an extended log format will be used, all messages will be processed + by both the handlers + """ + root_logger = logging.getLogger() + + if debug: + # print all logging in extended format + log_level = logging.DEBUG + formatter = extended_formatter + else: + # print only warning and critical message + # in a concise format + log_level = logging.INFO + formatter = concise_formatter + + handlers = [] + handlers.append(QueueHandler(log_queue)) + if logfile: + if logfile is '-': + handlers.append(logging.StreamHandler()) + else: + handlers.append(logging.FileHandler(logfile)) + + for handler in handlers: + handler.setLevel(log_level) + handler.setFormatter(formatter) + root_logger.addHandler(handler) + + root_logger.setLevel(0) diff --git a/ranger/ext/shell_escape.py b/ranger/ext/shell_escape.py index fe542084..d57ff339 100644 --- a/ranger/ext/shell_escape.py +++ b/ranger/ext/shell_escape.py @@ -3,7 +3,7 @@ """Functions to escape metacharacters of arguments for shell commands.""" -META_CHARS = (' ', "'", '"', '`', '&', '|', ';', +META_CHARS = (' ', "'", '"', '`', '&', '|', ';', '#', '$', '!', '(', ')', '[', ']', '<', '>', '\t') UNESCAPABLE = set(map(chr, list(range(9)) + list(range(10, 32)) + list(range(127, 256)))) diff --git a/ranger/ext/spawn.py b/ranger/ext/spawn.py index 7c5c921c..77983341 100644 --- a/ranger/ext/spawn.py +++ b/ranger/ext/spawn.py @@ -1,18 +1,44 @@ # This file is part of ranger, the console file manager. # License: GNU GPL version 3, see the file "AUTHORS" for details. -from subprocess import Popen, PIPE +from os import devnull +from subprocess import Popen, PIPE, CalledProcessError ENCODING = 'utf-8' -def spawn(*args): - """Runs a program, waits for its termination and returns its stdout""" - if len(args) == 1: - popen_arguments = args[0] - shell = isinstance(popen_arguments, str) +def check_output(popenargs, **kwargs): + """Runs a program, waits for its termination and returns its output + + This function is functionally identical to python 2.7's subprocess.check_output, + but is favored due to python 2.6 compatibility. + + Will be run through shell if popenargs is a string, otherwise the command + is executed directly. + + The keyword argument `decode` determines if the output shall be decoded + with the encoding ENCODING. + + Further keyword arguments are passed to Popen. + """ + + do_decode = kwargs.pop('decode', True) + kwargs.setdefault('stdout', PIPE) + kwargs.setdefault('shell', isinstance(popenargs, str)) + + if 'stderr' in kwargs: + process = Popen(popenargs, **kwargs) + stdout, _ = process.communicate() else: - popen_arguments = args - shell = False - process = Popen(popen_arguments, stdout=PIPE, shell=shell) - stdout, stderr = process.communicate() - return stdout.decode(ENCODING) + with open(devnull, mode='w') as fd_devnull: + process = Popen(popenargs, stderr=fd_devnull, **kwargs) + stdout, _ = process.communicate() + + if process.returncode != 0: + error = CalledProcessError(process.returncode, popenargs) + error.output = stdout + raise error + + if do_decode and stdout is not None: + stdout = stdout.decode(ENCODING) + + return stdout diff --git a/ranger/ext/vcs/bzr.py b/ranger/ext/vcs/bzr.py index 5decc8b1..d0db1faf 100644 --- a/ranger/ext/vcs/bzr.py +++ b/ranger/ext/vcs/bzr.py @@ -25,7 +25,7 @@ class Bzr(Vcs): def _remote_url(self): """Remote url""" try: - return self._run(['config', 'parent_location']).rstrip('\n') or None + return self._run(['config', 'parent_location']) or None except VcsError: return None @@ -87,29 +87,29 @@ class Bzr(Vcs): statuses = set() # Paths with status - output = self._run(['status', '--short', '--no-classify']).rstrip('\n') - if not output: + lines = self._run(['status', '--short', '--no-classify']).split('\n') + if not lines: return 'sync' - for line in output.split('\n'): + for line in lines: statuses.add(self._status_translate(line[:2])) for status in self.DIRSTATUSES: if status in statuses: return status + return 'sync' def data_status_subpaths(self): statuses = {} # Ignored - output = self._run(['ls', '--null', '--ignored']).rstrip('\x00') - if output: - for path in output.split('\x00'): - statuses[path] = 'ignored' + paths = self._run(['ls', '--null', '--ignored']).split('\0')[:-1] + for path in paths: + statuses[path] = 'ignored' # Paths with status - output = self._run(['status', '--short', '--no-classify']).rstrip('\n') - for line in output.split('\n'): + lines = self._run(['status', '--short', '--no-classify']).split('\n') + for line in lines: statuses[os.path.normpath(line[4:])] = self._status_translate(line[:2]) return statuses @@ -121,7 +121,7 @@ class Bzr(Vcs): def data_branch(self): try: - return self._run(['nick']).rstrip('\n') or None + return self._run(['nick']) or None except VcsError: return None diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py index 8f4d9ff8..06e066d2 100644 --- a/ranger/ext/vcs/git.py +++ b/ranger/ext/vcs/git.py @@ -4,13 +4,19 @@ """Git module""" from datetime import datetime -import json import os import re +import unicodedata from .vcs import Vcs, VcsError +def string_control_replace(string, replacement): + """Replace all unicode control characters with replacement""" + return ''.join( + (replacement if unicodedata.category(char)[0] == 'C' else char for char in string)) + + class Git(Vcs): """VCS implementation for Git""" _status_translations = ( @@ -30,26 +36,17 @@ class Git(Vcs): def _head_ref(self): """Returns HEAD reference""" - return self._run(['symbolic-ref', self.HEAD]).rstrip('\n') or None + return self._run(['symbolic-ref', self.HEAD]) or None def _remote_ref(self, ref): """Returns remote reference associated to given ref""" if ref is None: return None - return self._run(['for-each-ref', '--format=%(upstream)', ref]).rstrip('\n') or None + return self._run(['for-each-ref', '--format=%(upstream)', ref]) or None def _log(self, refspec=None, maxres=None, filelist=None): """Returns an array of dicts containing revision info for refspec""" - args = [ - '--no-pager', 'log', - '--pretty={' - '%x00short%x00:%x00%h%x00,' - '%x00revid%x00:%x00%H%x00,' - '%x00author%x00:%x00%an <%ae>%x00,' - '%x00date%x00:%ct,' - '%x00summary%x00:%x00%s%x00' - '}' - ] + args = ['--no-pager', 'log', '--pretty=%h%x00%H%x00%an <%ae>%x00%ct%x00%s%x00%x00'] if refspec: args += ['-1', refspec] elif maxres: @@ -58,18 +55,22 @@ class Git(Vcs): args += ['--'] + filelist try: - output = self._run(args).rstrip('\n') + output = self._run(args) except VcsError: return None if not output: return None log = [] - for line in output\ - .replace('\\', '\\\\').replace('"', '\\"').replace('\x00', '"').split('\n'): - line = json.loads(line) - line['date'] = datetime.fromtimestamp(line['date']) - log.append(line) + for line in output[:-2].split('\0\0\n'): + commit_hash_abbrev, commit_hash, author, timestamp, subject = line.split('\0') + log.append({ + 'short': commit_hash_abbrev, + 'revid': commit_hash, + 'author': string_control_replace(author, ' '), + 'date': datetime.fromtimestamp(int(timestamp)), + 'summary': string_control_replace(subject, ' '), + }) return log def _status_translate(self, code): @@ -100,10 +101,10 @@ class Git(Vcs): # Paths with status skip = False - output = self._run(['status', '--porcelain', '-z']).rstrip('\x00') - if not output: + lines = self._run(['status', '--porcelain', '-z']).split('\0')[:-1] + if not lines: return 'sync' - for line in output.split('\x00'): + for line in lines: if skip: skip = False continue @@ -114,39 +115,37 @@ class Git(Vcs): for status in self.DIRSTATUSES: if status in statuses: return status + return 'sync' def data_status_subpaths(self): statuses = {} # Ignored directories - output = self._run([ + paths = self._run([ 'ls-files', '-z', '--others', '--directory', '--ignored', '--exclude-standard' - ]).rstrip('\x00') - if output: - for path in output.split('\x00'): - if path.endswith('/'): - statuses[os.path.normpath(path)] = 'ignored' + ]).split('\0')[:-1] + for path in paths: + if path.endswith('/'): + statuses[os.path.normpath(path)] = 'ignored' # Empty directories - output = self._run( - ['ls-files', '-z', '--others', '--directory', '--exclude-standard']).rstrip('\x00') - if output: - for path in output.split('\x00'): - if path.endswith('/'): - statuses[os.path.normpath(path)] = 'none' + paths = self._run( + ['ls-files', '-z', '--others', '--directory', '--exclude-standard']).split('\0')[:-1] + for path in paths: + if path.endswith('/'): + statuses[os.path.normpath(path)] = 'none' # Paths with status - output = self._run(['status', '--porcelain', '-z', '--ignored']).rstrip('\x00') - if output: - skip = False - for line in output.split('\x00'): - if skip: - skip = False - continue - statuses[os.path.normpath(line[3:])] = self._status_translate(line[:2]) - if line.startswith('R'): - skip = True + lines = self._run(['status', '--porcelain', '-z', '--ignored']).split('\0')[:-1] + skip = False + for line in lines: + if skip: + skip = False + continue + statuses[os.path.normpath(line[3:])] = self._status_translate(line[:2]) + if line.startswith('R'): + skip = True return statuses diff --git a/ranger/ext/vcs/hg.py b/ranger/ext/vcs/hg.py index cf15e35e..21460975 100644 --- a/ranger/ext/vcs/hg.py +++ b/ranger/ext/vcs/hg.py @@ -36,7 +36,7 @@ class Hg(Vcs): args += ['--'] + filelist try: - output = self._run(args).rstrip('\n') + output = self._run(args) except VcsError: return None if not output: @@ -56,13 +56,13 @@ class Hg(Vcs): def _remote_url(self): """Remote url""" try: - return self._run(['showconfig', 'paths.default']).rstrip('\n') or None + return self._run(['showconfig', 'paths.default']) or None except VcsError: return None def _status_translate(self, code): """Translate status code""" - for code_x, status in self._status_translations: # pylint: disable=invalid-name + for code_x, status in self._status_translations: if code in code_x: return status return 'unknown' @@ -80,7 +80,7 @@ class Hg(Vcs): if filelist: args += filelist else: - args += self.rootvcs.status_subpaths.keys() + args += self.rootvcs.status_subpaths.keys() # pylint: disable=no-member self._run(args, catchout=False) # Data interface @@ -117,7 +117,7 @@ class Hg(Vcs): return 'unknown' def data_branch(self): - return self._run(['branch']).rstrip('\n') or None + return self._run(['branch']) or None def data_info(self, rev=None): if rev is None: diff --git a/ranger/ext/vcs/svn.py b/ranger/ext/vcs/svn.py index 1813f857..1f09cd52 100644 --- a/ranger/ext/vcs/svn.py +++ b/ranger/ext/vcs/svn.py @@ -36,7 +36,7 @@ class SVN(Vcs): args += ['--'] + filelist try: - output = self._run(args).rstrip('\n') + output = self._run(args) except VcsError: return None if not output: @@ -66,7 +66,7 @@ class SVN(Vcs): def _remote_url(self): """Remote url""" try: - output = self._run(['info', '--xml']).rstrip('\n') + output = self._run(['info', '--xml']) except VcsError: return None if not output: @@ -86,7 +86,7 @@ class SVN(Vcs): if filelist: args += filelist else: - args += self.rootvcs.status_subpaths.keys() + args += self.rootvcs.status_subpaths.keys() # pylint: disable=no-member self._run(args, catchout=False) # Data Interface @@ -95,10 +95,10 @@ class SVN(Vcs): statuses = set() # Paths with status - output = self._run(['status']).rstrip('\n') - if not output: + lines = self._run(['status']).split('\n') + if not lines: return 'sync' - for line in output.split('\n'): + for line in lines: code = line[0] if code == ' ': continue @@ -113,13 +113,12 @@ class SVN(Vcs): statuses = {} # Paths with status - output = self._run(['status']).rstrip('\n') - if output: - for line in output.split('\n'): - code, path = line[0], line[8:] - if code == ' ': - continue - statuses[os.path.normpath(path)] = self._status_translate(code) + lines = self._run(['status']).split('\n') + for line in lines: + code, path = line[0], line[8:] + if code == ' ': + continue + statuses[os.path.normpath(path)] = self._status_translate(code) return statuses diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 487c9405..fa00777a 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -7,6 +7,9 @@ import os import subprocess import threading import time +from logging import getLogger + +from ranger.ext import spawn # Python2 compatibility try: @@ -19,6 +22,9 @@ except NameError: FileNotFoundError = OSError # pylint: disable=redefined-builtin +LOG = getLogger(__name__) + + class VcsError(Exception): """VCS exception""" pass @@ -109,21 +115,24 @@ class Vcs(object): # pylint: disable=too-many-instance-attributes # Generic - def _run(self, args, path=None, catchout=True, retbytes=False): + 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 path is None: path = self.path - with open(os.devnull, 'w') as devnull: - try: - if catchout: - output = subprocess.check_output(cmd, cwd=path, stderr=devnull) - return output if retbytes else output.decode('UTF-8') - else: - subprocess.check_call(cmd, cwd=path, stdout=devnull, stderr=devnull) - except (subprocess.CalledProcessError, FileNotFoundError): - raise VcsError('{0:s}: {1:s}'.format(str(cmd), path)) + try: + if catchout: + output = spawn.check_output(cmd, cwd=path, decode=not retbytes) + if not retbytes and rstrip_newline and output.endswith('\n'): + return output[:-1] + return output + else: + with open(os.devnull, mode='w') as fd_devnull: + subprocess.check_call(cmd, cwd=path, stdout=fd_devnull, stderr=fd_devnull) + except (subprocess.CalledProcessError, FileNotFoundError): + raise VcsError('{0:s}: {1:s}'.format(str(cmd), path)) def _get_repotype(self, path): """Get type for path""" @@ -227,7 +236,9 @@ class VcsRoot(Vcs): # pylint: disable=abstract-method self.branch = self.data_branch() self.obj.vcsremotestatus = self.data_status_remote() self.obj.vcsstatus = self.data_status_root() - except VcsError: + except VcsError as error: + LOG.exception(error) + self.obj.fm.notify('VCS Exception: View log for more info', bad=True) return False self.rootinit = True return True @@ -240,7 +251,9 @@ class VcsRoot(Vcs): # pylint: disable=abstract-method self.status_subpaths = self.data_status_subpaths() self.obj.vcsremotestatus = self.data_status_remote() self.obj.vcsstatus = self._status_root() - except VcsError: + except VcsError as error: + LOG.exception(error) + self.obj.fm.notify('VCS Exception: View log for more info', bad=True) return False self.rootinit = True self.updatetime = time.time() @@ -453,15 +466,19 @@ class VcsThread(threading.Thread): # pylint: disable=too-many-instance-attribut self.paused.clear() self.awoken.clear() - self._queue_process() - - if self.redraw: - self.redraw = False - for column in self.ui.browser.columns: - if column.target and column.target.is_directory: - column.need_redraw = True - self.ui.status.need_redraw = True - self.ui.redraw() + try: + self._queue_process() + + if self.redraw: + self.redraw = False + for column in self.ui.browser.columns: + if column.target and column.target.is_directory: + column.need_redraw = True + self.ui.status.need_redraw = True + self.ui.redraw() + except Exception as error: # pylint: disable=broad-except + LOG.exception(error) + self.ui.fm.notify('VCS Exception: View log for more info', bad=True) def pause(self): """Pause thread""" diff --git a/ranger/gui/ansi.py b/ranger/gui/ansi.py index e761c379..091406e9 100644 --- a/ranger/gui/ansi.py +++ b/ranger/gui/ansi.py @@ -8,7 +8,7 @@ from ranger.gui import color import re ansi_re = re.compile('(\x1b' + r'\[\d*(?:;\d+)*?[a-zA-Z])') -codesplit_re = re.compile('38;5;(\d+);|48;5;(\d+);|(\d*);') +codesplit_re = re.compile(r'38;5;(\d+);|48;5;(\d+);|(\d*);') reset = '\x1b[0m' diff --git a/ranger/gui/displayable.py b/ranger/gui/displayable.py index 7b5aa954..62eb5300 100644 --- a/ranger/gui/displayable.py +++ b/ranger/gui/displayable.py @@ -211,6 +211,7 @@ class DisplayableContainer(Displayable): New methods: add_child(object) -- add the object to the container. + replace_child(old_obj, new_obj) -- replaces old object with new object. remove_child(object) -- remove the object from the container. New attributes: @@ -290,6 +291,11 @@ class DisplayableContainer(Displayable): self.container.append(obj) obj.parent = self + def replace_child(self, old_obj, new_obj): + """Replace the old object with the new instance in the container.""" + self.container[self.container.index(old_obj)] = new_obj + new_obj.parent = self + def remove_child(self, obj): """Remove the object from the container.""" try: diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py index 4c302e00..f10bc0f2 100644 --- a/ranger/gui/ui.py +++ b/ranger/gui/ui.py @@ -437,18 +437,18 @@ class UI(DisplayableContainer): if value in self.ALLOWED_VIEWMODES: if self._viewmode != value: self._viewmode = value - resize = False + new_browser = self._viewmode_to_class(value)(self.win) + if hasattr(self, 'browser'): old_size = self.browser.y, self.browser.x, self.browser.hei, self.browser.wid - self.remove_child(self.browser) + self.replace_child(self.browser, new_browser) self.browser.destroy() - resize = True + new_browser.resize(*old_size) + else: + self.add_child(new_browser) - self.browser = self._viewmode_to_class(value)(self.win) + self.browser = new_browser self.redraw_window() - self.add_child(self.browser) - if resize: - self.browser.resize(*old_size) else: raise ValueError("Attempting to set invalid viewmode `%s`, should " "be one of `%s`." % (value, "`, `".join(self.ALLOWED_VIEWMODES))) diff --git a/ranger/gui/widgets/pager.py b/ranger/gui/widgets/pager.py index 58b791cd..0c81079b 100644 --- a/ranger/gui/widgets/pager.py +++ b/ranger/gui/widgets/pager.py @@ -231,7 +231,7 @@ class Pager(Widget): line = ansi.char_slice(line, startx, self.wid) + ansi.reset else: line = line[startx:self.wid + startx] - yield line.rstrip() + yield line.rstrip().replace('\r\n', '\n') except IndexError: raise StopIteration i += 1 diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py index 4eb06692..0ac62d86 100644 --- a/ranger/gui/widgets/statusbar.py +++ b/ranger/gui/widgets/statusbar.py @@ -258,7 +258,7 @@ class StatusBar(Widget): right.add("', ", "space") if target.marked_items: - if len(target.marked_items) == len(target.files): + if len(target.marked_items) == target.size: right.add(human_readable(target.disk_usage, separator='')) else: sumsize = sum(f.size for f in target.marked_items if not @@ -290,7 +290,7 @@ class StatusBar(Widget): elif pos >= max_pos: right.add('Bot', base, 'bot') else: - right.add('{:0.0%}'.format(float(pos) / max_pos), + right.add('{0:0.0%}'.format(float(pos) / max_pos), base, 'percentage') else: right.add('0/0 All', base, 'all') |