diff options
32 files changed, 387 insertions, 113 deletions
diff --git a/.flake8 b/.flake8 index f5072c08..b77e4e6c 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,3 @@ [flake8] max-line-length = 99 -ignore = E221 +ignore = E221,W503 diff --git a/.gitignore b/.gitignore index 88c75b90..73ca85e6 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ /ranger_fm.egg-info /stuff/* + +.idea +.pytest_cache diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..36ad0a95 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +# Usage instructions: +# 1. "docker build -t ranger/ranger:latest ." +# 2. "docker run -it ranger/ranger" + +FROM debian + +RUN apt-get update && apt-get install -y ranger +ENTRYPOINT ["ranger"] diff --git a/README.md b/README.md index ef644ae6..8abb7265 100644 --- a/README.md +++ b/README.md @@ -75,10 +75,11 @@ 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` for pdf previews +* `pdftotext` or `mutool` 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`) +* `chardet` (Python package) for improved encoding detection of text files Installing diff --git a/doc/ranger.1 b/doc/ranger.1 index f362d263..1290cb58 100644 --- a/doc/ranger.1 +++ b/doc/ranger.1 @@ -1,4 +1,4 @@ -.\" Automatically generated by Pod::Man 4.07 (Pod::Simple 3.32) +.\" Automatically generated by Pod::Man 4.09 (Pod::Simple 3.35) .\" .\" Standard preamble: .\" ======================================================================== @@ -129,7 +129,7 @@ .\" ======================================================================== .\" .IX Title "RANGER 1" -.TH RANGER 1 "ranger-1.9.1" "05.03.2018" "ranger manual" +.TH RANGER 1 "ranger-1.9.1" "2018-05-14" "ranger manual" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l @@ -145,7 +145,7 @@ ranger \- visual file manager [\fB\-\-choosedir\fR=\fItarget\fR] [\fB\-\-selectfile\fR=\fIfilepath\fR] [\fB\-\-show\-only\-dirs\fR] [\fB\-\-list\-unused\-keys\fR] [\fB\-\-list\-tagged\-files\fR=\fItag\fR] -[\fB\-\-profile\fR] [\fB\-\-cmd\fR=\fIcommand\fR] [\fIpath\fR] +[\fB\-\-profile\fR] [\fB\-\-cmd\fR=\fIcommand\fR] [\fIpath ...\fR] .SH "DESCRIPTION" .IX Header "DESCRIPTION" ranger is a console file manager with \s-1VI\s0 key bindings. @@ -172,6 +172,12 @@ with other software. They are usually installed to 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" +.IX Header "POSITIONAL ARGUMENTS" +.IP "\fIpath ...\fR" 14 +.IX Item "path ..." +Each path will be opened in a tab and if the path is a file it will be selected. +Omitting this is equivalent to providing the current directory. .SH "OPTIONS" .IX Header "OPTIONS" .IP "\fB\-d\fR, \fB\-\-debug\fR" 14 @@ -225,7 +231,8 @@ Allows you to pick a directory with ranger. When you exit ranger, it will write the last visited directory into \fItargetfile\fR. .IP "\fB\-\-selectfile\fR=\fItargetfile\fR" 14 .IX Item "--selectfile=targetfile" -Open ranger with \fItargetfile\fR selected. +Open ranger with \fItargetfile\fR selected. This is a legacy option, superseded by +the behavior for the \s-1POSITIONAL ARGUMENTS.\s0 .IP "\fB\-\-show\-only\-dirs\fR" 14 .IX Item "--show-only-dirs" Display only the directories. May be used in conjunction with @@ -267,10 +274,10 @@ typing \fI"<tagname>\fR. By default, only text files are previewed, but you can enable external preview scripts by setting the option \f(CW\*(C`use_preview_script\*(C'\fR and \f(CW\*(C`preview_files\*(C'\fR to true. .PP -This default script is \fI~/.config/ranger/scope.sh\fR. It contains more +This default script is \fI\f(CI%rangerdir\fI/data/scope.sh\fR. It contains more documentation and calls to the programs \fIlynx\fR and \fIelinks\fR for html, \&\fIhighlight\fR for text/code, \fIimg2txt\fR for images, \fIatool\fR for archives, -\&\fIpdftotext\fR for PDFs and \fImediainfo\fR for video and audio files. +\&\fIpdftotext\fR or \fImutool\fR for PDFs and \fImediainfo\fR for video and audio files. .PP Install these programs (just the ones you need) and scope.sh will automatically use them. @@ -463,7 +470,7 @@ sample plugins in the \fI/usr/share/doc/ranger/examples/\fR directory, including hello-world plugin that describes this procedure. .SH "KEY BINDINGS" .IX Header "KEY BINDINGS" -Key bindings are defined in the file \fIranger/config/rc.conf\fR. Check this +Key bindings are defined in the file \fI\f(CI%rangerdir\fI/config/rc.conf\fR. Check this file for a list of all key bindings. You can copy it to your local configuration directory with the \-\-copy\-config=rc option. .PP @@ -599,7 +606,7 @@ Toggle the mark-status of all files .IP "V" 14 .IX Item "V" Starts the visual mode, which selects all files between the starting point and -the cursor until you press \s-1ESC. \s0 To unselect files in the same way, use \*(L"uV\*(R". +the cursor until you press \s-1ESC.\s0 To unselect files in the same way, use \*(L"uV\*(R". .IP "/" 14 Search for files in the current directory. .IP ":" 14 @@ -702,7 +709,7 @@ in ranger. .IP "automatically_count_files [bool]" 4 .IX Item "automatically_count_files [bool]" Should ranger count and display the number of files in each directory -as soon as it's visible? This gets slow with remote file sytems. Turning it +as soon as it's visible? This gets slow with remote file systems. Turning it off will still allow you to see the number of files after entering the directory. .IP "autosave_bookmarks [bool]" 4 @@ -764,6 +771,9 @@ Display the file size in the main column? .IP "display_size_in_status_bar [bool]" 4 .IX Item "display_size_in_status_bar [bool]" Display the file size in the status bar? +.IP "display_free_space_in_status_bar [bool]" 4 +.IX Item "display_free_space_in_status_bar [bool]" +Display the free disk space in the status bar? .IP "display_tags_in_all_columns [bool]" 4 .IX Item "display_tags_in_all_columns [bool]" Display tags in all columns? @@ -989,7 +999,7 @@ ranger. For your convenience, this is a list of the \*(L"public\*(R" commands i .Vb 10 \& alias [newcommand] [oldcommand] \& bulkrename -\& cd [directory] +\& cd [path] \& chain command1[; command2[; command3...]] \& chmod octal_number \& cmap key command @@ -1065,10 +1075,10 @@ renaming according to the changes you did in the file. .Sp This shell script is opened in an editor for you to review. After you close it, it will be executed. -.IP "cd [\fIdirectory\fR]" 2 -.IX Item "cd [directory]" -The cd command changes the directory. The command \f(CW\*(C`:cd \-\*(C'\fR is equivalent to -typing ``. +.IP "cd [\fIpath\fR]" 2 +.IX Item "cd [path]" +The cd command changes the directory. If path is a file, selects that file. +The command \f(CW\*(C`:cd \-\*(C'\fR is equivalent to typing ``. .IP "chain \fIcommand1\fR[; \fIcommand2\fR[; \fIcommand3\fR...]]" 2 .IX Item "chain command1[; command2[; command3...]]" Combines multiple commands into one, separated by semicolons. @@ -1425,6 +1435,9 @@ being bound despite the corresponding line being removed from the user's copy of the configuration file. This behavior may be disabled with an environment variable (see also: \fB\s-1ENVIRONMENT\s0\fR). Note: All other configuration files only read from one source; i.e. default \s-1OR\s0 user, not both. +\&\fIrc.conf\fR and \fIcommands.py\fR are additionally read from \fI/etc/ranger\fR if they +exist for system-wide configuration, user configuration overrides system +configuration which overrides the default configuration. .PP When starting ranger with the \fB\-\-clean\fR option, it will not access or create any of these files. diff --git a/doc/ranger.pod b/doc/ranger.pod index a4e782b3..4471a153 100644 --- a/doc/ranger.pod +++ b/doc/ranger.pod @@ -14,7 +14,7 @@ B<ranger> [B<--version>] [B<--help>] [B<--debug>] [B<--clean>] [B<--choosedir>=I<target>] [B<--selectfile>=I<filepath>] [B<--show-only-dirs>] [B<--list-unused-keys>] [B<--list-tagged-files>=I<tag>] -[B<--profile>] [B<--cmd>=I<command>] [I<path>] +[B<--profile>] [B<--cmd>=I<command>] [I<path ...>] @@ -53,6 +53,20 @@ The section I<LINKS> of this man page contains further resources. +=head1 POSITIONAL ARGUMENTS + +=over 14 + +=item I<path ...> + +Each path will be opened in a tab and if the path is a file it will be selected. +Omitting this is equivalent to providing the current directory. + +=back + + + + =head1 OPTIONS =over 14 @@ -117,7 +131,8 @@ write the last visited directory into I<targetfile>. =item B<--selectfile>=I<targetfile> -Open ranger with I<targetfile> selected. +Open ranger with I<targetfile> selected. This is a legacy option, superseded by +the behavior for the POSITIONAL ARGUMENTS. =item B<--show-only-dirs> @@ -174,10 +189,10 @@ typing I<"<tagnameE<gt>>. By default, only text files are previewed, but you can enable external preview scripts by setting the option C<use_preview_script> and C<preview_files> to true. -This default script is F<~/.config/ranger/scope.sh>. It contains more +This default script is F<%rangerdir/data/scope.sh>. It contains more documentation and calls to the programs I<lynx> and I<elinks> for html, I<highlight> for text/code, I<img2txt> for images, I<atool> for archives, -I<pdftotext> for PDFs and I<mediainfo> for video and audio files. +I<pdftotext> or I<mutool> for PDFs and I<mediainfo> for video and audio files. Install these programs (just the ones you need) and scope.sh will automatically use them. @@ -364,7 +379,7 @@ hello-world plugin that describes this procedure. =head1 KEY BINDINGS -Key bindings are defined in the file F<ranger/config/rc.conf>. Check this +Key bindings are defined in the file F<%rangerdir/config/rc.conf>. Check this file for a list of all key bindings. You can copy it to your local configuration directory with the --copy-config=rc option. @@ -678,7 +693,7 @@ in ranger. =item automatically_count_files [bool] Should ranger count and display the number of files in each directory -as soon as it's visible? This gets slow with remote file sytems. Turning it +as soon as it's visible? This gets slow with remote file systems. Turning it off will still allow you to see the number of files after entering the directory. @@ -752,6 +767,10 @@ Display the file size in the main column? Display the file size in the status bar? +=item display_free_space_in_status_bar [bool] + +Display the free disk space in the status bar? + =item display_tags_in_all_columns [bool] Display tags in all columns? @@ -1004,6 +1023,11 @@ Sets the view mode, which can be B<miller> to display the files in the traditional miller column view that shows multiple levels of the hierarchy, or B<multipane> to use multiple panes (one per tab) similar to midnight-commander. +=item w3m_delay [float] + +Delay in seconds before displaying an image with the w3m method. +Increase it in case of experiencing display corruption. + =item wrap_scroll [bool] Enable scroll wrapping - moving down while on the last item will wrap around to @@ -1026,7 +1050,7 @@ ranger. For your convenience, this is a list of the "public" commands including alias [newcommand] [oldcommand] bulkrename - cd [directory] + cd [path] chain command1[; command2[; command3...]] chmod octal_number cmap key command @@ -1106,10 +1130,10 @@ renaming according to the changes you did in the file. This shell script is opened in an editor for you to review. After you close it, it will be executed. -=item cd [I<directory>] +=item cd [I<path>] -The cd command changes the directory. The command C<:cd -> is equivalent to -typing ``. +The cd command changes the directory. If path is a file, selects that file. +The command C<:cd -> is equivalent to typing ``. =item chain I<command1>[; I<command2>[; I<command3>...]] @@ -1518,6 +1542,9 @@ being bound despite the corresponding line being removed from the user's copy of the configuration file. This behavior may be disabled with an environment variable (see also: B<ENVIRONMENT>). Note: All other configuration files only read from one source; i.e. default OR user, not both. +F<rc.conf> and F<commands.py> are additionally read from F</etc/ranger> if they +exist for system-wide configuration, user configuration overrides system +configuration which overrides the default configuration. When starting ranger with the B<--clean> option, it will not access or create any of these files. diff --git a/examples/plugin_avfs.py b/examples/plugin_avfs.py new file mode 100644 index 00000000..07968a03 --- /dev/null +++ b/examples/plugin_avfs.py @@ -0,0 +1,33 @@ +# Tested with ranger 1.9.1 +# +# A very simple and possibly buggy support for AVFS +# (http://avf.sourceforge.net/), that allows ranger to handle +# archives. +# +# Run `:avfs' to browse the selected archive. + +from __future__ import (absolute_import, division, print_function) + +import os +import os.path + +from ranger.api.commands import Command + + +class avfs(Command): # pylint: disable=invalid-name + avfs_root = os.path.join(os.environ["HOME"], ".avfs") + avfs_suffix = "#" + + def execute(self): + if os.path.isdir(self.avfs_root): + archive_directory = "".join([ + self.avfs_root, + self.fm.thisfile.path, + self.avfs_suffix, + ]) + if os.path.isdir(archive_directory): + self.fm.cd(archive_directory) + else: + self.fm.notify("This file cannot be handled by avfs.", bad=True) + else: + self.fm.notify("Install `avfs' and run `mountavfs' first.", bad=True) diff --git a/examples/rc_emacs.conf b/examples/rc_emacs.conf index 26074a42..0462282e 100644 --- a/examples/rc_emacs.conf +++ b/examples/rc_emacs.conf @@ -122,6 +122,9 @@ set mouse_enabled true set display_size_in_main_column true set display_size_in_status_bar true +# Display the free disk space in the status bar? +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 @@ -129,7 +132,7 @@ set display_tags_in_all_columns true set update_title false # Set the title to "ranger" in the tmux program? -set update_tmux_title false +set update_tmux_title true # Shorten the title if it gets long? The number defines how many # directories are displayed at once, 0 turns off this feature. diff --git a/ranger/colorschemes/default.py b/ranger/colorschemes/default.py index c88cdc7c..350c8359 100644 --- a/ranger/colorschemes/default.py +++ b/ranger/colorschemes/default.py @@ -138,6 +138,8 @@ class Default(ColorScheme): attr &= ~bold if context.vcsconflict: fg = magenta + elif context.vcsuntracked: + fg = cyan elif context.vcschanged: fg = red elif context.vcsunknown: diff --git a/ranger/config/__init__.py b/ranger/config/__init__.py index 71df3cb3..0facbdf8 100644 --- a/ranger/config/__init__.py +++ b/ranger/config/__init__.py @@ -1 +1 @@ -"""Default options and configration files""" +"""Default options and configuration files""" diff --git a/ranger/config/commands.py b/ranger/config/commands.py index a3837d8e..7f5fc764 100755 --- a/ranger/config/commands.py +++ b/ranger/config/commands.py @@ -3,8 +3,9 @@ # This configuration file is licensed under the same terms as ranger. # =================================================================== # -# NOTE: If you copied this file to ~/.config/ranger/commands_full.py, -# then it will NOT be loaded by ranger, and only serve as a reference. +# NOTE: If you copied this file to /etc/ranger/commands_full.py or +# ~/.config/ranger/commands_full.py, then it will NOT be loaded by ranger, +# and only serve as a reference. # # =================================================================== # This file contains ranger's commands. @@ -13,9 +14,14 @@ # Note that additional commands are automatically generated from the methods # of the class ranger.core.actions.Actions. # -# You can customize commands in the file ~/.config/ranger/commands.py. -# It has the same syntax as this file. In fact, you can just copy this -# file there with `ranger --copy-config=commands' and make your modifications. +# You can customize commands in the files /etc/ranger/commands.py (system-wide) +# and ~/.config/ranger/commands.py (per user). +# They have the same syntax as this file. In fact, you can just copy this +# file to ~/.config/ranger/commands_full.py with +# `ranger --copy-config=commands_full' and make your modifications, don't +# forget to rename it to commands.py. You can also use +# `ranger --copy-config=commands' to copy a short sample commands.py that +# has everything you need to get started. # But make sure you update your configs when you update ranger. # # =================================================================== @@ -120,9 +126,10 @@ class echo(Command): class cd(Command): - """:cd [-r] <dirname> + """:cd [-r] <path> The cd command changes the directory. + If the path is a file, selects that file. The command 'cd -' is equivalent to typing ``. Using the option "-r" will get you to the real path. """ @@ -1393,6 +1400,8 @@ class scout(Command): self.fm.cd(pattern) else: self.fm.move(right=1) + if self.quickly_executed: + self.fm.block_input(0.5) if self.KEEP_OPEN in flags and thisdir != self.fm.thisdir: # reopen the console: @@ -1718,6 +1727,7 @@ class yank(Command): modes = { '': 'basename', + 'name_without_extension': 'basename_without_extension', 'name': 'basename', 'dir': 'dirname', 'path': 'path', @@ -1750,7 +1760,9 @@ class yank(Command): clipboard_commands = clipboards() - selection = self.get_selection_attr(self.modes[self.arg(1)]) + mode = self.modes[self.arg(1)] + selection = self.get_selection_attr(mode) + new_clipboard_contents = "\n".join(selection) for command in clipboard_commands: process = subprocess.Popen(command, universal_newlines=True, diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf index 5a7a9a6d..1369bb27 100644 --- a/ranger/config/rc.conf +++ b/ranger/config/rc.conf @@ -1,7 +1,8 @@ # =================================================================== # This file contains the default startup commands for ranger. -# To change them, it is recommended to create the file -# ~/.config/ranger/rc.conf and add your custom commands there. +# To change them, it is recommended to create either /etc/ranger/rc.conf +# (system-wide) or ~/.config/ranger/rc.conf (per user) and add your custom +# commands there. # # If you copy this whole file there, you may want to set the environment # variable RANGER_LOAD_DEFAULT_RC to FALSE to avoid loading it twice. @@ -94,6 +95,10 @@ set preview_images false # whole terminal window. set preview_images_method w3m +# Delay in seconds before displaying an image with the w3m method. +# Increase it in case of experiencing display corruption. +set w3m_delay 0.02 + # Default iTerm2 font size (see: preview_images_method: iterm2) set iterm2_font_width 8 set iterm2_font_height 11 @@ -141,6 +146,9 @@ set mouse_enabled true set display_size_in_main_column true set display_size_in_status_bar true +# Display the free disk space in the status bar? +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 @@ -148,7 +156,7 @@ set display_tags_in_all_columns true set update_title false # Set the title to "ranger" in the tmux program? -set update_tmux_title false +set update_tmux_title true # Shorten the title if it gets long? The number defines how many # directories are displayed at once, 0 turns off this feature. @@ -257,6 +265,10 @@ set wrap_scroll false # directories, files and symlinks respectively. set global_inode_type_filter +# This setting allows to freeze the list of files to save I/O bandwidth. It +# should be 'false' during start-up, but you can toggle it by pressing F. +set freeze_files false + # =================================================================== # == Local Options # =================================================================== @@ -278,8 +290,8 @@ alias qall quitall alias qall! quitall! alias setl setlocal -alias filter scout -prt -alias find scout -aeit +alias filter scout -prts +alias find scout -aets alias mark scout -mr alias unmark scout -Mr alias search scout -rs @@ -382,6 +394,7 @@ map L history_go 1 map ] move_parent 1 map [ move_parent -1 map } traverse +map { traverse_backwards map ) jump_non map gh cd ~ @@ -393,6 +406,7 @@ map gL cd -r %f map go cd /opt map gv cd /var map gm cd /media +map gi eval fm.cd('/run/media/' + os.getenv('USER')) map gM cd /mnt map gs cd /srv map gp cd /tmp @@ -408,6 +422,7 @@ map dU shell -p du --max-depth=1 -h --apparent-size | sort -rh map yp yank path map yd yank dir map yn yank name +map y. yank name_without_extension # Filesystem Operations map = chmod @@ -511,6 +526,8 @@ map zc set collapse_preview! map zd set sort_directories_first! map zh set show_hidden! map <C-h> set show_hidden! +copymap <C-h> <backspace> +copymap <backspace> <backspace2> map zI set flushinput! map zi set preview_images! map zm set mouse_enabled! diff --git a/ranger/config/rifle.conf b/ranger/config/rifle.conf index e2653a76..b1a9bb71 100644 --- a/ranger/config/rifle.conf +++ b/ranger/config/rifle.conf @@ -151,7 +151,7 @@ ext pdf, has atril, X, flag f = atril -- "$@" ext pdf, has okular, X, flag f = okular -- "$@" ext pdf, has epdfview, X, flag f = epdfview -- "$@" ext pdf, has qpdfview, X, flag f = qpdfview "$@" -ext pdf, has open, X, flat f = open "$@" +ext pdf, has open, X, flag f = open "$@" ext docx?, has catdoc, terminal = catdoc -- "$@" | "$PAGER" @@ -183,6 +183,7 @@ mime ^image, has eog, X, flag f = eog -- "$@" mime ^image, has eom, X, flag f = eom -- "$@" mime ^image, has nomacs, X, flag f = nomacs -- "$@" mime ^image, has geeqie, X, flag f = geeqie -- "$@" +mime ^image, has gwenview, X, flag f = gwenview -- "$@" mime ^image, has gimp, X, flag f = gimp -- "$@" ext xcf, X, flag f = gimp -- "$@" diff --git a/ranger/container/directory.py b/ranger/container/directory.py index 18b1687c..e281e6c9 100644 --- a/ranger/container/directory.py +++ b/ranger/container/directory.py @@ -338,8 +338,9 @@ class Directory( # pylint: disable=too-many-instance-attributes,too-many-public dirlist = [ os.path.join("/", dirpath, d) for d in dirnames - if self.flat == -1 or - (dirpath.count(os.path.sep) - mypath.count(os.path.sep)) <= self.flat + if self.flat == -1 + or (dirpath.count(os.path.sep) + - mypath.count(os.path.sep)) <= self.flat ] filelist += dirlist filelist += [os.path.join("/", dirpath, f) for f in filenames] diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py index 0c9f70f6..37151ecf 100644 --- a/ranger/container/fsobject.py +++ b/ranger/container/fsobject.py @@ -6,7 +6,7 @@ from __future__ import (absolute_import, division, print_function) import re from grp import getgrgid from os import lstat, stat -from os.path import abspath, basename, dirname, realpath, relpath +from os.path import abspath, basename, dirname, realpath, relpath, splitext from pwd import getpwuid from time import time @@ -171,6 +171,10 @@ class FileSystemObject( # pylint: disable=too-many-instance-attributes,too-many return basename_list @lazy_property + def basename_without_extension(self): + return splitext(self.basename)[0] + + @lazy_property def safe_basename(self): return self.basename.translate(_SAFE_STRING_TABLE) diff --git a/ranger/container/settings.py b/ranger/container/settings.py index 9603401f..d0ff2f5e 100644 --- a/ranger/container/settings.py +++ b/ranger/container/settings.py @@ -39,6 +39,7 @@ ALLOWED_SETTINGS = { 'dirname_in_tabs': bool, 'display_size_in_main_column': bool, 'display_size_in_status_bar': bool, + "display_free_space_in_status_bar": bool, 'display_tags_in_all_columns': bool, 'draw_borders': bool, 'draw_progress_bar_in_status_bar': bool, @@ -91,6 +92,7 @@ ALLOWED_SETTINGS = { 'vcs_backend_hg': str, 'vcs_backend_svn': str, 'viewmode': str, + 'w3m_delay': float, 'wrap_scroll': bool, 'xterm_alt_key': bool, } @@ -113,6 +115,7 @@ DEFAULT_VALUES = { type(None): None, str: "", int: 0, + float: 0.0, list: [], tuple: tuple([]), } diff --git a/ranger/core/actions.py b/ranger/core/actions.py index 31fa9518..ae8e33d4 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -7,7 +7,7 @@ from __future__ import (absolute_import, division, print_function) import codecs import os -from os import link, symlink, getcwd, listdir, stat +from os import link, symlink, listdir, stat from os.path import join, isdir, realpath, exists import re import shlex @@ -110,7 +110,8 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m self.settings.set(option_name, self._parse_option_value(option_name, value), localpath, tags) - def _parse_option_value(self, name, value): + def _parse_option_value( # pylint: disable=too-many-return-statements + self, name, value): types = self.fm.settings.types_of(name) if bool in types: if value.lower() in ('false', 'off', '0'): @@ -124,6 +125,11 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m return int(value) except ValueError: pass + if float in types: + try: + return float(value) + except ValueError: + pass if str in types: return value if list in types: @@ -409,7 +415,8 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m # ranger can act as a file chooser when running with --choosefile=... if mode == 0 and 'label' not in kw: if ranger.args.choosefile: - open(ranger.args.choosefile, 'w').write(self.fm.thisfile.path) + with open(ranger.args.choosefile, 'w') as fobj: + fobj.write(self.fm.thisfile.path) if ranger.args.choosefiles: paths = [] @@ -605,6 +612,23 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m self.move(down=1) self.traverse() + def traverse_backwards(self): + self.change_mode('normal') + if self.thisdir.pointer == 0: + self.move(left=1) + if self.thisdir.pointer != 0: + self.traverse_backwards() + else: + self.move(up=1) + while True: + if self.thisfile is not None and self.thisfile.is_directory: + self.enter_dir(self.thisfile.path) + self.move(to=100, percentage=True) + elif self.thisdir.pointer == 0: + break + else: + self.move(up=1) + # -------------------------- # -- Shortcuts / Wrappers # -------------------------- @@ -978,6 +1002,7 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m if not self.settings.preview_script or not self.settings.use_preview_script: try: + # XXX: properly determine file's encoding return codecs.open(path, 'r', errors='ignore') # IOError for Python2, OSError for Python3 except (IOError, OSError): @@ -1063,14 +1088,7 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m data[(-1, -1)] = None data['foundpreview'] = False elif rcode == 2: - fobj = codecs.open(path, 'r', errors='ignore') - try: - data[(-1, -1)] = fobj.read(1024 * 32) - except UnicodeDecodeError: - fobj.close() - fobj = codecs.open(path, 'r', encoding='latin-1', errors='ignore') - data[(-1, -1)] = fobj.read(1024 * 32) - fobj.close() + data[(-1, -1)] = self.read_text_file(path, 1024 * 32) else: data[(-1, -1)] = None @@ -1111,6 +1129,35 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m return None + @staticmethod + def read_text_file(path, count=None): + """Encoding-aware reading of a text file.""" + try: + import chardet + except ImportError: + # Guess encoding ourselves. These should be the most frequently used ones. + encodings = ('utf-8', 'utf-16') + for encoding in encodings: + try: + with codecs.open(path, 'r', encoding=encoding) as fobj: + text = fobj.read(count) + except UnicodeDecodeError: + pass + else: + LOG.debug("guessed encoding of '%s' as %r", path, encoding) + return text + else: + with open(path, 'rb') as fobj: + data = fobj.read(count) + result = chardet.detect(data) + LOG.debug("chardet guess for '%s': %s", path, result) + guessed_encoding = result['encoding'] + return codecs.decode(data, guessed_encoding, 'replace') + + # latin-1 as the last resort + with codecs.open(path, 'r', encoding='latin-1', errors='replace') as fobj: + return fobj.read(count) + # -------------------------- # -- Tabs # -------------------------- @@ -1186,10 +1233,10 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m def tab_new(self, path=None, narg=None): if narg: return self.tab_open(narg, path) - for i in range(1, 10): - if i not in self.tabs: - return self.tab_open(i, path) - return None + i = 1 + while i in self.tabs: + i += 1 + return self.tab_open(i, path) def tab_switch(self, path, create_directory=False): """Switches to tab of given path, opening a new tab as necessary. @@ -1237,7 +1284,18 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m def get_tab_list(self): assert self.tabs, "There must be at least 1 tab at all times" - return sorted(self.tabs) + + class NaturalOrder(object): # pylint: disable=too-few-public-methods + def __init__(self, obj): + self.obj = obj + + def __lt__(self, other): + try: + return self.obj < other.obj + except TypeError: + return str(self.obj) < str(other.obj) + + return sorted(self.tabs, key=NaturalOrder) # -------------------------- # -- Overview of internals @@ -1373,9 +1431,9 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m self.notify(new_name) try: if relative: - relative_symlink(fobj.path, join(getcwd(), new_name)) + relative_symlink(fobj.path, join(self.fm.thisdir.path, new_name)) else: - symlink(fobj.path, join(getcwd(), new_name)) + symlink(fobj.path, join(self.fm.thisdir.path, new_name)) except OSError as ex: self.notify('Failed to paste symlink: View log for more info', bad=True, exception=ex) @@ -1384,7 +1442,7 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m for fobj in self.copy_buffer: new_name = next_available_filename(fobj.basename) try: - link(fobj.path, join(getcwd(), new_name)) + link(fobj.path, join(self.fm.thisdir.path, new_name)) except OSError as ex: self.notify('Failed to paste hardlink: View log for more info', bad=True, exception=ex) @@ -1392,7 +1450,7 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m def paste_hardlinked_subtree(self): for fobj in self.copy_buffer: try: - target_path = join(getcwd(), fobj.basename) + target_path = join(self.fm.thisdir.path, fobj.basename) self._recurse_hardlinked_tree(fobj.path, target_path) except OSError as ex: self.notify('Failed to paste hardlinked subtree: View log for more info', diff --git a/ranger/core/fm.py b/ranger/core/fm.py index c55a3922..d85dd48c 100644 --- a/ranger/core/fm.py +++ b/ranger/core/fm.py @@ -424,5 +424,5 @@ class FM(Actions, # pylint: disable=too-many-instance-attributes if not ranger.args.clean and self.settings.save_tabs_on_exit and len(self.tabs) > 1: with open(self.datapath('tabs'), 'a') as fobj: # Don't save active tab since launching ranger changes the active tab - fobj.write('\0'.join(v.path for t, v in self.tabs.items() - if t != self.current_tab) + '\0\0') + fobj.write('\0'.join(v.path for t, v in self.tabs.items()) + + '\0\0') diff --git a/ranger/core/main.py b/ranger/core/main.py index 4adea918..b5d0af77 100644 --- a/ranger/core/main.py +++ b/ranger/core/main.py @@ -89,14 +89,12 @@ def main( SettingsAware.settings_set(Settings()) + # TODO: deprecate --selectfile if args.selectfile: args.selectfile = os.path.abspath(args.selectfile) args.paths.insert(0, os.path.dirname(args.selectfile)) - if args.paths: - paths = [p[7:] if p.startswith('file:///') else p for p in args.paths] - else: - paths = [os.environ.get('PWD', os.getcwd())] + paths = get_paths(args) paths_inaccessible = [] for path in paths: try: @@ -182,6 +180,7 @@ def main( fm.select_file(args.selectfile) if args.cmd: + fm.enter_dir(fm.thistab.path) for command in args.cmd: fm.execute_console(command) @@ -235,6 +234,24 @@ https://github.com/ranger/ranger/issues return exit_code # pylint: disable=lost-exception +def get_paths(args): + if args.paths: + prefix = 'file:///' + prefix_length = len(prefix) + paths = [path[prefix_length:] if path.startswith(prefix) else path for path in args.paths] + else: + start_directory = os.environ.get('PWD') + is_valid_start_directory = start_directory and os.path.exists(start_directory) + if not is_valid_start_directory: + start_directory = __get_home_directory() + paths = [start_directory] + return paths + + +def __get_home_directory(): + return os.path.expanduser('~') + + def xdg_path(env_var): path = os.environ.get(env_var) if path and os.path.isabs(path): @@ -339,23 +356,50 @@ def load_settings( # pylint: disable=too-many-locals,too-many-branches,too-many fm.commands.load_commands_from_module(commands_default) if not clean: + system_confdir = os.path.join(os.sep, 'etc', 'ranger') + if os.path.exists(system_confdir): + sys.path.append(system_confdir) allow_access_to_confdir(ranger.args.confdir, True) # Load custom commands - custom_comm_path = fm.confpath('commands.py') - if os.path.exists(custom_comm_path): + def import_file(name, path): # From https://stackoverflow.com/a/67692 + # pragma pylint: disable=no-name-in-module,import-error,no-member + if sys.version_info >= (3, 5): + import importlib.util as util + spec = util.spec_from_file_location(name, path) + module = util.module_from_spec(spec) + spec.loader.exec_module(module) + elif (3, 3) <= sys.version_info < (3, 5): + from importlib.machinery import SourceFileLoader + module = SourceFileLoader(name, path).load_module() + else: + import imp + module = imp.load_source(name, path) + # pragma pylint: enable=no-name-in-module,import-error,no-member + return module + + def load_custom_commands(*paths): old_bytecode_setting = sys.dont_write_bytecode sys.dont_write_bytecode = True - try: - import commands as commands_custom - fm.commands.load_commands_from_module(commands_custom) - except ImportError as ex: - LOG.debug("Failed to import custom commands from '%s'", custom_comm_path) - LOG.exception(ex) - else: - LOG.debug("Loaded custom commands from '%s'", custom_comm_path) + for custom_comm_path in paths: + if os.path.exists(custom_comm_path): + try: + commands_custom = import_file('commands', + custom_comm_path) + fm.commands.load_commands_from_module(commands_custom) + except ImportError as ex: + LOG.debug("Failed to import custom commands from '%s'", + custom_comm_path) + LOG.exception(ex) + else: + LOG.debug("Loaded custom commands from '%s'", + custom_comm_path) sys.dont_write_bytecode = old_bytecode_setting + system_comm_path = os.path.join(system_confdir, 'commands.py') + custom_comm_path = fm.confpath('commands.py') + load_custom_commands(system_comm_path, custom_comm_path) + # XXX Load plugins (experimental) plugindir = fm.confpath('plugins') try: @@ -394,12 +438,16 @@ def load_settings( # pylint: disable=too-many-locals,too-many-branches,too-many allow_access_to_confdir(ranger.args.confdir, False) # Load rc.conf custom_conf = fm.confpath('rc.conf') + system_conf = os.path.join(system_confdir, 'rc.conf') default_conf = fm.relpath('config', 'rc.conf') custom_conf_is_readable = os.access(custom_conf, os.R_OK) - if (os.environ.get('RANGER_LOAD_DEFAULT_RC', 'TRUE').upper() != 'FALSE' or - not custom_conf_is_readable): + system_conf_is_readable = os.access(system_conf, os.R_OK) + if (os.environ.get('RANGER_LOAD_DEFAULT_RC', 'TRUE').upper() != 'FALSE' + or not (custom_conf_is_readable or system_conf_is_readable)): fm.source(default_conf) + if system_conf_is_readable: + fm.source(system_conf) if custom_conf_is_readable: fm.source(custom_conf) diff --git a/ranger/data/scope.sh b/ranger/data/scope.sh index 540a910e..25251533 100755 --- a/ranger/data/scope.sh +++ b/ranger/data/scope.sh @@ -60,7 +60,8 @@ handle_extension() { # PDF pdf) # Preview as text conversion - pdftotext -l 10 -nopgbrk -q -- "${FILE_PATH}" - && exit 5 + pdftotext -l 10 -nopgbrk -q -- "${FILE_PATH}" - | fmt -w ${PV_WIDTH} && exit 5 + mutool draw -F txt -i -- "${FILE_PATH}" 1-10 | fmt -w ${PV_WIDTH} && exit 5 exiftool "${FILE_PATH}" && exit 5 exit 1;; diff --git a/ranger/ext/direction.py b/ranger/ext/direction.py index 7df45556..33ebb604 100644 --- a/ranger/ext/direction.py +++ b/ranger/ext/direction.py @@ -97,8 +97,8 @@ class Direction(dict): return self.get('cycle') in (True, 'true', 'on', 'yes') def one_indexed(self): - return ('one_indexed' in self and - self.get('one_indexed') in (True, 'true', 'on', 'yes')) + return ('one_indexed' in self + and self.get('one_indexed') in (True, 'true', 'on', 'yes')) def multiply(self, n): for key in ('up', 'right', 'down', 'left'): diff --git a/ranger/ext/human_readable.py b/ranger/ext/human_readable.py index df74eabf..f365e594 100644 --- a/ranger/ext/human_readable.py +++ b/ranger/ext/human_readable.py @@ -15,6 +15,10 @@ def human_readable(byte, separator=' '): # pylint: disable=too-many-return-stat '1023 M' """ + # handle automatically_count_files false + if byte is None: + return '' + # I know this can be written much shorter, but this long version # performs much better than what I had before. If you attempt to # shorten this code, take performance into consideration. diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py index 67941e27..4f447f39 100644 --- a/ranger/ext/img_display.py +++ b/ranger/ext/img_display.py @@ -60,7 +60,7 @@ class ImageDisplayer(object): pass -class W3MImageDisplayer(ImageDisplayer): +class W3MImageDisplayer(ImageDisplayer, FileManagerAware): """Implementation of ImageDisplayer using w3mimgdisplay, an utilitary program from w3m (a text-based web browser). w3mimgdisplay can display images either in virtual tty (using linux framebuffer) or in a Xorg session. @@ -119,6 +119,14 @@ class W3MImageDisplayer(ImageDisplayer): input_gen = self._generate_w3m_input(path, start_x, start_y, width, height) except ImageDisplayError: raise + + # Mitigate the issue with the horizontal black bars when + # selecting some images on some systems. 2 milliseconds seems + # enough. Adjust as necessary. + if self.fm.settings.w3m_delay > 0: + from time import sleep + sleep(self.fm.settings.w3m_delay) + self.process.stdin.write(input_gen) self.process.stdin.flush() self.process.stdout.readline() @@ -434,20 +442,20 @@ class URXVTImageDisplayer(ImageDisplayer, FileManagerAware): pct_width, pct_height = self._get_sizes() sys.stdout.write( - self.display_protocol + - path + - ";{pct_width}x{pct_height}+{pct_x}+{pct_y}:op=keep-aspect".format( + self.display_protocol + + path + + ";{pct_width}x{pct_height}+{pct_x}+{pct_y}:op=keep-aspect".format( pct_width=pct_width, pct_height=pct_height, pct_x=pct_x, pct_y=pct_y - ) + - self.close_protocol + ) + + self.close_protocol ) sys.stdout.flush() def clear(self, start_x, start_y, width, height): sys.stdout.write( - self.display_protocol + - ";100x100+1000+1000" + - self.close_protocol + self.display_protocol + + ";100x100+1000+1000" + + self.close_protocol ) sys.stdout.flush() diff --git a/ranger/ext/rifle.py b/ranger/ext/rifle.py index 70215039..672b0597 100755 --- a/ranger/ext/rifle.py +++ b/ranger/ext/rifle.py @@ -261,6 +261,14 @@ class Rifle(object): # pylint: disable=too-many-instance-attributes process = Popen(["file", "--mime-type", "-Lb", fname], stdout=PIPE, stderr=PIPE) mimetype, _ = process.communicate() self._mimetype = mimetype.decode(ENCODING).strip() + if self._mimetype == 'application/octet-stream': + try: + process = Popen(["mimetype", "--output-format", "%m", fname], + stdout=PIPE, stderr=PIPE) + mimetype, _ = process.communicate() + self._mimetype = mimetype.decode(ENCODING).strip() + except OSError: + pass return self._mimetype def _build_command(self, files, action, flags): diff --git a/ranger/gui/context.py b/ranger/gui/context.py index d8d1957c..96849686 100644 --- a/ranger/gui/context.py +++ b/ranger/gui/context.py @@ -23,7 +23,7 @@ CONTEXT_KEYS = [ 'keybuffer', 'infostring', 'vcsfile', 'vcsremote', 'vcsinfo', 'vcscommit', 'vcsdate', - 'vcsconflict', 'vcschanged', 'vcsunknown', 'vcsignored', + 'vcsconflict', 'vcschanged', 'vcsunknown', 'vcsignored', 'vcsuntracked', 'vcsstaged', 'vcssync', 'vcsnone', 'vcsbehind', 'vcsahead', 'vcsdiverged' ] diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py index 990db0ad..4f76dfab 100644 --- a/ranger/gui/ui.py +++ b/ranger/gui/ui.py @@ -113,7 +113,7 @@ class UI( # pylint: disable=too-many-instance-attributes,too-many-public-method self._draw_title = curses.tigetflag('hs') # has_status_line # Save tmux setting `automatic-rename` - if self.settings.update_tmux_title: + if self.settings.update_tmux_title and 'TMUX' in os.environ: try: self._tmux_automatic_rename = check_output( ['tmux', 'show-window-options', '-v', 'automatic-rename']).strip() @@ -123,7 +123,7 @@ class UI( # pylint: disable=too-many-instance-attributes,too-many-public-method self.update_size() self.is_on = True - if self.settings.update_tmux_title: + if self.settings.update_tmux_title and 'TMUX' in os.environ: sys.stdout.write("\033kranger\033\\") sys.stdout.flush() @@ -172,7 +172,7 @@ class UI( # pylint: disable=too-many-instance-attributes,too-many-public-method DisplayableContainer.destroy(self) # Restore tmux setting `automatic-rename` - if self.settings.update_tmux_title: + if self.settings.update_tmux_title and 'TMUX' in os.environ: if self._tmux_automatic_rename: try: check_output(['tmux', 'set-window-option', diff --git a/ranger/gui/widgets/__init__.py b/ranger/gui/widgets/__init__.py index 36292103..c8f1262b 100644 --- a/ranger/gui/widgets/__init__.py +++ b/ranger/gui/widgets/__init__.py @@ -12,11 +12,11 @@ class Widget(Displayable): 'conflict': ( 'X', ['vcsconflict']), 'untracked': ( - '+', ['vcschanged']), + '?', ['vcsuntracked']), 'deleted': ( '-', ['vcschanged']), 'changed': ( - '*', ['vcschanged']), + '+', ['vcschanged']), 'staged': ( '*', ['vcsstaged']), 'ignored': ( @@ -26,7 +26,7 @@ class Widget(Displayable): 'none': ( ' ', []), 'unknown': ( - '?', ['vcsunknown']), + '!', ['vcsunknown']), } vcsremotestatus_symb = { @@ -41,7 +41,7 @@ class Widget(Displayable): 'none': ( '⌂', ['vcsnone']), 'unknown': ( - '?', ['vcsunknown']), + '!', ['vcsunknown']), } ellipsis = {False: '~', True: '…'} diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index b38595c1..474fa7a6 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -324,8 +324,8 @@ class BrowserColumn(Pager): # pylint: disable=too-many-instance-attributes text = current_linemode.filetitle(drawn, metadata) - if drawn.marked and (self.main_column or - self.settings.display_tags_in_all_columns): + if drawn.marked and (self.main_column + or self.settings.display_tags_in_all_columns): text = " " + text # Computing predisplay data. predisplay contains a list of lists diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py index 266d48ca..3457955e 100644 --- a/ranger/gui/widgets/statusbar.py +++ b/ranger/gui/widgets/statusbar.py @@ -275,13 +275,14 @@ class StatusBar(Widget): # pylint: disable=too-many-instance-attributes right.add("/" + str(len(target.marked_items))) else: right.add(human_readable(target.disk_usage, separator='') + " sum") - try: - free = get_free_space(target.mount_path) - except OSError: - pass - else: - right.add(", ", "space") - right.add(human_readable(free, separator='') + " free") + if self.settings.display_free_space_in_status_bar: + try: + free = get_free_space(target.mount_path) + except OSError: + pass + else: + right.add(", ", "space") + right.add(human_readable(free, separator='') + " free") right.add(" ", "space") if target.marked_items: diff --git a/ranger/gui/widgets/view_base.py b/ranger/gui/widgets/view_base.py index 80061004..4493443e 100644 --- a/ranger/gui/widgets/view_base.py +++ b/ranger/gui/widgets/view_base.py @@ -72,8 +72,8 @@ class ViewBase(Widget, DisplayableContainer): # pylint: disable=too-many-instan sorted_bookmarks = sorted( ( item for item in self.fm.bookmarks - if self.fm.settings.show_hidden_bookmarks or - '/.' not in item[1].path + if self.fm.settings.show_hidden_bookmarks + or '/.' not in item[1].path ), key=lambda t: t[0].lower(), ) diff --git a/tests/ranger/core/__init__.py b/tests/ranger/core/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/tests/ranger/core/__init__.py diff --git a/tests/ranger/core/test_main.py b/tests/ranger/core/test_main.py new file mode 100644 index 00000000..d992b8a7 --- /dev/null +++ b/tests/ranger/core/test_main.py @@ -0,0 +1,18 @@ +import collections +import os + +from ranger.core import main + + +def test_get_paths(): + args_tuple = collections.namedtuple('args', 'paths') + args = args_tuple(paths=None) + + paths = main.get_paths(args) + + for path in paths: + assert os.path.exists(path) + + +if __name__ == '__main__': + test_get_paths() |