diff options
32 files changed, 661 insertions, 142 deletions
diff --git a/README b/README index 84bf30a3..bc67c16b 100644 --- a/README +++ b/README @@ -56,6 +56,7 @@ Dependencies Optional: * The "file" program for determining file types * The python module "chardet", in case of encoding detection problems +* "sudo" to use the "run as root"-feature Optional, for enhanced file previews (with "scope.sh"): * img2txt (from caca-utils) for previewing images @@ -89,7 +90,7 @@ to open a file or type Q to quit. The third column shows a preview of the current file. The second is the main column and the first shows the parent directory. -Ranger will automatically copy simple configuration files to ~/.config/ranger. -If you mess them up, just delete them and ranger will copy them again. Run -ranger with --dont-copy-config to disable this. Also check ranger/defaults/ -for the default configuration. +Ranger can automatically copy default configuration files to ~/.config/ranger +if you run it with the switch --copy-config. (see ranger --help for a +description of that switch.) Also check ranger/defaults/ for the default +configuration. diff --git a/doc/ranger.1 b/doc/ranger.1 index e9f00501..433bb8a7 100644 --- a/doc/ranger.1 +++ b/doc/ranger.1 @@ -124,7 +124,7 @@ .\" ======================================================================== .\" .IX Title "RANGER 1" -.TH RANGER 1 "ranger-1.5.2" "10/24/2011" "ranger manual" +.TH RANGER 1 "ranger-1.5.2" "03/05/2012" "ranger manual" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l @@ -178,6 +178,9 @@ selected files into \fItargetfile\fR, adding one newline after each filename. .IX Item "--choosedir=targetfile" 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. .IP "\fB\-\-copy\-config\fR=\fIfile\fR" 14 .IX Item "--copy-config=file" Create copies of the default configuration files in your local configuration @@ -188,6 +191,10 @@ directory. Existing ones will not be overwritten. Possible values: \fIall\fR, List common keys which are not bound to any action in the \*(L"browser\*(R" context. This list is not complete, you can bind any key that is supported by curses: use the key code returned by \f(CW\*(C`getch()\*(C'\fR. +.IP "\fB\-\-list\-tagged\-files\fR=\fItag\fR" 14 +.IX Item "--list-tagged-files=tag" +List all files which are tagged with the given tag. Note: Tags are single +characters. The default tag is \*(L"*\*(R" .IP "\fB\-\-fail\-unless\-cd\fR" 14 .IX Item "--fail-unless-cd" Return the exit code 1 if ranger is used to run a file instead of used for file @@ -201,6 +208,10 @@ the execution of this file type is explicitly handled in the configuration. When a filename is supplied, run it with the given \fIflags\fR to modify behavior. The execution of this file type is explicitly handled in the configuration. +.IP "\fB\-\-cmd\fR=\fIcommand\fR" 14 +.IX Item "--cmd=command" +Execute the command after the configuration has been read. Use this option +multiple times to run multiple commands. .IP "\fB\-\-version\fR" 14 .IX Item "--version" Print the version and exit. @@ -279,18 +290,23 @@ Note: The bookmarks ' (Apostrophe) and ` (Backtick) are the same. Flags give you a way to modify the behavior of the spawned process. They are used in the commands :open_with (key \*(L"r\*(R") and :shell (key \*(L"!\*(R"). .PP -.Vb 5 +.Vb 7 \& s Silent mode. Output will be discarded. \& d Detach the process. (Run in background) \& p Redirect output to the pager \& w Wait for an Enter\-press when the process is done \& c Run the current file only, instead of the selection +\& r Run application with root privilege (requires sudo) +\& t Run application in a new terminal window .Ve .PP By default, all the flags are off unless specified otherwise in the \fIapps.py\fR configuration file. You can specify as many flags as you want. An uppercase flag negates the effect: \*(L"ddcccDs\*(R" is equivalent to \*(L"cs\*(R". .PP +The \*(L"t\*(R" flag looks for the environment variable \s-1TERMCMD\s0, and uses it as the +terminal command, if it's not set it'll use xterm. +.PP Examples: \f(CW\*(C`:open_with p\*(C'\fR will pipe the output of that process into the pager. \f(CW\*(C`:shell \-w df\*(C'\fR will run \*(L"df\*(R" and wait for you to press Enter before switching back to ranger. @@ -733,6 +749,9 @@ it by typing `` or '' the next time you start ranger. .IX Item "rename newname" Rename the current file. If a file with that name already exists, the renaming will fail. Also try the key binding A for appending something to a file name. +.IP "relink \fInewpath\fR" 2 +.IX Item "relink newpath" +Change the link destination of the current symlink file to <newpath>. First <tab> will load the original link. .IP "save_copy_buffer" 2 .IX Item "save_copy_buffer" Save the copy buffer from \fI~/.config/ranger/copy_buffer\fR. This can be used to @@ -853,6 +872,11 @@ with T. To assign a named tag, type "<tagname>. .SH "ENVIRONMENT" .IX Header "ENVIRONMENT" These environment variables have an effect on ranger: +.IP "\s-1RANGER_LEVEL\s0" 8 +.IX Item "RANGER_LEVEL" +Ranger sets this environment variable to \*(L"1\*(R" or increments it if it already +exists. External programs can determine whether they were spawned from ranger +by checking for this variable. .IP "\s-1EDITOR\s0" 8 .IX Item "EDITOR" Defines the editor to be used for the \*(L"E\*(R" key. Defaults to the first installed @@ -861,6 +885,11 @@ program out of \*(L"vim\*(R", \*(L"emacs\*(R" and \*(L"nano\*(R". .IX Item "SHELL" Defines the shell that ranger is going to use with the :shell command and the \*(L"S\*(R" key. Defaults to \*(L"bash\*(R". +.IP "\s-1TERMCMD\s0" 8 +.IX Item "TERMCMD" +Defines the terminal emulator command that ranger is going to use with the +:terminal command and the \*(L"t\*(R" run flag. Defaults to \*(L"x\-terminal-emulator\*(R" or +\&\*(L"xterm\*(R" .IP "\s-1XDG_CONFIG_HOME\s0" 8 .IX Item "XDG_CONFIG_HOME" Specifies the directory for configuration files. Defaults to \fI\f(CI$HOME\fI/.config\fR. @@ -875,6 +904,14 @@ Using PYTHONOPTIMIZE=2 (like python \-OO) will additionally discard any docstrings. Using this will disable the <F1> key on commands. .SH "EXAMPLES" .IX Header "EXAMPLES" +.SS "\s-1BASH:\s0 Display that the shell spawned from ranger:" +.IX Subsection "BASH: Display that the shell spawned from ranger:" +By putting this in ~/.bashrc, \*(L"(in ranger) \*(R" will be displayed next to your +prompt to notify you that the shell spawned from ranger. +.PP +.Vb 1 +\& [ \-n "$RANGER_LEVEL" ] && PS1="$PS1"\*(Aq(in ranger) \*(Aq +.Ve .SS "\s-1VIM:\s0 File Chooser" .IX Subsection "VIM: File Chooser" This is a vim function which allows you to use ranger to select a file for @@ -882,7 +919,7 @@ opening in your current vim session. .PP .Vb 9 \& fun! RangerChooser() -\& silent !ranger \-\-choosefile=/tmp/chosenfile \`[ \-z \*(Aq%\*(Aq ] && echo \-n . || dirname %\` +\& exec "silent !ranger \-\-choosefile=/tmp/chosenfile " . expand("%:p:h") \& if filereadable(\*(Aq/tmp/chosenfile\*(Aq) \& exec \*(Aqedit \*(Aq . system(\*(Aqcat /tmp/chosenfile\*(Aq) \& call system(\*(Aqrm /tmp/chosenfile\*(Aq) diff --git a/doc/ranger.pod b/doc/ranger.pod index 069b9de1..9f8b4f04 100644 --- a/doc/ranger.pod +++ b/doc/ranger.pod @@ -67,6 +67,10 @@ selected files into I<targetfile>, adding one newline after each filename. Allows you to pick a directory with ranger. When you exit ranger, it will write the last visited directory into I<targetfile>. +=item B<--selectfile>=I<targetfile> + +Open ranger with I<targetfile> selected. + =item B<--copy-config>=I<file> Create copies of the default configuration files in your local configuration @@ -79,6 +83,11 @@ List common keys which are not bound to any action in the "browser" context. This list is not complete, you can bind any key that is supported by curses: use the key code returned by C<getch()>. +=item B<--list-tagged-files>=I<tag> + +List all files which are tagged with the given tag. Note: Tags are single +characters. The default tag is "*" + =item B<--fail-unless-cd> Return the exit code 1 if ranger is used to run a file instead of used for file @@ -95,6 +104,11 @@ When a filename is supplied, run it with the given I<flags> to modify behavior. The execution of this file type is explicitly handled in the configuration. +=item B<--cmd>=I<command> + +Execute the command after the configuration has been read. Use this option +multiple times to run multiple commands. + =item B<--version> Print the version and exit. @@ -186,11 +200,16 @@ used in the commands :open_with (key "r") and :shell (key "!"). p Redirect output to the pager w Wait for an Enter-press when the process is done c Run the current file only, instead of the selection + r Run application with root privilege (requires sudo) + t Run application in a new terminal window By default, all the flags are off unless specified otherwise in the F<apps.py> configuration file. You can specify as many flags as you want. An uppercase flag negates the effect: "ddcccDs" is equivalent to "cs". +The "t" flag looks for the environment variable TERMCMD, and uses it as the +terminal command, if it's not set it'll use xterm. + Examples: C<:open_with p> will pipe the output of that process into the pager. C<:shell -w df> will run "df" and wait for you to press Enter before switching back to ranger. @@ -778,6 +797,10 @@ it by typing `` or '' the next time you start ranger. Rename the current file. If a file with that name already exists, the renaming will fail. Also try the key binding A for appending something to a file name. +=item relink I<newpath> + +Change the link destination of the current symlink file to <newpath>. First <tab> will load the original link. + =item save_copy_buffer Save the copy buffer from I<~/.config/ranger/copy_buffer>. This can be used to @@ -937,6 +960,12 @@ These environment variables have an effect on ranger: =over 8 +=item RANGER_LEVEL + +Ranger sets this environment variable to "1" or increments it if it already +exists. External programs can determine whether they were spawned from ranger +by checking for this variable. + =item EDITOR Defines the editor to be used for the "E" key. Defaults to the first installed @@ -947,6 +976,13 @@ program out of "vim", "emacs" and "nano". Defines the shell that ranger is going to use with the :shell command and the "S" key. Defaults to "bash". +=item TERMCMD + +Defines the terminal emulator command that ranger is going to use with the +:terminal command and the "t" run flag. Defaults to "x-terminal-emulator" or +"xterm" + + =item XDG_CONFIG_HOME Specifies the directory for configuration files. Defaults to F<$HOME/.config>. @@ -968,13 +1004,20 @@ docstrings. Using this will disable the <F1> key on commands. =head1 EXAMPLES +=head2 BASH: Display that the shell spawned from ranger: + +By putting this in ~/.bashrc, "(in ranger) " will be displayed next to your +prompt to notify you that the shell spawned from ranger. + + [ -n "$RANGER_LEVEL" ] && PS1="$PS1"'(in ranger) ' + =head2 VIM: File Chooser This is a vim function which allows you to use ranger to select a file for opening in your current vim session. fun! RangerChooser() - silent !ranger --choosefile=/tmp/chosenfile `[ -z '%' ] && echo -n . || dirname %` + exec "silent !ranger --choosefile=/tmp/chosenfile " . expand("%:p:h") if filereadable('/tmp/chosenfile') exec 'edit ' . system('cat /tmp/chosenfile') call system('rm /tmp/chosenfile') diff --git a/ranger/__init__.py b/ranger/__init__.py index df413a2c..df759dc8 100644 --- a/ranger/__init__.py +++ b/ranger/__init__.py @@ -36,7 +36,7 @@ TIME_BEFORE_FILE_BECOMES_GARBAGE = 1200 MACRO_DELIMITER = '%' LOGFILE = '/tmp/ranger_errorlog' USAGE = '%prog [options] [path/filename]' -STABLE = True +STABLE = False # If the environment variable XDG_CONFIG_HOME is non-empty, CONFDIR is ignored # and the configuration directory will be $XDG_CONFIG_HOME/ranger instead. diff --git a/ranger/api/commands.py b/ranger/api/commands.py index ae3bdc94..a2501c7f 100644 --- a/ranger/api/commands.py +++ b/ranger/api/commands.py @@ -13,6 +13,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +# TODO: Add an optional "!" to all commands and set a flag if it's there + import os import ranger import re @@ -264,9 +266,11 @@ class Command(FileManagerAware): if len(names) == 0: return - # one result. since it must be a directory, append a slash. + # one result. append a slash if it's a directory if len(names) == 1: - return self.start(1) + join(rel_dirname, names[0]) + '/' + path = join(rel_dirname, names[0]) + slash = '/' if os.path.isdir(path) else '' + return self.start(1) + path + slash # more than one result. append no slash, so the user can # manually type in the slash to advance into that directory diff --git a/ranger/container/settingobject.py b/ranger/container/settingobject.py index 5c24d663..e7ded15e 100644 --- a/ranger/container/settingobject.py +++ b/ranger/container/settingobject.py @@ -19,6 +19,7 @@ from ranger.core.shared import FileManagerAware ALLOWED_SETTINGS = { 'autosave_bookmarks': bool, + 'autoupdate_cumulative_size': bool, 'collapse_preview': bool, 'colorscheme_overlay': (type(None), type(lambda:0)), 'colorscheme': str, @@ -31,6 +32,7 @@ ALLOWED_SETTINGS = { 'draw_borders': bool, 'flushinput': bool, 'hidden_filter': lambda x: isinstance(x, str) or hasattr(x, 'match'), + 'init_function': (type(None), type(lambda:0)), 'load_default_rc': (bool, type(None)), 'max_console_history_size': (int, type(None)), 'max_history_size': (int, type(None)), diff --git a/ranger/core/actions.py b/ranger/core/actions.py index c8922734..4e72de77 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -19,8 +19,8 @@ import re import shutil import string import tempfile -from os.path import join, isdir, realpath -from os import link, symlink, getcwd +from os.path import join, isdir, realpath, exists +from os import link, symlink, getcwd, listdir, stat from inspect import cleandoc import ranger @@ -28,6 +28,7 @@ from ranger.ext.direction import Direction from ranger.ext.relative_symlink import relative_symlink from ranger.ext.keybinding_parser import key_to_string, construct_keybinding from ranger.ext.shell_escape import shell_quote +from ranger.ext.next_available_filename import next_available_filename from ranger.core.shared import FileManagerAware, EnvironmentAware, \ SettingsAware from ranger.fsobject import File @@ -41,6 +42,11 @@ class _MacroTemplate(string.Template): class Actions(FileManagerAware, EnvironmentAware, SettingsAware): search_method = 'ctime' + mode = 'normal' # either 'normal' or 'visual'. + _visual_reverse = False + _visual_start = None + _visual_start_pos = None + _previous_selection = None # -------------------------- # -- Basic Commands @@ -56,6 +62,32 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): self.previews = {} self.env.garbage_collect(-1, self.tabs) self.enter_dir(old_path) + self.change_mode('normal') + + def change_mode(self, mode): + if mode == self.mode: + return + if mode == 'visual': + self._visual_start = self.env.cwd.pointed_obj + self._visual_start_pos = self.env.cwd.pointer + self._previous_selection = set(self.env.cwd.marked_items) + self.mark_files(val=not self._visual_reverse, movedown=False) + elif mode == 'normal': + if self.mode == 'visual': + self._visual_start = None + self._visual_start_pos = None + self._previous_selection = None + else: + return + self.mode = mode + self.ui.status.request_redraw() + + def toggle_visual_mode(self, reverse=False): + if self.mode == 'normal': + self._visual_reverse = reverse + self.change_mode('visual') + else: + self.change_mode('normal') def reload_cwd(self): try: @@ -87,12 +119,19 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): self.notify("Aborting: " + item.get_description()) self.loader.remove(index=0) + def get_cumulative_size(self): + for f in self.env.get_selection() or (): + f.look_up_cumulative_size() + self.ui.status.request_redraw() + self.ui.redraw_main_column() + def redraw_window(self): """Redraw the window""" self.ui.redraw_window() def open_console(self, string='', prompt=None, position=None): - """Open the console if the current UI supports that""" + """Open the console""" + self.change_mode('normal') self.ui.open_console(string, prompt=prompt, position=position) def execute_console(self, string='', wildcards=[], quantifier=None): @@ -247,15 +286,16 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): Both flags and mode specify how the program is run.""" # ranger can act as a file chooser when running with --choosefile=... - if ranger.arg.choosefile: - open(ranger.arg.choosefile, 'w').write(self.fm.env.cf.path) + if ('mode' not in kw or kw['mode'] == 0) and 'app' not in kw: + if ranger.arg.choosefile: + open(ranger.arg.choosefile, 'w').write(self.fm.env.cf.path) - if ranger.arg.choosefiles: - open(ranger.arg.choosefiles, 'w').write("".join( - f.path + "\n" for f in self.fm.env.get_selection())) + if ranger.arg.choosefiles: + open(ranger.arg.choosefiles, 'w').write("".join( + f.path + "\n" for f in self.fm.env.get_selection())) - if ranger.arg.choosefile or ranger.arg.choosefiles: - raise SystemExit() + if ranger.arg.choosefile or ranger.arg.choosefiles: + raise SystemExit() if isinstance(files, set): files = list(files) @@ -306,6 +346,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): except: return self.env.enter_dir(directory) + self.change_mode('normal') if cwd and cwd.accessible and cwd.content_loaded: if 'right' in direction: mode = 0 @@ -324,8 +365,34 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): current=cwd.pointer, pagesize=self.ui.browser.hei) cwd.move(to=newpos) + if self.mode == 'visual': + try: + startpos = cwd.index(self._visual_start) + except: + self._visual_start = None + startpos = min(self._visual_start_pos, len(cwd)) + # The files between here and _visual_start_pos + targets = set(cwd.files[min(startpos, newpos):\ + max(startpos, newpos) + 1]) + # The selection before activating visual mode + old = self._previous_selection + # The current selection + current = set(cwd.marked_items) + + # Set theory anyone? + if not self._visual_reverse: + for f in targets - current: + cwd.mark_item(f, True) + for f in current - old - targets: + cwd.mark_item(f, False) + else: + for f in targets & current: + cwd.mark_item(f, False) + for f in old - current - targets: + cwd.mark_item(f, True) def move_parent(self, n, narg=None): + self.change_mode('normal') if narg is not None: n *= narg parent = self.env.at_level(-1) @@ -354,18 +421,20 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): def enter_dir(self, path, remember=False, history=True): """Enter the directory at the given path""" - if remember: - cwd = self.env.cwd - result = self.env.enter_dir(path, history=history) - self.bookmarks.remember(cwd) - return result - return self.env.enter_dir(path, history=history) + cwd = self.env.cwd + result = self.env.enter_dir(path, history=history) + if cwd != self.env.cwd: + if remember: + self.bookmarks.remember(cwd) + self.change_mode('normal') + return result def cd(self, path, remember=True): """enter the directory at the given path, remember=True""" self.enter_dir(path, remember=remember) def traverse(self): + self.change_mode('normal') cf = self.env.cf cwd = self.env.cwd if cf is not None and cf.is_directory: @@ -470,6 +539,8 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): cwd.toggle_all_marks() else: cwd.mark_all(val) + if self.mode == 'visual': + self.change_mode('normal') else: for i in range(cwd.pointer, min(cwd.pointer + narg, len(cwd))): item = cwd.files[i] @@ -762,13 +833,14 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): # directory paths only. def tab_open(self, name, path=None): - do_emit_signal = name != self.current_tab + tab_has_changed = name != self.current_tab self.current_tab = name if path or (name in self.tabs): self.enter_dir(path or self.tabs[name]) else: self._update_current_tab() - if do_emit_signal: + if tab_has_changed: + self.change_mode('normal') self.signal_emit('tab.change') def tab_close(self, name=None): @@ -913,21 +985,46 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): def paste_symlink(self, relative=False): copied_files = self.env.copy for f in copied_files: + self.notify(next_available_filename(f.basename)) try: + new_name = next_available_filename(f.basename) if relative: - relative_symlink(f.path, join(getcwd(), f.basename)) + relative_symlink(f.path, join(getcwd(), new_name)) else: - symlink(f.path, join(getcwd(), f.basename)) + symlink(f.path, join(getcwd(), new_name)) except Exception as x: self.notify(x) def paste_hardlink(self): for f in self.env.copy: try: - link(f.path, join(getcwd(), f.basename)) + new_name = next_available_filename(f.basename) + link(f.path, join(getcwd(), new_name)) except Exception as x: self.notify(x) + def paste_hardlinked_subtree(self): + for f in self.env.copy: + try: + target_path = join(getcwd(), f.basename) + self._recurse_hardlinked_tree(f.path, target_path) + except Exception as x: + self.notify(x) + + def _recurse_hardlinked_tree(self, source_path, target_path): + if isdir(source_path): + if not exists(target_path): + os.mkdir(target_path, stat(source_path).st_mode) + for item in listdir(source_path): + self._recurse_hardlinked_tree( + join(source_path, item), + join(target_path, item)) + else: + if not exists(target_path) \ + or stat(source_path).st_ino != stat(target_path).st_ino: + link(source_path, + next_available_filename(target_path)) + def paste(self, overwrite=False): """Paste the selected items into the current directory""" copied_files = tuple(self.env.copy) diff --git a/ranger/core/fm.py b/ranger/core/fm.py index 59eb4e18..20327a71 100644 --- a/ranger/core/fm.py +++ b/ranger/core/fm.py @@ -85,10 +85,13 @@ class FM(Actions, SignalDispatcher): def mylogfunc(text): self.notify(text, bad=True) self.run = Runner(ui=self.ui, apps=self.apps, - logfunc=mylogfunc) + logfunc=mylogfunc, fm=self) self.env.signal_bind('cd', self._update_current_tab) + if self.settings.init_function: + self.settings.init_function(self) + def destroy(self): debug = ranger.arg.debug if self.ui: @@ -209,6 +212,8 @@ class FM(Actions, SignalDispatcher): finally: if ranger.arg.choosedir and self.env.cwd and self.env.cwd.path: + # XXX: UnicodeEncodeError: 'utf-8' codec can't encode character + # '\udcf6' in position 42: surrogates not allowed open(ranger.arg.choosedir, 'w').write(self.env.cwd.path) self.bookmarks.remember(env.cwd) self.bookmarks.save() diff --git a/ranger/core/helper.py b/ranger/core/helper.py index c22a52b8..c556b9bd 100644 --- a/ranger/core/helper.py +++ b/ranger/core/helper.py @@ -66,6 +66,16 @@ def parse_arguments(): ", it will write the name of the last visited directory to TARGET") parser.add_option('--list-unused-keys', action='store_true', help="List common keys which are not bound to any action.") + parser.add_option('--selectfile', type='string', metavar='filepath', + help="Open ranger with supplied file selected.") + parser.add_option('--list-tagged-files', type='string', default=None, + metavar='tag', + help="List all files which are tagged with the given tag, default: *") + parser.add_option('--profile', action='store_true', + help="Print statistics of CPU usage on exit.") + parser.add_option('--cmd', action='append', type='string', metavar='COMMAND', + help="Execute COMMAND after the configuration has been read. " + "Use this option multiple times to run multiple commands.") options, positional = parser.parse_args() arg = OpenStruct(options.__dict__, targets=positional) diff --git a/ranger/core/loader.py b/ranger/core/loader.py index e1262718..59d3e6c0 100644 --- a/ranger/core/loader.py +++ b/ranger/core/loader.py @@ -146,7 +146,7 @@ def safeDecode(string): return string.decode("utf-8") except (UnicodeDecodeError): if HAVE_CHARDET: - return string.decode(chardet.detect(str)["encoding"]) + return string.decode(chardet.detect(string)["encoding"]) else: return "" diff --git a/ranger/core/main.py b/ranger/core/main.py index c87a4660..b4629801 100644 --- a/ranger/core/main.py +++ b/ranger/core/main.py @@ -38,6 +38,13 @@ def main(): except: print("Warning: Unable to set locale. Expect encoding problems.") + # so that programs can know that ranger spawned them: + level = 'RANGER_LEVEL' + if level in os.environ and os.environ[level].isdigit(): + os.environ[level] = str(int(os.environ[level]) + 1) + else: + os.environ[level] = '1' + if not 'SHELL' in os.environ: os.environ['SHELL'] = 'bash' @@ -46,9 +53,27 @@ def main(): fm = FM() fm.copy_config_files(arg.copy_config) return 1 if arg.fail_unless_cd else 0 + if arg.list_tagged_files: + fm = FM() + try: + f = open(fm.confpath('tagged'), 'r') + except: + pass + else: + for line in f.readlines(): + if len(line) > 2 and line[1] == ':': + if line[0] in arg.list_tagged_files: + sys.stdout.write(line[2:]) + elif len(line) > 0 and '*' in arg.list_tagged_files: + sys.stdout.write(line) + return 1 if arg.fail_unless_cd else 0 SettingsAware._setup(clean=arg.clean) + if arg.selectfile: + arg.selectfile = os.path.abspath(arg.selectfile) + arg.targets.insert(0, os.path.dirname(arg.selectfile)) + targets = arg.targets or ['.'] target = targets[0] if arg.targets: @@ -62,7 +87,8 @@ def main(): print(string) from ranger.core.runner import Runner from ranger.fsobject import File - runner = Runner(logfunc=print_function) + fm = FM() + runner = Runner(logfunc=print_function, fm=fm) load_apps(runner, arg.clean) runner(files=[File(target)], mode=arg.mode, flags=arg.flags) return 1 if arg.fail_unless_cd else 0 @@ -97,10 +123,26 @@ def main(): from ranger.ext import curses_interrupt_handler curses_interrupt_handler.install_interrupt_handler() + if arg.selectfile: + fm.select_file(arg.selectfile) + # Run the file manager fm.initialize() fm.ui.initialize() - fm.loop() + + if arg.cmd: + for command in arg.cmd: + fm.execute_console(command) + + if ranger.arg.profile: + import cProfile + import pstats + profile = None + ranger.__fm = fm + cProfile.run('ranger.__fm.loop()', '/tmp/ranger_profile') + profile = pstats.Stats('/tmp/ranger_profile', stream=sys.stderr) + else: + fm.loop() except Exception: import traceback crash_traceback = traceback.format_exc() @@ -116,11 +158,16 @@ def main(): fm.ui.destroy() except (AttributeError, NameError): pass + if ranger.arg.profile and profile: + profile.strip_dirs().sort_stats('cumulative').print_callees() if crash_traceback: print("ranger version: %s, executed with python %s" % (ranger.__version__, sys.version.split()[0])) print("Locale: %s" % '.'.join(str(s) for s in locale.getlocale())) - print("Current file: %s" % filepath) + try: + print("Current file: %s" % filepath) + except: + pass print(crash_traceback) print("ranger crashed. " \ "Please report this traceback at:") diff --git a/ranger/core/runner.py b/ranger/core/runner.py index 940f410e..17cdcca5 100644 --- a/ranger/core/runner.py +++ b/ranger/core/runner.py @@ -30,15 +30,18 @@ d: detach the process. p: redirect output to the pager c: run only the current file (not handled here) w: wait for enter-press afterwards +r: run application with root privilege (requires sudo) +t: run application in a new terminal window (An uppercase key negates the respective lower case flag) """ import os import sys from subprocess import Popen, PIPE +from ranger.ext.get_executables import get_executables -ALLOWED_FLAGS = 'sdpwcSDPWC' +ALLOWED_FLAGS = 'sdpwcrtSDPWCRT' def press_enter(): @@ -94,8 +97,9 @@ class Context(object): class Runner(object): - def __init__(self, ui=None, logfunc=None, apps=None): + def __init__(self, ui=None, logfunc=None, apps=None, fm=None): self.ui = ui + self.fm = fm self.logfunc = logfunc self.apps = apps self.zombies = set() @@ -132,7 +136,7 @@ class Runner(object): # creating a Context object and passing it to # an Application object. - context = Context(app=app, files=files, mode=mode, + context = Context(app=app, files=files, mode=mode, fm=self.fm, flags=flags, wait=wait, popen_kws=popen_kws, file=files and files[0] or None) @@ -159,7 +163,6 @@ class Runner(object): wait_for_enter = False devnull = None - popen_kws['args'] = action if 'shell' not in popen_kws: popen_kws['shell'] = isinstance(action, str) if 'stdout' not in popen_kws: @@ -188,16 +191,45 @@ class Runner(object): if 'w' in context.flags: if not pipe_output and context.wait: # <-- sanity check wait_for_enter = True + if 'r' in context.flags: + if 'sudo' not in get_executables(): + return self._log("Can not run with 'r' flag, sudo is not installed!") + dflag = ('d' in context.flags) + if isinstance(action, str): + action = 'sudo ' + (dflag and '-b ' or '') + action + else: + action = ['sudo'] + (dflag and ['-b'] or []) + action + toggle_ui = True + context.wait = True + 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' + if isinstance(action, str): + action = term + ' -e ' + action + else: + action = [term, '-e'] + action + toggle_ui = False + context.wait = False + popen_kws['args'] = action # Finally, run it if toggle_ui: self._activate_ui(False) try: + error = None process = None + self.fm.signal_emit('runner.execute.before', + popen_kws=popen_kws, context=context) try: process = Popen(**popen_kws) except Exception as e: + error = e self._log("Failed to run: %s\n%s" % (str(action), str(e))) else: if context.wait: @@ -207,6 +239,8 @@ class Runner(object): if wait_for_enter: press_enter() finally: + self.fm.signal_emit('runner.execute.after', + popen_kws=popen_kws, context=context, error=error) if devnull: devnull.close() if toggle_ui: diff --git a/ranger/data/scope.sh b/ranger/data/scope.sh index aeb47a13..ed4f01e1 100755 --- a/ranger/data/scope.sh +++ b/ranger/data/scope.sh @@ -26,7 +26,7 @@ maxln=200 # Stop after $maxln lines. Can be used like ls | head -n $maxln # Find out something about the file: mimetype=$(file --mime-type -Lb "$path") -extension=$(echo "$path" | grep '\.' | grep -o '[^.]\+$') +extension=${path##*.} # Functions: # "have $1" succeeds if $1 is an existing command/installed program diff --git a/ranger/defaults/apps.py b/ranger/defaults/apps.py index 3ec6bff2..fbcc83c0 100644 --- a/ranger/defaults/apps.py +++ b/ranger/defaults/apps.py @@ -14,9 +14,17 @@ # in your ~/.config/ranger/apps.py, you should subclass the class defined # here like this: # -# from ranger.defaults.apps import CustomApplications as DefaultApps -# class CustomApplications(DeafultApps): -# <your definitions here> +# from ranger.defaults.apps import CustomApplications as DefaultApps +# class CustomApplications(DeafultApps): +# <your definitions here> +# +# To override app_defaults, you can write something like: +# +# def app_defaults(self, c): +# f = c.file +# if f.extension == 'lol': +# return "lolopener", c +# return DefaultApps.app_default(self, c) # # =================================================================== # This system is based on things called MODES and FLAGS. You can read @@ -27,6 +35,8 @@ # p Redirect output to the pager # w Wait for an Enter-press when the process is done # c Run the current file only, instead of the selection +# r Run application with root privilege +# t Run application in a new terminal window # # To implement flags in this file, you could do this: # context.flags += "d" @@ -97,23 +107,38 @@ class CustomApplications(Applications): if f.extension is not None: if f.extension in ('pdf', ): - return self.either(c, 'evince', 'zathura', 'apvlv') + return self.either(c, 'llpp', 'zathura', 'mupdf', 'apvlv', + 'evince', 'okular', 'epdfview') if f.extension == 'djvu': return self.either(c, 'evince') - if f.extension in ('xml', ): + if f.extension in ('xml', 'csv'): return self.either(c, 'editor') + if f.extension == 'mid': + return self.either(c, 'wildmidi') + if f.extension in ('html', 'htm', 'xhtml') or f.extension == 'swf': + c.flags += 'd' + handler = self.either(c, + 'luakit', 'uzbl', 'vimprobable', 'vimprobable2', 'jumanji', + 'firefox', 'seamonkey', 'iceweasel', 'opera', + 'surf', 'midori', 'epiphany', 'konqueror') + # Only return if some program was found: + if handler: + return handler if f.extension in ('html', 'htm', 'xhtml'): - return self.either(c, 'firefox', 'opera', 'jumanji', - 'luakit', 'elinks', 'lynx') - if f.extension == 'swf': - return self.either(c, 'firefox', 'opera', 'jumanji', 'luakit') + # These browsers can't handle flash, so they're not called above. + c.flags += 'D' + return self.either(c, 'elinks', 'links', 'links2', 'lynx', 'w3m') if f.extension == 'nes': return self.either(c, 'fceux') if f.extension in ('swc', 'smc', 'sfc'): return self.either(c, 'zsnes') - if f.extension in ('odt', 'ods', 'odp', 'odf', 'odg', - 'doc', 'xls'): - return self.either(c, 'libreoffice', 'soffice', 'ooffice') + if f.extension == 'doc': + return self.either(c, 'abiword', 'libreoffice', + 'soffice', 'ooffice') + if f.extension in ('odt', 'ods', 'odp', 'odf', 'odg', 'sxc', + 'stc', 'xls', 'xlsx', 'xlt', 'xlw', 'gnm', 'gnumeric'): + return self.either(c, 'gnumeric', 'kspread', + 'libreoffice', 'soffice', 'ooffice') if f.mimetype is not None: if INTERPRETED_LANGUAGES.match(f.mimetype): @@ -125,8 +150,8 @@ class CustomApplications(Applications): if f.video or f.audio: if f.video: c.flags += 'd' - return self.either(c, 'mplayer2', 'mplayer', 'smplayer', 'vlc', - 'totem') + return self.either(c, 'smplayer', 'gmplayer', 'mplayer2', + 'mplayer', 'vlc', 'totem') if f.image: if c.mode in (11, 12, 13, 14): @@ -281,8 +306,14 @@ class CustomApplications(Applications): CustomApplications.generic('fceux', 'wine', 'zsnes', deps=['X']) # Add those which should only run in X AND should be detached/forked here: -CustomApplications.generic('opera', 'firefox', 'apvlv', 'evince', - 'zathura', 'gimp', 'mirage', 'eog', 'jumanji', +CustomApplications.generic( + 'luakit', 'uzbl', 'vimprobable', 'vimprobable2', 'jumanji', + 'firefox', 'seamonkey', 'iceweasel', 'opera', + 'surf', 'midori', 'epiphany', 'konqueror', + 'evince', 'zathura', 'apvlv', 'okular', 'epdfview', 'mupdf', 'llpp', + 'eog', 'mirage', 'gimp', + 'libreoffice', 'soffice', 'ooffice', 'gnumeric', 'kspread', 'abiword', + 'gmplayer', 'smplayer', 'vlc', flags='d', deps=['X']) # What filetypes are recognized as scripts for interpreted languages? diff --git a/ranger/defaults/commands.py b/ranger/defaults/commands.py index 6cfeff17..a89dd0f7 100644 --- a/ranger/defaults/commands.py +++ b/ranger/defaults/commands.py @@ -218,7 +218,11 @@ class shell(Command): return (start + program + ' ' for program \ in get_executables() if program.startswith(command)) if position_of_last_space == len(command) - 1: - return self.line + '%s ' + selection = self.fm.env.get_selection() + if len(selection) == 1: + return self.line + selection[0].shell_escaped_basename + ' ' + else: + return self.line + '%s ' else: before_word, start_of_word = self.line.rsplit(' ', 1) return (before_word + ' ' + file.shell_escaped_basename \ @@ -455,7 +459,12 @@ class terminal(Command): Spawns an "x-terminal-emulator" starting in the current directory. """ def execute(self): - self.fm.run('x-terminal-emulator', flags='d') + 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='d') class delete(Command): @@ -797,11 +806,11 @@ class bulkrename(Command): cmdfile.write(b"# This file will be executed when you close the editor.\n") cmdfile.write(b"# Please double-check everything, clear the file to abort.\n") if py3: - cmdfile.write("\n".join("mv -vi " + esc(old) + " " + esc(new) \ + cmdfile.write("\n".join("mv -vi -- " + esc(old) + " " + esc(new) \ for old, new in zip(filenames, new_filenames) \ if old != new).encode("utf-8")) else: - cmdfile.write("\n".join("mv -vi " + esc(old) + " " + esc(new) \ + cmdfile.write("\n".join("mv -vi -- " + esc(old) + " " + esc(new) \ for old, new in zip(filenames, new_filenames) if old != new)) cmdfile.flush() self.fm.execute_file([File(cmdfile.name)], app='editor') @@ -809,6 +818,45 @@ class bulkrename(Command): cmdfile.close() +class relink(Command): + """ + :relink <newpath> + + Changes the linked path of the currently highlighted symlink to <newpath> + """ + + def execute(self): + from ranger.fsobject import File + + new_path = self.rest(1) + cf = self.fm.env.cf + + if not new_path: + return self.fm.notify('Syntax: relink <newpath>', bad=True) + + if not cf.is_link: + return self.fm.notify('%s is not a symlink!' % cf.basename, bad=True) + + if new_path == os.readlink(cf.path): + return + + try: + os.remove(cf.path) + os.symlink(new_path, cf.path) + except OSError as err: + self.fm.notify(err) + + self.fm.reset() + self.fm.env.cwd.pointed_obj = cf + self.fm.env.cf = cf + + def tab(self): + if not self.rest(1): + return self.line+os.readlink(self.fm.env.cf.path) + else: + return self._tab_directory_content() + + class help_(Command): """ :help diff --git a/ranger/defaults/options.py b/ranger/defaults/options.py index 3b44d4f6..d076a96d 100644 --- a/ranger/defaults/options.py +++ b/ranger/defaults/options.py @@ -104,19 +104,35 @@ padding_right = True # When false, bookmarks are saved when ranger is exited. autosave_bookmarks = True +# You can display the "real" cumulative size of directories by using the +# command :get_cumulative_size or typing "dc". The size is expensive to +# calculate and will not be updated automatically. You can choose +# to update it automatically though by turning on this option: +autoupdate_cumulative_size = False + # Makes sense for screen readers: show_cursor = False # One of: size, basename, mtime, type sort = 'natural' sort_reverse = False -sort_case_insensitive = False +sort_case_insensitive = True sort_directories_first = True # Enable this if key combinations with the Alt Key don't work for you. # (Especially on xterm) xterm_alt_key = False +# A function that is called when the user interface is being set up. +init_function = None + +# You can use it to initialize some custom functionality or bind singals +#def init_function(fm): +# fm.notify("Hello :)") +# def on_tab_change(signal): +# signal.origin.notify("Changing tab! Yay!") +# fm.signal_bind("tab.change", on_tab_change) + # The color scheme overlay. Explained below. colorscheme_overlay = None diff --git a/ranger/defaults/rc.conf b/ranger/defaults/rc.conf index a9e64622..77ffa5c3 100644 --- a/ranger/defaults/rc.conf +++ b/ranger/defaults/rc.conf @@ -39,6 +39,7 @@ map R reload_cwd map <C-r> reset map <C-l> redraw_window map <C-c> abort +map <esc> change_mode normal map i display_file map ? help @@ -62,8 +63,9 @@ map T tag_remove map "<any> tag_toggle tag=%any map <Space> mark_files toggle=True map v mark_files all=True toggle=True -map V mark_files all=True val=False map uv mark_files all=True val=False +map V toggle_visual_mode +map uV toggle_visual_mode reverse=True # For the nostalgics: Midnight Commander bindings map <F1> help @@ -145,6 +147,7 @@ map po paste overwrite=True map pl paste_symlink relative=False map pL paste_symlink relative=True map phl paste_hardlink +map pht paste_hardlinked_subtree map dd cut map ud uncut @@ -216,6 +219,8 @@ map oC chain set sort=ctime; set sort_reverse=True map oA chain set sort=atime; set sort_reverse=True map oT chain set sort="type"; set sort_reverse=True +map dc get_cumulative_size + # Settings map zc toggle_option collapse_preview map zd toggle_option sort_directories_first @@ -226,6 +231,7 @@ map zm toggle_option mouse_enabled map zp toggle_option preview_files map zP toggle_option preview_directories map zs toggle_option sort_case_insensitive +map zu toggle_option autoupdate_cumulative_size map zv toggle_option use_preview_script map zf console filter diff --git a/ranger/ext/cached_function.py b/ranger/ext/cached_function.py new file mode 100644 index 00000000..4d9ded18 --- /dev/null +++ b/ranger/ext/cached_function.py @@ -0,0 +1,27 @@ +# Copyright (C) 2012 Roman Zimbelmann <romanz@lavabit.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +def cached_function(fnc): + cache = {} + def inner_cached_function(*args): + try: + return cache[args] + except: + value = fnc(*args) + cache[args] = value + return value + inner_cached_function._cache = cache + return inner_cached_function + diff --git a/ranger/ext/human_readable.py b/ranger/ext/human_readable.py index 9cdce409..c5bd2aac 100644 --- a/ranger/ext/human_readable.py +++ b/ranger/ext/human_readable.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -def human_readable(byte, seperator=' '): +def human_readable(byte, separator=' '): """ Convert a large number of bytes to an easily readable format. @@ -27,27 +27,27 @@ def human_readable(byte, seperator=' '): if byte <= 0: return '0' if byte < 2**10: - return '%d%sB' % (byte, seperator) + return '%d%sB' % (byte, separator) if byte < 2**10 * 999: - return '%.3g%sK' % (byte / 2**10.0, seperator) + return '%.3g%sK' % (byte / 2**10.0, separator) if byte < 2**20: - return '%.4g%sK' % (byte / 2**10.0, seperator) + return '%.4g%sK' % (byte / 2**10.0, separator) if byte < 2**20 * 999: - return '%.3g%sM' % (byte / 2**20.0, seperator) + return '%.3g%sM' % (byte / 2**20.0, separator) if byte < 2**30: - return '%.4g%sM' % (byte / 2**20.0, seperator) + return '%.4g%sM' % (byte / 2**20.0, separator) if byte < 2**30 * 999: - return '%.3g%sG' % (byte / 2**30.0, seperator) + return '%.3g%sG' % (byte / 2**30.0, separator) if byte < 2**40: - return '%.4g%sG' % (byte / 2**30.0, seperator) + return '%.4g%sG' % (byte / 2**30.0, separator) if byte < 2**40 * 999: - return '%.3g%sT' % (byte / 2**40.0, seperator) + return '%.3g%sT' % (byte / 2**40.0, separator) if byte < 2**50: - return '%.4g%sT' % (byte / 2**40.0, seperator) + return '%.4g%sT' % (byte / 2**40.0, separator) if byte < 2**50 * 999: - return '%.3g%sP' % (byte / 2**50.0, seperator) + return '%.3g%sP' % (byte / 2**50.0, separator) if byte < 2**60: - return '%.4g%sP' % (byte / 2**50.0, seperator) + return '%.4g%sP' % (byte / 2**50.0, separator) return '>9000' if __name__ == '__main__': diff --git a/ranger/ext/next_available_filename.py b/ranger/ext/next_available_filename.py new file mode 100644 index 00000000..696063cf --- /dev/null +++ b/ranger/ext/next_available_filename.py @@ -0,0 +1,30 @@ +# Copyright (C) 2011 Roman Zimbelmann <romanz@lavabit.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os.path + +def next_available_filename(fname, directory="."): + existing_files = os.listdir(directory) + + if fname not in existing_files: + return fname + if not fname.endswith("_"): + fname += "_" + if fname not in existing_files: + return fname + + for i in range(1, len(existing_files) + 1): + if fname + str(i) not in existing_files: + return fname + str(i) diff --git a/ranger/ext/shell_escape.py b/ranger/ext/shell_escape.py index 28a502bf..b68afc33 100644 --- a/ranger/ext/shell_escape.py +++ b/ranger/ext/shell_escape.py @@ -18,17 +18,20 @@ Functions to escape metacharacters of arguments for shell commands. """ META_CHARS = (' ', "'", '"', '`', '&', '|', ';', - '$', '!', '(', ')', '[', ']', '<', '>') + '$', '!', '(', ')', '[', ']', '<', '>', '\t') +UNESCAPABLE = set(map(chr, list(range(9)) + list(range(10, 32)) \ + + list(range(127, 256)))) META_DICT = dict([(mc, '\\' + mc) for mc in META_CHARS]) def shell_quote(string): """Escapes by quoting""" return "'" + str(string).replace("'", "'\\''") + "'" - def shell_escape(arg): """Escapes by adding backslashes""" arg = str(arg) + if UNESCAPABLE & set(arg): + return shell_quote(arg) arg = arg.replace('\\', '\\\\') # make sure this comes at the start for k, v in META_DICT.items(): arg = arg.replace(k, v) diff --git a/ranger/ext/signals.py b/ranger/ext/signals.py index ecb48de3..0df39fe0 100644 --- a/ranger/ext/signals.py +++ b/ranger/ext/signals.py @@ -126,7 +126,7 @@ class SignalDispatcher(object): handler._function = None self._signals = dict() - def signal_bind(self, signal_name, function, priority=0.5, weak=False): + def signal_bind(self, signal_name, function, priority=0.5, weak=False, autosort=True): """ Bind a function to the signal. @@ -162,9 +162,25 @@ class SignalDispatcher(object): handler = SignalHandler(signal_name, function, priority, nargs > 0) handlers.append(handler) - handlers.sort(key=lambda handler: -handler._priority) + if autosort: + handlers.sort(key=lambda handler: -handler._priority) return handler + def signal_force_sort(self, signal_name=None): + """ + Forces a sorting of signal handlers by priority. + + This is only necessary if you used signal_bind with autosort=False + after finishing to bind many signals at once. + """ + if signal_name is None: + for handlers in self._signals.values(): + handlers.sort(key=lambda handler: -handler._priority) + elif signal_name in self._signals: + self._signals[signal_name].sort(key=lambda handler: -handler._priority) + else: + return False + def signal_unbind(self, signal_handler): """ Removes a signal binding. diff --git a/ranger/fsobject/directory.py b/ranger/fsobject/directory.py index b3a35a0a..81e50ed9 100644 --- a/ranger/fsobject/directory.py +++ b/ranger/fsobject/directory.py @@ -25,6 +25,7 @@ from ranger.fsobject import File, FileSystemObject from ranger.core.shared import SettingsAware from ranger.ext.accumulator import Accumulator from ranger.ext.lazy_property import lazy_property +from ranger.ext.human_readable import human_readable def sort_by_basename(path): """returns path.basename (for sorting)""" @@ -79,6 +80,8 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware): content_outdated = False content_loaded = False + _cumulative_size_calculated = False + sort_dict = { 'basename': sort_by_basename, 'natural': sort_naturally, @@ -100,11 +103,11 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware): for opt in ('sort_directories_first', 'sort', 'sort_reverse', 'sort_case_insensitive'): self.settings.signal_bind('setopt.' + opt, - self.request_resort, weak=True) + self.request_resort, weak=True, autosort=False) for opt in ('hidden_filter', 'show_hidden'): self.settings.signal_bind('setopt.' + opt, - self.request_reload, weak=True) + self.request_reload, weak=True, autosort=False) self.use() def request_resort(self): @@ -185,8 +188,25 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware): hidden_filter = not self.settings.show_hidden \ and self.settings.hidden_filter filelist = os.listdir(mypath) - self.size = len(filelist) - self.infostring = ' %d' % self.size + + if self._cumulative_size_calculated: + # If self.content_loaded is true, this is not the first + # time loading. So I can't really be sure if the + # size has changed and I'll add a "?". + if self.content_loaded: + if self.fm.settings.autoupdate_cumulative_size: + self.look_up_cumulative_size() + else: + self.infostring = ' %s' % human_readable( + self.size, separator='? ') + else: + self.infostring = ' %s' % human_readable(self.size) + else: + self.size = len(filelist) + self.infostring = ' %d' % self.size + if self.is_link: + self.infostring = '->' + self.infostring + filenames = [mypath + (mypath == '/' and fname or '/' + fname)\ for fname in filelist if accept_file( fname, mypath, hidden_filter, self.filter)] @@ -327,6 +347,30 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware): else: self.correct_pointer() + def _get_cumulative_size(self): + if self.size == 0: + return 0 + cum = 0 + realpath = os.path.realpath + for dirpath, dirnames, filenames in os.walk(self.path, + onerror=lambda _: None): + for file in filenames: + try: + if dirpath == self.path: + stat = os_stat(realpath(dirpath + "/" + file)) + else: + stat = os_stat(dirpath + "/" + file) + cum += stat.st_size + except: + pass + return cum + + def look_up_cumulative_size(self): + self._cumulative_size_calculated = True + self.size = self._get_cumulative_size() + self.infostring = ('-> ' if self.is_link else ' ') + \ + human_readable(self.size) + @lazy_property def size(self): try: diff --git a/ranger/fsobject/fsobject.py b/ranger/fsobject/fsobject.py index d74b21c1..943c8aa4 100644 --- a/ranger/fsobject/fsobject.py +++ b/ranger/fsobject/fsobject.py @@ -26,8 +26,17 @@ from ranger.ext.spawn import spawn from ranger.ext.lazy_property import lazy_property from ranger.ext.human_readable import human_readable +if hasattr(str, 'maketrans'): + maketrans = str.maketrans +else: + from string import maketrans +_unsafe_chars = '\n' + ''.join(map(chr, range(32))) + ''.join(map(chr, range(128, 256))) +_safe_string_table = maketrans(_unsafe_chars, '?' * len(_unsafe_chars)) _extract_number_re = re.compile(r'([^0-9]?)(\d*)') +def safe_path(path): + return path.translate(_safe_string_table) + class FileSystemObject(FileManagerAware): (basename, basename_lower, @@ -106,6 +115,11 @@ class FileSystemObject(FileManagerAware): return [c if i % 3 == 1 else (int(c) if c else 0) for i, c in \ enumerate(_extract_number_re.split(self.basename_lower))] + @lazy_property + def safe_basename(self): + return self.basename.translate(_safe_string_table) + + for attr in ('video', 'audio', 'image', 'media', 'document', 'container'): exec("%s = lazy_property(" "lambda self: self.set_mimetype() or self.%s)" % (attr, attr)) @@ -117,6 +131,9 @@ class FileSystemObject(FileManagerAware): def use(self): """Used in garbage-collecting. Override in Directory""" + def look_up_cumulative_size(self): + pass # normal files have no cumulative size + def set_mimetype(self): """assign attributes such as self.video according to the mimetype""" basename = self.basename diff --git a/ranger/gui/colorscheme.py b/ranger/gui/colorscheme.py index bc5a67a5..cc72d6a8 100644 --- a/ranger/gui/colorscheme.py +++ b/ranger/gui/colorscheme.py @@ -46,6 +46,8 @@ from ranger.gui.color import get_color from ranger.gui.context import Context from ranger.core.helper import allow_access_to_confdir from ranger.core.shared import SettingsAware +from ranger.ext.cached_function import cached_function +from ranger.ext.iter_tools import flatten # ColorScheme is not SettingsAware but it will gain access # to the settings during the initialization. We can't import @@ -60,9 +62,6 @@ class ColorScheme(SettingsAware): which fits to the given keys. """ - def __init__(self): - self.cache = {} - def get(self, *keys): """ Returns the (fg, bg, attr) for the given keys. @@ -70,33 +69,28 @@ class ColorScheme(SettingsAware): Using this function rather than use() will cache all colors for faster access. """ - keys = frozenset(keys) - try: - return self.cache[keys] - - except KeyError: - context = Context(keys) - - # add custom error messages for broken colorschemes - color = self.use(context) - if self.settings.colorscheme_overlay: - result = self.settings.colorscheme_overlay(context, *color) - assert isinstance(result, (tuple, list)), \ - "Your colorscheme overlay doesn't return a tuple!" - assert all(isinstance(val, int) for val in result), \ - "Your colorscheme overlay doesn't return a tuple"\ - " containing 3 integers!" - color = result - self.cache[keys] = color - return color - + context = Context(keys) + + # add custom error messages for broken colorschemes + color = self.use(context) + if self.settings.colorscheme_overlay: + result = self.settings.colorscheme_overlay(context, *color) + assert isinstance(result, (tuple, list)), \ + "Your colorscheme overlay doesn't return a tuple!" + assert all(isinstance(val, int) for val in result), \ + "Your colorscheme overlay doesn't return a tuple"\ + " containing 3 integers!" + color = result + return color + + @cached_function def get_attr(self, *keys): """ Returns the curses attribute for the specified keys Ready to use for curses.setattr() """ - fg, bg, attr = self.get(*keys) + fg, bg, attr = self.get(*flatten(keys)) return attr | color_pair(get_color(fg, bg)) def use(self, context): diff --git a/ranger/gui/curses_shortcuts.py b/ranger/gui/curses_shortcuts.py index 10a159a1..a383ab4c 100644 --- a/ranger/gui/curses_shortcuts.py +++ b/ranger/gui/curses_shortcuts.py @@ -17,7 +17,6 @@ import curses import _curses -from ranger.ext.iter_tools import flatten from ranger.gui.color import get_color from ranger.core.shared import SettingsAware @@ -63,7 +62,6 @@ class CursesShortcuts(SettingsAware): def color(self, *keys): """Change the colors from now on.""" - keys = flatten(keys) attr = self.settings.colorscheme.get_attr(*keys) try: self.win.attrset(attr) @@ -72,7 +70,6 @@ class CursesShortcuts(SettingsAware): def color_at(self, y, x, wid, *keys): """Change the colors at the specified position""" - keys = flatten(keys) attr = self.settings.colorscheme.get_attr(*keys) try: self.win.chgat(y, x, wid, attr) diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py index 91d0d774..e6c7d065 100644 --- a/ranger/gui/ui.py +++ b/ranger/gui/ui.py @@ -23,8 +23,8 @@ from .mouse_event import MouseEvent from ranger.ext.keybinding_parser import ALT_KEY TERMINALS_WITH_TITLE = ("xterm", "xterm-256color", "rxvt", - "rxvt-256color", "rxvt-unicode", "aterm", "Eterm", - "screen", "screen-256color") + "rxvt-256color", "rxvt-unicode", "rxvt-unicode-256color", + "aterm", "Eterm", "screen", "screen-256color") MOUSEMASK = curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION @@ -293,10 +293,10 @@ class UI(DisplayableContainer): split = cwd.rsplit(os.sep, self.settings.shorten_title) if os.sep in split[0]: cwd = os.sep.join(split[1:]) - try: - sys.stdout.write("\033]2;ranger:" + cwd + "\007") - except UnicodeEncodeError: - sys.stdout.write("\033]2;ranger:" + ascii_only(cwd) + "\007") + fixed_cwd = cwd.encode('utf-8', 'surrogateescape'). \ + decode('utf-8', 'replace') + sys.stdout.write("\033]2;ranger:" + fixed_cwd + "\007") + sys.stdout.flush() self.win.refresh() def finalize(self): diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index b6b745aa..e9c08439 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -178,7 +178,7 @@ class BrowserColumn(Pager): self.win.move(0, 0) if not self.target.content_loaded: - self.color(base_color) + self.color(tuple(base_color)) self.addnstr("...", self.wid) self.color_reset() return @@ -187,13 +187,13 @@ class BrowserColumn(Pager): base_color.append('main_column') if not self.target.accessible: - self.color(base_color, 'error') + self.color(tuple(base_color + ['error'])) self.addnstr("not accessible", self.wid) self.color_reset() return if self.target.empty(): - self.color(base_color, 'empty') + self.color(tuple(base_color + ['empty'])) self.addnstr("empty", self.wid) self.color_reset() return @@ -289,15 +289,15 @@ class BrowserColumn(Pager): if x > 0: self.addstr(line, x, infostring) - self.color_at(line, 0, self.wid, this_color) + self.color_at(line, 0, self.wid, tuple(this_color)) if bad_info_color: start, wid = bad_info_color - self.color_at(line, start, wid, this_color, 'badinfo') + self.color_at(line, start, wid, tuple(this_color), 'badinfo') if (self.main_column or self.settings.display_tags_in_all_columns) \ and tagged and self.wid > 2: this_color.append('tag_marker') - self.color_at(line, 0, len(tagged_marker), this_color) + self.color_at(line, 0, len(tagged_marker), tuple(this_color)) self.color_reset() diff --git a/ranger/gui/widgets/browserview.py b/ranger/gui/widgets/browserview.py index d386d389..ea04c1e0 100644 --- a/ranger/gui/widgets/browserview.py +++ b/ranger/gui/widgets/browserview.py @@ -215,7 +215,10 @@ class BrowserView(Widget, DisplayableContainer): ystart = self.hei - hei self.addnstr(ystart - 1, 0, "key command".ljust(self.wid), self.wid) - self.win.chgat(ystart - 1, 0, curses.A_UNDERLINE) + try: + self.win.chgat(ystart - 1, 0, curses.A_UNDERLINE) + except: + pass whitespace = " " * self.wid i = ystart for key, cmd in hints: diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py index 1e2e2520..1ffb9fa3 100644 --- a/ranger/gui/widgets/statusbar.py +++ b/ranger/gui/widgets/statusbar.py @@ -159,7 +159,10 @@ class StatusBar(Widget): if stat is None: return - perms = target.get_permission_string() + if self.fm.mode != 'normal': + perms = '--%s--' % self.fm.mode.upper() + else: + perms = target.get_permission_string() how = getuid() == stat.st_uid and 'good' or 'bad' left.add(perms, 'permissions', how) left.add_space() @@ -231,17 +234,21 @@ class StatusBar(Widget): if target.marked_items: if len(target.marked_items) == len(target.files): - right.add(human_readable(target.disk_usage, seperator='')) + right.add(human_readable(target.disk_usage, separator='')) else: - right.add(human_readable(sum(f.size \ - for f in target.marked_items \ - if f.is_file), seperator='')) + sumsize = sum(f.size for f in target.marked_items if not + f.is_directory or f._cumulative_size_calculated) + right.add(human_readable(sumsize, separator='')) right.add("/" + str(len(target.marked_items))) else: - right.add(human_readable(target.disk_usage, seperator='') + - " sum, ") - right.add(human_readable(self.env.get_free_space( \ - target.mount_path), seperator='') + " free") + right.add(human_readable(target.disk_usage, separator='') + " sum") + try: + free = self.env.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: @@ -251,7 +258,7 @@ class StatusBar(Widget): elif len(target.files): right.add(str(target.pointer + 1) + '/' + str(len(target.files)) + ' ', base) - if max_pos == 0: + if max_pos <= 0: right.add('All', base, 'all') elif pos == 0: right.add('Top', base, 'top') diff --git a/ranger/gui/widgets/taskview.py b/ranger/gui/widgets/taskview.py index c4476b9c..a3f8e314 100644 --- a/ranger/gui/widgets/taskview.py +++ b/ranger/gui/widgets/taskview.py @@ -17,8 +17,6 @@ The TaskView allows you to modify what the loader is doing. """ -from collections import deque - from . import Widget from ranger.ext.accumulator import Accumulator @@ -31,7 +29,7 @@ class TaskView(Widget, Accumulator): self.scroll_begin = 0 def draw(self): - base_clr = deque() + base_clr = [] base_clr.append('in_taskview') lst = self.get_list() @@ -48,7 +46,7 @@ class TaskView(Widget, Accumulator): return self.addstr(0, 0, "Task View") - self.color_at(0, 0, self.wid, base_clr, 'title') + self.color_at(0, 0, self.wid, tuple(base_clr), 'title') if lst: for i in range(self.hei - 1): @@ -59,19 +57,19 @@ class TaskView(Widget, Accumulator): break y = i + 1 - clr = deque(base_clr) + clr = list(base_clr) if self.pointer == i: clr.append('selected') descr = obj.get_description() self.addstr(y, 0, descr, self.wid) - self.color_at(y, 0, self.wid, clr) + self.color_at(y, 0, self.wid, tuple(clr)) else: if self.hei > 1: self.addstr(1, 0, "No task in the queue.") - self.color_at(1, 0, self.wid, base_clr, 'error') + self.color_at(1, 0, self.wid, tuple(base_clr), 'error') self.color_reset() diff --git a/ranger/gui/widgets/titlebar.py b/ranger/gui/widgets/titlebar.py index 6b92ccfa..2b5e836b 100644 --- a/ranger/gui/widgets/titlebar.py +++ b/ranger/gui/widgets/titlebar.py @@ -102,6 +102,7 @@ class TitleBar(Widget): self.result = bar.combine() def _get_left_part(self, bar): + # TODO: Properly escape non-printable chars without breaking unicode if self.env.username == 'root': clr = 'bad' else: @@ -160,5 +161,6 @@ class TitleBar(Widget): self.win.move(0, 0) for part in result: self.color(*part.lst) - self.addstr(str(part)) + y, x = self.win.getyx() + self.addstr(y, x, str(part)) self.color_reset() |