diff options
39 files changed, 864 insertions, 418 deletions
diff --git a/HACKING b/HACKING index 85f44ed6..b184150c 100644 --- a/HACKING +++ b/HACKING @@ -39,20 +39,16 @@ add the name of your option to the constant ALLOWED_SETTINGS The setting is now accessible at self.settings.my_option, assuming <self> is a "SettingsAware" object. -* Change commands: +* Changing commands, adding aliases: ranger/defaults/commands.py -* Create aliases for commands: -In ranger/defaults/commands.py -at the bottom, write something like: alias(exit=quit) - * Adding colorschemes: Copy ranger/colorschemes/default.py to ranger/colorschemes/myscheme.py and modify it according to your needs. Alternatively, mimic the jungle colorscheme. It subclasses the default scheme and just modifies a few things. In ranger/defaults/options.py (or ~/.ranger/options.py), change - colorscheme = colorschemes.default -to: colorscheme = colorschemes.myscheme + colorscheme = 'default' +to: colorscheme = 'myscheme' * Change which files are considered to be "hidden": In ranger/defaults/options.py @@ -71,7 +67,6 @@ extension, etc. For a full list, check ranger/fsobject/fsobject.py * Change the file extension => mime type associations: Modify ranger/data/mime.types -and run ranger/data/generate.py to compile it. Version Numbering diff --git a/Makefile b/Makefile index 05c565e6..1f75728c 100644 --- a/Makefile +++ b/Makefile @@ -1,92 +1,112 @@ +# Copyright (C) 2009, 2010 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/>. + NAME = ranger VERSION = 1.0.4 PYTHON ?= python DOCDIR ?= doc/pydoc -PREFIX ?= /usr/local -PYTHONOPTIMIZE ?= 2 +PREFIX ?= /usr +MANPREFIX ?= /share/man +PYOPTIMIZE ?= 1 +PYTHON_SITE_DEST ?= $(shell $(PYTHON) -c 'import sys; sys.stdout.write( \ + [p for p in sys.path if "site" in p][0])' 2> /dev/null) +BMCOUNT ?= 5 # how often to run the benchmarks? + CWD = $(shell pwd) -EDITOR ?= vim -DEST ?= $(shell $(PYTHON) -c 'import sys; sys.stdout.write( \ - [p for p in sys.path if "site" in p][0])' 2> /dev/null)/ranger -.PHONY: all compile clean doc cleandoc edit push test commit \ - install uninstall info snapshot minimal_snapshot +default: compile + @echo 'Run `make options` for a list of all options' -info: - @echo 'This makefile provides shortcuts for common tasks.' - @echo 'make clean: Remove all unnecessary files (.pyc, .pyo)' - @echo 'make cleandoc: Remove the pydoc documentation' +options: help + @echo + @echo 'Options:' + @echo 'PYTHON = $(PYTHON)' + @echo 'PYOPTIMIZE = $(PYOPTIMIZE)' + @echo 'PYTHON_SITE_DEST = $(PYTHON_SITE_DEST)' + @echo 'PREFIX = $(PREFIX)' + @echo 'MANPREFIX = $(MANPREFIX)' + @echo 'DOCDIR = $(DOCDIR)' + +help: + @echo 'make: Compile $(NAME)' @echo 'make doc: Create the pydoc documentation' @echo 'make install: Install ranger' + @echo 'make clean: Remove the compiled files (*.pyc, *.pyo)' + @echo 'make cleandoc: Remove the pydoc documentation' + @echo 'make uninstall: Uninstall ranger' @echo 'make snapshot: Create a tar.gz of the current git revision' - @echo - @echo 'For developers:' - @echo 'make commit: Test and commit the changes' @echo 'make test: Run all unittests.' - @echo 'make push: push the changes via git' - @echo 'make edit: open all relevant files in your editor' -all: test install +all: test compile install + +install: + @if [ '$(PYTHON_SITE_DEST)' == '' ]; then \ + echo -n 'Cannot find a suitable destination for the files.'; \ + echo ' Please install $(NAME) manually.'; \ + false; \ + fi + @echo "Installing $(NAME) version $(VERSION)..." + @mkdir -p $(PREFIX)/bin + cp -f ranger.py $(PREFIX)/bin/ranger + @mkdir -p $(PYTHON_SITE_DEST) + cp -fruT ranger $(PYTHON_SITE_DEST)/ranger + @chmod 755 $(PREFIX)/bin/ranger + @chmod -R +rX $(PYTHON_SITE_DEST)/ranger + @mkdir -p $(PREFIX)$(MANPREFIX)/man1 + cp -f doc/ranger.1 $(PREFIX)$(MANPREFIX)/man1/ranger.1 + @chmod 644 $(PREFIX)$(MANPREFIX)/man1/ranger.1 + +uninstall: + rm -f $(PREFIX)/bin/ranger + rm -f '$(PREFIX)$(MANPREFIX)/man1/ranger.1' + @if [ '$(PYTHON_SITE_DEST)' == '' ]; then \ + echo 'Cannot find a possible location of rangers library files'; \ + false; \ + fi + rm -rf '$(PYTHON_SITE_DEST)/ranger/*' + @echo 'NOTE: By default, configuration files are stored at "~/.ranger".' + @echo 'This script will not delete those.' compile: clean @echo 'Compiling...' - python -m compileall -q ranger - PYTHONOPTIMIZE=$(PYTHONOPTIMIZE) python -m compileall -q ranger + PYTHONOPTIMIZE=$(PYOPTIMIZE) python -m compileall -q ranger + +clean: + @echo 'Cleaning...' + find . -regex .\*.py[co]\$$ -exec rm -f -- {} \; doc: cleandoc + @echo 'Creating pydoc html documentation...' mkdir -p $(DOCDIR) cd $(DOCDIR); \ $(PYTHON) -c 'import pydoc, sys; \ sys.path[0] = "$(CWD)"; \ pydoc.writedocs("$(CWD)")' -uninstall: - @echo 'To uninstall ranger, please remove these files:' - @echo $(DEST)'/*' - @echo $(PREFIX)'/bin/ranger' - @echo 'and optionally the config files at:' - @echo '~/.ranger' - -install: compile - @if [ '$(DEST)' == '/ranger' ]; then \ - echo 'Cannot find a suitable destination for the files.'; \ - false; \ - fi - @echo "Installing..." - cp ranger.py $(PREFIX)/bin/ranger - cp -ruT ranger $(DEST) - chmod 755 $(PREFIX)/bin/ranger - chmod -R +rX $(DEST) - @echo '--------------------------------------' - @echo 'Finished.' - @echo 'If you use BASH or ZSH, you can activate an extra feature now:' - @echo 'When you exit ranger, the directory of the current shell can be' - @echo 'changed to the last visited directory in ranger. To do so, add' - @echo 'this alias to your shell rc file (like ~/.bashrc):' - @echo 'alias rng="source ranger ranger"' - @echo 'And run ranger by typing rng.' - - cleandoc: + @echo 'Removing pydoc html documentation...' test -d $(DOCDIR) && rm -f -- $(DOCDIR)/*.html -clean: - find . -regex .\*.py[co]\$$ -exec rm -f -- {} \; - test: - ./all_tests.py 1 + @./all_tests.py 1 -edit: - @$(EDITOR) ranger.py Makefile README COPYING HACKING INSTALL $(shell find ranger test -regex .\*py$ ) - -push: - @for repo in $(shell git remote); do \ - echo "Pushing to $$repo..."; \ - git push $$repo master; \ - done - -commit: test - @git citool +bm: + @./all_benchmarks.py $(BMCOUNT) snapshot: git archive HEAD | gzip > $(NAME)-$(VERSION)-$(shell git rev-parse HEAD | cut -b 1-8).tar.gz + +.PHONY: default options all compile clean doc cleandoc test bm \ + install uninstall snapshot diff --git a/TODO b/TODO index 169fa6d6..775605e9 100644 --- a/TODO +++ b/TODO @@ -55,6 +55,8 @@ General (X) #83 10/04/19 better ways to mark files. eg by regexp, filetype,.. ( ) #86 10/04/21 narg for move_parent ( ) #60 10/02/05 utf support improvable + ( ) #91 10/05/12 in keys.py, fm.move(right=N) should run with mode=N + ( ) #92 10/05/14 allow to enter the realpath of a directory Bugs @@ -81,6 +83,9 @@ Bugs (X) #74 10/03/21 console doesn't scroll (X) #78 10/03/31 broken preview when deleting all files in a directory (X) #85 10/04/26 no automatic reload of directory after using :filter + ( ) #87 10/05/10 files are not properly closed after previewing + ( ) #88 10/05/10 race conditions for data loading from FS + (X) #90 10/05/11 no link target for broken links Ideas @@ -89,14 +94,24 @@ Ideas ( ) #24 10/01/06 progress bar (X) #27 10/01/06 hide bookmarks in list which contain hidden dir (X) #28 10/01/06 use regexp instead of string for searching - ( ) #33 10/01/08 accelerate mousewheel speed + (X) #33 10/01/08 accelerate mousewheel speed + won't do this (X) #45 10/01/18 hooks for events like setting changes - ( ) #53 10/01/23 merge fm and environment - ( ) #68 10/03/10 threads, to seperate ui and loading + (X) #53 10/01/23 merge fm and environment + won't do this + (X) #68 10/03/10 threads, to seperate ui and loading + won't do this ( ) #72 10/03/21 ranger daemon which does the slow io tasks ( ) #75 10/03/28 navigate in history (X) #76 10/03/28 save history between sessions (X) #77 10/03/28 colorscheme overlay in options.py ( ) #82 10/04/19 :s command for batch renaming ( ) #84 10/04/25 use pygments for syntax highlighting + ( ) #89 10/05/10 branch view + + +Blocking + + ( ) #60 10/02/05 utf support improvable + ( ) #81 10/04/15 system crash when previewing /proc/kcore with root permissions diff --git a/all_benchmarks.py b/all_benchmarks.py new file mode 100755 index 00000000..73316658 --- /dev/null +++ b/all_benchmarks.py @@ -0,0 +1,58 @@ +#!/usr/bin/python +# Copyright (C) 2009, 2010 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/>. + +"""Run all the tests inside the test/ directory as a test suite.""" +if __name__ == '__main__': + from re import compile + from test import * + from time import time + from types import FunctionType as function + from sys import argv + bms = [] + try: + n = int(argv[1]) + except IndexError: + n = 10 + if len(argv) > 2: + args = [compile(re) for re in argv[2:]] + def allow(name): + for re in args: + if re.search(name): + return True + else: + return False + else: + allow = lambda name: True + for key, val in vars().copy().items(): + if key.startswith('bm_'): + bms.extend(v for k,v in vars(val).items() if type(v) == type) + for bmclass in bms: + for attrname in vars(bmclass): + if not attrname.startswith('bm_'): + continue + bmobj = bmclass() + t1 = time() + method = getattr(bmobj, attrname) + methodname = "{0}.{1}".format(bmobj.__class__.__name__, method.__name__) + if allow(methodname): + try: + method(n) + except: + print("{0} failed!".format(methodname)) + raise + else: + t2 = time() + print("{0:60}: {1:10}s".format(methodname, t2 - t1)) diff --git a/all_tests.py b/all_tests.py index 6693b870..90926918 100755 --- a/all_tests.py +++ b/all_tests.py @@ -1,4 +1,19 @@ #!/usr/bin/python +# Copyright (C) 2009, 2010 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/>. + """Run all the tests inside the test/ directory as a test suite.""" if __name__ == '__main__': import unittest diff --git a/ranger.py b/ranger.py index 754f0a8f..aca1b557 100755 --- a/ranger.py +++ b/ranger.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python -O # coding=utf-8 # # Ranger: Explore your forest of files from inside your terminal diff --git a/ranger/__main__.py b/ranger/__main__.py index d25065a1..6d574693 100644 --- a/ranger/__main__.py +++ b/ranger/__main__.py @@ -16,29 +16,19 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +# Most import statements in this module are inside the functions. +# This enables more convenient exception handling in ranger.py +# (ImportError will imply that this module can't be found) +# convenient exception handling in ranger.py (ImportError) + import os import sys -import ranger - -from optparse import OptionParser, SUPPRESS_HELP -from ranger.ext.openstruct import OpenStruct -from ranger import __version__, USAGE, DEFAULT_CONFDIR -import ranger.api.commands - -from signal import signal, SIGINT -from locale import getdefaultlocale, setlocale, LC_ALL - -from ranger.ext import curses_interrupt_handler -from ranger.core.runner import Runner -from ranger.core.fm import FM -from ranger.core.environment import Environment -from ranger.shared import (EnvironmentAware, FileManagerAware, - SettingsAware) -from ranger.gui.defaultui import DefaultUI as UI -from ranger.fsobject.file import File def parse_arguments(): """Parse the program arguments""" + from optparse import OptionParser, SUPPRESS_HELP + from ranger import __version__, USAGE, DEFAULT_CONFDIR + from ranger.ext.openstruct import OpenStruct parser = OptionParser(usage=USAGE, version='ranger ' + __version__) parser.add_option('-d', '--debug', action='store_true', @@ -83,6 +73,8 @@ def allow_access_to_confdir(confdir, allow): def load_settings(fm, clean): + import ranger + import ranger.api.commands if not clean: allow_access_to_confdir(ranger.arg.confdir, True) @@ -132,6 +124,7 @@ def load_settings(fm, clean): def load_apps(fm, clean): + import ranger if not clean: allow_access_to_confdir(ranger.arg.confdir, True) try: @@ -152,15 +145,28 @@ def main(): print(errormessage) print('ranger requires the python curses module. Aborting.') sys.exit(1) + from locale import getdefaultlocale, setlocale, LC_ALL + import ranger + from ranger.ext import curses_interrupt_handler + from ranger.core.runner import Runner + from ranger.core.fm import FM + from ranger.core.environment import Environment + from ranger.gui.defaultui import DefaultUI as UI + from ranger.fsobject import File + from ranger.shared import (EnvironmentAware, FileManagerAware, + SettingsAware) # Ensure that a utf8 locale is set. - if getdefaultlocale()[1] not in ('utf8', 'UTF-8'): - for locale in ('en_US.utf8', 'en_US.UTF-8'): - try: setlocale(LC_ALL, locale) - except: pass - else: break + try: + if getdefaultlocale()[1] not in ('utf8', 'UTF-8'): + for locale in ('en_US.utf8', 'en_US.UTF-8'): + try: setlocale(LC_ALL, locale) + except: pass + else: break + else: setlocale(LC_ALL, '') else: setlocale(LC_ALL, '') - else: setlocale(LC_ALL, '') + except: + print("Warning: Unable to set locale. Expect encoding problems.") arg = parse_arguments() ranger.arg = arg @@ -172,6 +178,8 @@ def main(): if arg.targets: target = arg.targets[0] + if target.startswith('file://'): + target = target[7:] if not os.access(target, os.F_OK): print("File or directory doesn't exist: %s" % target) sys.exit(1) @@ -190,6 +198,7 @@ def main(): # Initialize objects EnvironmentAware._assign(Environment(path)) fm = FM() + crash_exception = None try: load_settings(fm, ranger.arg.clean) FileManagerAware._assign(fm) @@ -199,11 +208,25 @@ def main(): fm.initialize() fm.ui.initialize() fm.loop() + except Exception as e: + crash_exception = e + if not (arg.debug or arg.clean): + import traceback + dumpname = ranger.relpath_conf('traceback') + traceback.print_exc(file=open(dumpname, 'w')) finally: fm.destroy() + if crash_exception: + print("Fatal: " + str(crash_exception)) + if arg.debug or arg.clean: + raise crash_exception + else: + print("A traceback has been saved to " + dumpname) + print("Please include it in a bugreport.") if __name__ == '__main__': - top_dir = os.path.dirname(sys.path[0]) - sys.path.insert(0, top_dir) + # The ranger directory can be executed directly, for example by typing + # python /usr/lib/python2.6/site-packages/ranger + sys.path.insert(0, os.path.dirname(sys.path[0])) main() diff --git a/ranger/api/commands.py b/ranger/api/commands.py index 55bb5b54..ca3f730d 100644 --- a/ranger/api/commands.py +++ b/ranger/api/commands.py @@ -48,13 +48,14 @@ class CommandContainer(object): def get_command(self, name, abbrev=True): if abbrev: lst = [cls for cmd, cls in self.commands.items() \ - if cmd.startswith(name) \ - and cls.allow_abbrev \ + if cls.allow_abbrev and cmd.startswith(name) \ or cmd == name] if len(lst) == 0: raise KeyError - if len(lst) == 1 or self.commands[name] in lst: + if len(lst) == 1: return lst[0] + if self.commands[name] in lst: + return self.commands[name] raise ValueError("Ambiguous command") else: try: @@ -80,7 +81,7 @@ class Command(FileManagerAware): def tab(self): """Override this""" - def quick_open(self): + def quick(self): """Override this""" def _tab_only_directories(self): diff --git a/ranger/core/environment.py b/ranger/core/environment.py index a485e277..296ba108 100644 --- a/ranger/core/environment.py +++ b/ranger/core/environment.py @@ -19,7 +19,7 @@ import pwd import socket from os.path import abspath, normpath, join, expanduser, isdir -from ranger.fsobject.directory import Directory, NoDirectoryGiven +from ranger.fsobject import Directory from ranger.container import KeyBuffer, KeyManager, History from ranger.ext.signal_dispatcher import SignalDispatcher from ranger.shared import SettingsAware @@ -179,12 +179,8 @@ class Environment(SettingsAware, SignalDispatcher): path = normpath(join(self.path, expanduser(path))) if not isdir(path): - return - - try: - new_cwd = self.get_directory(path) - except NoDirectoryGiven: return False + new_cwd = self.get_directory(path) try: os.chdir(path) diff --git a/ranger/core/fm.py b/ranger/core/fm.py index 7a55afa7..d2aa00ec 100644 --- a/ranger/core/fm.py +++ b/ranger/core/fm.py @@ -30,7 +30,7 @@ from ranger.container import Bookmarks from ranger.core.runner import Runner from ranger import relpath_conf from ranger.ext.get_executables import get_executables -from ranger.fsobject.directory import Directory +from ranger.fsobject import Directory from ranger.ext.signal_dispatcher import SignalDispatcher from ranger import __version__ from ranger.core.loader import Loader diff --git a/ranger/defaults/commands.py b/ranger/defaults/commands.py index bfe038c5..8728f9be 100644 --- a/ranger/defaults/commands.py +++ b/ranger/defaults/commands.py @@ -58,8 +58,8 @@ from ranger.api.commands import * alias('e', 'edit') alias('q', 'quit') -alias('q!', 'quit!') -alias('qall', 'quit!') +alias('q!', 'quitall') +alias('qall', 'quitall') class cd(Command): """ @@ -213,18 +213,27 @@ class quit(Command): self.fm.tab_close() -class quit_now(Command): +class quitall(Command): """ - :quit! + :quitall Quits the program immediately. """ - name = 'quit!' def execute(self): self.fm.exit() +class quit_bang(quitall): + """ + :quit! + + Quits the program immediately. + """ + name = 'quit!' + allow_abbrev = False + + class terminal(Command): """ :terminal @@ -358,7 +367,10 @@ class edit(Command): def execute(self): line = parse(self.line) - self.fm.edit_file(line.rest(1)) + if not line.chunk(1): + self.fm.edit_file(self.fm.env.cf.path) + else: + self.fm.edit_file(line.rest(1)) def tab(self): return self._tab_directory_content() @@ -403,7 +415,7 @@ class rename(Command): """ def execute(self): - from ranger.fsobject.file import File + from ranger.fsobject import File line = parse(self.line) if not line.rest(1): return self.fm.notify('Syntax: rename <newname>', bad=True) diff --git a/ranger/defaults/keys.py b/ranger/defaults/keys.py index 4dd7f280..b17e5e8f 100644 --- a/ranger/defaults/keys.py +++ b/ranger/defaults/keys.py @@ -162,26 +162,28 @@ map('T', fm.tag_remove()) map(' ', fm.mark(toggle=True)) map('v', fm.mark(all=True, toggle=True)) -map('V', fm.mark(all=True, val=False)) +map('V', 'uv', fm.mark(all=True, val=False)) # ------------------------------------------ file system operations map('yy', 'y<dir>', fm.copy()) map('dd', 'd<dir>', fm.cut()) -map('ud', fm.uncut()) map('pp', fm.paste()) map('po', fm.paste(overwrite=True)) map('pl', fm.paste_symlink()) map('p<bg>', fm.hint('press *p* once again to confirm pasting' \ ', or *l* to create symlinks')) +map('u<bg>', fm.hint("un*y*ank, unbook*m*ark, unselect:*v*")) +map('ud', 'uy', fm.uncut()) + # ---------------------------------------------------- run programs map('S', fm.execute_command(os.environ['SHELL'])) map('E', fm.edit_file()) map('du', fm.execute_command('du --max-depth=1 -h | less')) # -------------------------------------------------- toggle options -map('z<bg>', fm.hint("show_*h*idden *p*review_files *P*review_dirs " \ - "*d*irs_first flush*i*nput *m*ouse")) +map('z<bg>', fm.hint("[*cdfhimpPs*] show_*h*idden *p*review_files "\ + "*P*review_dirs *f*ilter flush*i*nput *m*ouse")) map('zh', fm.toggle_boolean_option('show_hidden')) map('zp', fm.toggle_boolean_option('preview_files')) map('zP', fm.toggle_boolean_option('preview_directories')) @@ -190,6 +192,7 @@ map('zd', fm.toggle_boolean_option('sort_directories_first')) map('zc', fm.toggle_boolean_option('collapse_preview')) map('zs', fm.toggle_boolean_option('sort_case_insensitive')) map('zm', fm.toggle_boolean_option('mouse_enabled')) +map('zf', fm.open_console(cmode.COMMAND, 'filter ')) # ------------------------------------------------------------ sort map('o<bg>', 'O<bg>', fm.hint("*s*ize *b*ase*n*ame *m*time" \ @@ -217,10 +220,14 @@ def append_to_filename(arg): command = 'rename ' + arg.fm.env.cf.basename arg.fm.open_console(cmode.COMMAND, command) +@map("I") +def insert_before_filename(arg): + append_to_filename(arg) + arg.fm.ui.console.move(right=len('rename '), absolute=True) + map('cw', fm.open_console(cmode.COMMAND, 'rename ')) map('cd', fm.open_console(cmode.COMMAND, 'cd ')) map('f', fm.open_console(cmode.COMMAND_QUICK, 'find ')) -map('bf', fm.open_console(cmode.COMMAND, 'filter ')) map('d<bg>', fm.hint('d*u* (disk usage) d*d* (cut)')) map('@', fm.open_console(cmode.OPEN, '@')) map('#', fm.open_console(cmode.OPEN, 'p!')) @@ -243,7 +250,10 @@ map('gR', fm.cd(RANGERDIR)) map('gc', '<C-W>', fm.tab_close()) map('gt', '<TAB>', fm.tab_move(1)) map('gT', '<S-TAB>', fm.tab_move(-1)) -map('gn', '<C-N>', fm.tab_new()) +@map('gn', '<C-N>') +def newtab_and_gohome(arg): + arg.fm.tab_new() + arg.fm.cd('~') # To return to the original directory, type `` for n in range(1, 10): map('g' + str(n), fm.tab_open(n)) map('<A-' + str(n) + '>', fm.tab_open(n)) @@ -258,7 +268,7 @@ map('ct', fm.search(order='tag')) map('cc', fm.search(order='ctime')) map('cm', fm.search(order='mimetype')) map('cs', fm.search(order='size')) -map('c<bg>', fm.hint('*c*time *m*imetype *s*ize')) +map('c<bg>', fm.hint('*c*time *m*imetype *s*ize *t*ag *w*:rename')) # ------------------------------------------------------- bookmarks for key in ALLOWED_BOOKMARK_KEYS: @@ -266,6 +276,7 @@ for key in ALLOWED_BOOKMARK_KEYS: map("m" + key, fm.set_bookmark(key)) map("um" + key, fm.unset_bookmark(key)) map("`<bg>", "'<bg>", "m<bg>", fm.draw_bookmarks()) +map('um<bg>', fm.hint("delete which bookmark?")) # ---------------------------------------------------- change views map('i', fm.display_file()) @@ -350,18 +361,18 @@ map = keymanager.get_context('console') map.merge(global_keys) map.merge(readline_aliases) -map('<up>', wdg.history_move(-1)) -map('<down>', wdg.history_move(1)) +map('<up>', '<C-P>', wdg.history_move(-1)) +map('<down>', '<C-N>', wdg.history_move(1)) map('<home>', wdg.move(right=0, absolute=True)) map('<end>', wdg.move(right=-1, absolute=True)) map('<tab>', wdg.tab()) map('<s-tab>', wdg.tab(-1)) -map('<c-c>', '<esc>', wdg.close()) +map('<C-C>', '<C-D>', '<ESC>', wdg.close()) map('<CR>', '<c-j>', wdg.execute()) map('<F1>', lambda arg: arg.fm.display_command_help(arg.wdg)) map('<backspace>', wdg.delete(-1)) -map('<delete>', wdg.delete(1)) +map('<delete>', wdg.delete(0)) map('<C-W>', wdg.delete_word()) map('<C-K>', wdg.delete_rest(1)) map('<C-U>', wdg.delete_rest(-1)) diff --git a/ranger/defaults/options.py b/ranger/defaults/options.py index ac074e1c..d93d7685 100644 --- a/ranger/defaults/options.py +++ b/ranger/defaults/options.py @@ -88,7 +88,7 @@ max_history_size = 20 max_console_history_size = 20 # Try to keep so much space between the top/bottom border when scrolling: -scroll_offset = 2 +scroll_offset = 8 # Flush the input after each key hit? (Noticable when ranger lags) flushinput = True diff --git a/ranger/ext/accumulator.py b/ranger/ext/accumulator.py index 2e599c85..75f58ad7 100644 --- a/ranger/ext/accumulator.py +++ b/ranger/ext/accumulator.py @@ -68,6 +68,7 @@ class Accumulator(object): return self.move(to=self.pointer) + # XXX Is this still necessary? move() ensures correct pointer position def correct_pointer(self): lst = self.get_list() diff --git a/ranger/ext/human_readable.py b/ranger/ext/human_readable.py index 1a3d1ec9..beeaf6d3 100644 --- a/ranger/ext/human_readable.py +++ b/ranger/ext/human_readable.py @@ -24,7 +24,7 @@ def human_readable(byte, seperator=' '): return '0' exponent = int(math.log(byte, 2) / 10) - flt = float(byte) / (1 << (10 * exponent)) + flt = round(float(byte) / (1 << (10 * exponent)), 2) if exponent > MAX_EXPONENT: return '>9000' # off scale diff --git a/ranger/ext/spawn.py b/ranger/ext/spawn.py new file mode 100644 index 00000000..9723c1ed --- /dev/null +++ b/ranger/ext/spawn.py @@ -0,0 +1,27 @@ +# Copyright (C) 2009, 2010 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/>. + +from subprocess import Popen, PIPE +ENCODING = 'utf-8' + +def spawn(*args): + """Runs a program, waits for its termination and returns its stdout""" + if len(args) == 1: + popen_arguments = args[0] + else: + popen_arguments = args + process = Popen(popen_arguments, stdout=PIPE) + stdout, stderr = process.communicate() + return stdout.decode(ENCODING) diff --git a/ranger/fsobject/__init__.py b/ranger/fsobject/__init__.py index 10997df6..5fb4b877 100644 --- a/ranger/fsobject/__init__.py +++ b/ranger/fsobject/__init__.py @@ -16,16 +16,9 @@ """FileSystemObjects are representation of files and directories with fast access to their properties through caching""" -T_FILE = 'file' -T_DIRECTORY = 'directory' -T_UNKNOWN = 'unknown' -T_NONEXISTANT = 'nonexistant' - BAD_INFO = '?' -class NotLoadedYet(Exception): - pass - +# So they can be imported from other files more easily: from .fsobject import FileSystemObject from .file import File -from .directory import Directory, NoDirectoryGiven +from .directory import Directory diff --git a/ranger/fsobject/directory.py b/ranger/fsobject/directory.py index 43af772a..60387e7b 100644 --- a/ranger/fsobject/directory.py +++ b/ranger/fsobject/directory.py @@ -13,10 +13,14 @@ # 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 +import os.path +import stat +from stat import S_ISLNK, S_ISDIR +from os.path import join, isdir, basename from collections import deque from time import time +from ranger.ext.mount_path import mount_path from ranger.fsobject import BAD_INFO, File, FileSystemObject from ranger.shared import SettingsAware from ranger.ext.accumulator import Accumulator @@ -34,8 +38,17 @@ def sort_by_directory(path): """returns 0 if path is a directory, otherwise 1 (for sorting)""" return 1 - path.is_directory -class NoDirectoryGiven(Exception): - pass +def accept_file(fname, hidden_filter, name_filter): + if hidden_filter: + try: + if hidden_filter.search(fname): + return False + except: + if hidden_filter in fname: + return False + if name_filter and name_filter not in fname: + return False + return True class Directory(FileSystemObject, Accumulator, SettingsAware): is_directory = True @@ -68,14 +81,11 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): 'type': lambda path: path.mimetype, } - def __init__(self, path): - from os.path import isfile - - if isfile(path): - raise NoDirectoryGiven() + def __init__(self, path, **kw): + assert not os.path.isfile(path), "No directory given!" Accumulator.__init__(self) - FileSystemObject.__init__(self, path) + FileSystemObject.__init__(self, path, **kw) self.marked_items = list() @@ -150,33 +160,19 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): in each iteration. """ - # log("generating loader for " + self.path + "(" + str(id(self)) + ")") - from os.path import join, isdir, basename - from os import listdir - import ranger.ext.mount_path - self.loading = True self.load_if_outdated() try: if self.exists and self.runnable: yield - self.mount_path = ranger.ext.mount_path.mount_path(self.path) - - filenames = [] - for fname in listdir(self.path): - if not self.settings.show_hidden: - hfilter = self.settings.hidden_filter - if hfilter: - if isinstance(hfilter, str) and hfilter in fname: - continue - if hasattr(hfilter, 'search') and \ - hfilter.search(fname): - continue - if isinstance(self.filter, str) and self.filter \ - and self.filter not in fname: - continue - filenames.append(join(self.path, fname)) + self.mount_path = mount_path(self.path) + + hidden_filter = not self.settings.show_hidden \ + and self.settings.hidden_filter + filenames = [join(self.path, fname) \ + for fname in os.listdir(self.path) \ + if accept_file(fname, hidden_filter, self.filter)] yield self.load_content_mtime = os.stat(self.path).st_mtime @@ -185,13 +181,25 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): files = [] for name in filenames: - if isdir(name): + try: + file_lstat = os.lstat(name) + if S_ISLNK(file_lstat.st_mode): + file_stat = os.stat(name) + else: + file_stat = file_lstat + stats = (file_stat, file_lstat) + is_a_dir = S_ISDIR(file_stat.st_mode) + except: + stats = None + is_a_dir = False + if is_a_dir: try: item = self.fm.env.get_directory(name) except: - item = Directory(name) + item = Directory(name, preload=stats, + path_is_abs=True) else: - item = File(name) + item = File(name, preload=stats, path_is_abs=True) item.load_if_outdated() files.append(item) yield @@ -205,9 +213,10 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): self._clear_marked_items() for item in self.files: if item.path in marked_paths: - self.mark_item(item, True) + item._mark(True) + self.marked_items.append(item) else: - self.mark_item(item, False) + item._mark(False) self.sort() @@ -402,8 +411,8 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): def __len__(self): """The number of containing files""" - if not self.accessible or not self.content_loaded: - raise ranger.fsobject.NotLoadedYet() + assert self.accessible + assert self.content_loaded assert self.files is not None return len(self.files) diff --git a/ranger/fsobject/file.py b/ranger/fsobject/file.py index aa44016e..4618df33 100644 --- a/ranger/fsobject/file.py +++ b/ranger/fsobject/file.py @@ -13,11 +13,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -control_characters = set(chr(n) for n in set(range(0, 9)) | set(range(14,32))) N_FIRST_BYTES = 20 +control_characters = set(chr(n) for n in + set(range(0, 9)) | set(range(14, 32))) -from .fsobject import FileSystemObject as SuperClass -class File(SuperClass): +from ranger.fsobject import FileSystemObject +class File(FileSystemObject): is_file = True @property @@ -37,4 +38,3 @@ class File(SuperClass): if self.firstbytes and control_characters & set(self.firstbytes): return True return False - diff --git a/ranger/fsobject/fsobject.py b/ranger/fsobject/fsobject.py index f3d40614..6bb8b6ec 100644 --- a/ranger/fsobject/fsobject.py +++ b/ranger/fsobject/fsobject.py @@ -14,19 +14,17 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. CONTAINER_EXTENSIONS = 'rar zip tar gz bz bz2 tgz 7z iso cab'.split() -DOCUMENT_EXTENSIONS = 'pdf doc ppt odt'.split() -DOCUMENT_BASENAMES = 'README TODO LICENSE COPYING INSTALL'.split() -DOCUMENT_EXTENSIONS = () -DOCUMENT_BASENAMES = () import stat import os +from stat import S_ISLNK, S_ISCHR, S_ISBLK, S_ISSOCK, S_ISFIFO, \ + S_ISDIR, S_IXUSR from time import time -from subprocess import Popen, PIPE from os.path import abspath, basename, dirname, realpath -from . import T_FILE, T_DIRECTORY, T_UNKNOWN, T_NONEXISTANT, BAD_INFO +from . import BAD_INFO from ranger.shared import MimeTypeAware, FileManagerAware from ranger.ext.shell_escape import shell_escape +from ranger.ext.spawn import spawn from ranger.ext.human_readable import human_readable class FileSystemObject(MimeTypeAware, FileManagerAware): @@ -55,7 +53,6 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): stat = None infostring = None permissions = None - type = T_UNKNOWN size = 0 last_used = None @@ -70,15 +67,17 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): container = False mimetype_tuple = () - def __init__(self, path): + def __init__(self, path, preload=None, path_is_abs=False): MimeTypeAware.__init__(self) - path = abspath(path) + if not path_is_abs: + path = abspath(path) self.path = path self.basename = basename(path) self.basename_lower = self.basename.lower() self.dirname = dirname(path) self.realpath = self.path + self.preload = preload try: lastdot = self.basename.rindex('.') + 1 @@ -86,7 +85,6 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): except ValueError: self.extension = None - self.set_mimetype() self.use() def __repr__(self): @@ -102,8 +100,7 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): def filetype(self): if self._filetype is None: try: - got = Popen(["file", '-Lb', '--mime-type', self.path], - stdout=PIPE).communicate()[0] + got = spawn(["file", '-Lb', '--mime-type', self.path]) except OSError: self._filetype = '' else: @@ -129,24 +126,38 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): def set_mimetype(self): """assign attributes such as self.video according to the mimetype""" - self.mimetype = self.mimetypes.guess_type(self.basename, False)[0] - if self.mimetype is None: - self.mimetype = '' + self._mimetype = self.mimetypes.guess_type(self.basename, False)[0] + if self._mimetype is None: + self._mimetype = '' - self.video = self.mimetype.startswith('video') - self.image = self.mimetype.startswith('image') - self.audio = self.mimetype.startswith('audio') + self.video = self._mimetype.startswith('video') + self.image = self._mimetype.startswith('image') + self.audio = self._mimetype.startswith('audio') self.media = self.video or self.image or self.audio - self.document = self.mimetype.startswith('text') \ - or (self.extension in DOCUMENT_EXTENSIONS) \ - or (self.basename in DOCUMENT_BASENAMES) + self.document = self._mimetype.startswith('text') self.container = self.extension in CONTAINER_EXTENSIONS keys = ('video', 'audio', 'image', 'media', 'document', 'container') - self.mimetype_tuple = tuple(key for key in keys if getattr(self, key)) + self._mimetype_tuple = tuple(key for key in keys if getattr(self, key)) - if self.mimetype == '': - self.mimetype = None + if self._mimetype == '': + self._mimetype = None + + @property + def mimetype(self): + try: + return self._mimetype + except: + self.set_mimetype() + return self._mimetype + + @property + def mimetype_tuple(self): + try: + return self._mimetype_tuple + except: + self.set_mimetype() + return self._mimetype_tuple def mark(self, boolean): directory = self.env.get_directory(self.dirname) @@ -166,20 +177,20 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): self.infostring = 'sock' elif self.is_directory: try: - self.size = len(os.listdir(self.path)) + self.size = len(os.listdir(self.path)) # bite me except OSError: self.infostring = BAD_INFO self.accessible = False else: - self.infostring = " %d" % self.size + self.infostring = ' %d' % self.size self.accessible = True self.runnable = True elif self.is_file: - try: + if self.stat: self.size = self.stat.st_size self.infostring = ' ' + human_readable(self.size) - except: - pass + else: + self.infostring = BAD_INFO if self.is_link: self.infostring = '->' + self.infostring @@ -190,42 +201,48 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): """ self.loaded = True - try: - self.stat = os.lstat(self.path) - except OSError: - self.stat = None - self.is_link = False - self.accessible = False - else: - self.is_link = stat.S_ISLNK(self.stat.st_mode) + + # Get the stat object, either from preload or from os.[l]stat + self.stat = None + if self.preload: + self.stat = self.preload[1] + self.is_link = S_ISLNK(self.stat.st_mode) if self.is_link: - try: # try to resolve the link - self.readlink = os.readlink(self.path) - self.realpath = realpath(self.path) - self.stat = os.stat(self.path) - except: # it failed, so it must be a broken link - pass + self.stat = self.preload[0] + self.preload = None + else: + try: + self.stat = os.lstat(self.path) + except: + pass + else: + self.is_link = S_ISLNK(self.stat.st_mode) + if self.is_link: + try: + self.stat = os.stat(self.path) + except: + pass + + # Set some attributes + if self.stat: mode = self.stat.st_mode - self.is_device = bool(stat.S_ISCHR(mode) or stat.S_ISBLK(mode)) - self.is_socket = bool(stat.S_ISSOCK(mode)) - self.is_fifo = bool(stat.S_ISFIFO(mode)) - self.accessible = True - - if self.accessible and os.access(self.path, os.F_OK): - self.exists = True + self.is_device = bool(S_ISCHR(mode) or S_ISBLK(mode)) + self.is_socket = bool(S_ISSOCK(mode)) + self.is_fifo = bool(S_ISFIFO(mode)) self.accessible = True - if os.path.isdir(self.path): - self.type = T_DIRECTORY - self.runnable = bool(mode & stat.S_IXUSR) - elif os.path.isfile(self.path): - self.type = T_FILE + if os.access(self.path, os.F_OK): + self.exists = True + if S_ISDIR(mode): + self.runnable = bool(mode & S_IXUSR) else: - self.type = T_UNKNOWN - + self.exists = False + self.runnable = False + if self.is_link: + self.realpath = realpath(self.path) + self.readlink = os.readlink(self.path) else: - self.type = T_NONEXISTANT - self.exists = False - self.runnable = False + self.accessible = False + self.determine_infostring() def get_permission_string(self): @@ -237,9 +254,9 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): except: return '----??----' - if stat.S_ISDIR(mode): + if S_ISDIR(mode): perms = ['d'] - elif stat.S_ISLNK(mode): + elif S_ISLNK(mode): perms = ['l'] else: perms = ['-'] diff --git a/ranger/gui/mouse_event.py b/ranger/gui/mouse_event.py index f3955825..4a2860b8 100644 --- a/ranger/gui/mouse_event.py +++ b/ranger/gui/mouse_event.py @@ -21,6 +21,7 @@ class MouseEvent(object): curses.BUTTON2_PRESSED, curses.BUTTON3_PRESSED, curses.BUTTON4_PRESSED ] + CTRL_SCROLLWHEEL_MULTIPLIER = 5 def __init__(self, getmouse): """Creates a MouseEvent object from the result of win.getmouse()""" @@ -42,11 +43,15 @@ class MouseEvent(object): return False def mouse_wheel_direction(self): + """Returns the direction of the scroll action, 0 if there was none""" + # If the bstate > ALL_MOUSE_EVENTS, it's an invalid mouse button. + # I interpret invalid buttons as "scroll down" because all tested + # systems have a broken curses implementation and this is a workaround. if self.bstate & curses.BUTTON4_PRESSED: - return -1 + return self.ctrl() and -self.CTRL_SCROLLWHEEL_MULTIPLIER or -1 elif self.bstate & curses.BUTTON2_PRESSED \ or self.bstate > curses.ALL_MOUSE_EVENTS: - return 1 + return self.ctrl() and self.CTRL_SCROLLWHEEL_MULTIPLIER or 1 else: return 0 diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py index e40003a2..e9c20395 100644 --- a/ranger/gui/ui.py +++ b/ranger/gui/ui.py @@ -185,12 +185,12 @@ class UI(DisplayableContainer): keys = [27, keys[1] - 128] self.handle_keys(*keys) self.set_load_mode(previous_load_mode) - if self.settings.flushinput: + if self.settings.flushinput and not self.console.visible: curses.flushinp() else: # Handle simple key presses, CTRL+X, etc here: if key > 0: - if self.settings.flushinput: + if self.settings.flushinput and not self.console.visible: curses.flushinp() if key == curses.KEY_MOUSE: self.handle_mouse() diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index e4ba4b64..6e020e5f 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -87,7 +87,8 @@ class BrowserColumn(Pager): def click(self, event): """Handle a MouseEvent""" - if not (event.pressed(1) or event.pressed(3)): + direction = event.mouse_wheel_direction() + if not (event.pressed(1) or event.pressed(3) or direction): return False if self.target is None: @@ -97,7 +98,12 @@ class BrowserColumn(Pager): if self.target.accessible and self.target.content_loaded: index = self.scroll_begin + event.y - self.y - if event.pressed(1): + if direction: + if self.level == -1: + self.fm.move_parent(direction) + else: + return False + elif event.pressed(1): if not self.main_column: self.fm.enter_dir(self.target.path) diff --git a/ranger/gui/widgets/browserview.py b/ranger/gui/widgets/browserview.py index 33418a2f..a90231f2 100644 --- a/ranger/gui/widgets/browserview.py +++ b/ranger/gui/widgets/browserview.py @@ -24,7 +24,7 @@ from ..displayable import DisplayableContainer class BrowserView(Widget, DisplayableContainer): ratios = None preview = True - preview_available = True + is_collapsed = False draw_bookmarks = False stretch_ratios = None need_clear = False @@ -196,6 +196,11 @@ class BrowserView(Widget, DisplayableContainer): except: pass + def _collapse(self): + # Should the last column be cut off? (Because there is no preview) + return self.settings.collapse_preview and self.preview and \ + not self.columns[-1].has_preview() and self.stretch_ratios + def resize(self, y, x, hei, wid): """Resize all the columns according to the given ratio""" DisplayableContainer.resize(self, y, x, hei, wid) @@ -203,10 +208,8 @@ class BrowserView(Widget, DisplayableContainer): pad = 1 if borders else 0 left = pad - cut_off_last = self.preview and not self.preview_available \ - and self.stretch_ratios - - if cut_off_last: + self.is_collapsed = self._collapse() + if self.is_collapsed: generator = enumerate(self.stretch_ratios) else: generator = enumerate(self.ratios) @@ -232,12 +235,12 @@ class BrowserView(Widget, DisplayableContainer): left += wid def click(self, event): - n = event.ctrl() and 5 or 1 + if DisplayableContainer.click(self, event): + return True direction = event.mouse_wheel_direction() if direction: self.main_column.scroll(direction) - else: - DisplayableContainer.click(self, event) + return False def open_pager(self): self.pager.visible = True @@ -263,8 +266,5 @@ class BrowserView(Widget, DisplayableContainer): def poke(self): DisplayableContainer.poke(self) - if self.settings.collapse_preview and self.preview: - has_preview = self.columns[-1].has_preview() - if self.preview_available != has_preview: - self.preview_available = has_preview - self.resize(self.y, self.x, self.hei, self.wid) + if self.preview and self.is_collapsed != self._collapse(): + self.resize(self.y, self.x, self.hei, self.wid) diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py index 59a779de..fa9e438e 100644 --- a/ranger/gui/widgets/console.py +++ b/ranger/gui/widgets/console.py @@ -452,17 +452,29 @@ class OpenConsole(ConsoleWithTab): if file.shell_escaped_basename.startswith(start_of_word)) def _substitute_metachars(self, command): - dct = {} + macros = {} if self.fm.env.cf: - dct['f'] = shell_quote(self.fm.env.cf.basename) + macros['f'] = shell_quote(self.fm.env.cf.basename) else: - dct['f'] = '' + macros['f'] = '' - dct['s'] = ' '.join(shell_quote(fl.basename) \ + macros['s'] = ' '.join(shell_quote(fl.basename) \ for fl in self.fm.env.get_selection()) - return _CustomTemplate(command).safe_substitute(dct) + macros['c'] = ' '.join(shell_quote(fl.path) + for fl in self.fm.env.copy) + + macros['t'] = ' '.join(shell_quote(fl.basename) + for fl in self.fm.env.cwd.files + if fl.realpath in self.fm.tags) + + if self.fm.env.cwd: + macros['d'] = shell_quote(self.fm.env.cwd.path) + else: + macros['d'] = '.' + + return _CustomTemplate(command).safe_substitute(macros) def _parse(self): if '!' in self.line: diff --git a/ranger/help/__init__.py b/ranger/help/__init__.py index 1a651d56..f304c7bc 100644 --- a/ranger/help/__init__.py +++ b/ranger/help/__init__.py @@ -24,7 +24,8 @@ NO_HELP = """No help was found. Possibly the program was invoked with "python -OO" which discards all documentation.""" -HELP_TOPICS = ('index', 'movement', 'starting', 'console', 'fileop') +HELP_TOPICS = ('index', 'movement', 'starting', 'console', 'fileop', + 'invocation') def get_docstring_of_module(path, module_name): imported = __import__(path, fromlist=[module_name]) diff --git a/ranger/help/console.py b/ranger/help/console.py index 04372f38..d1764841 100644 --- a/ranger/help/console.py +++ b/ranger/help/console.py @@ -19,6 +19,7 @@ 3.2. List of Commands 3.3. The (Quick) Command Console 3.4. The Open Console +3.5. The Quick Open Console ============================================================================== @@ -144,9 +145,9 @@ ranger/ext/command_parser.py is used. The tab method should return None, a string or an iterable sequence containing the strings which should be cycled through by pressing tab. -Only those commands which implement the quick_open method will be specially +Only those commands which implement the quick() method will be specially treated by the Quick Command Console. For the rest, both consoles are equal. -quick_open is called after each key press and if it returns True, the +quick() is called after each key press and if it returns True, the command will be executed immediately. @@ -163,8 +164,21 @@ Open this console by pressing "!" or "s" The Open Console allows you to execute shell commands: !vim * will run vim and open all files in the directory. -%f will be replaced with the basename of the highlighted file -%s will be selected with all files in the selection +Like in similar filemanagers there are some macros. Use them in +commands and they will be replaced with a list of files. + %f the highlighted file + %d the path of the current directory + %s the selected files in the current directory. If no files are + selected, it defaults to the same as %f + %t all tagged files in the current directory + %c the full paths of the currently copied/cut files + +%c is the only macro which ranges out of the current directory. So you may +"abuse" the copying function for other purposes, like diffing two files which +are in different directories: + + Yank the file A (type yy), move to the file B and use: + !p!diff %c %f There is a special syntax for more control: @@ -175,7 +189,12 @@ There is a special syntax for more control: Those two can be combinated: !d!@mplayer will open the selection with a detached mplayer - (again, this is equivalent to !d!mplayer %s) + (again, this is equivalent to !d!mplayer %s) + +These keys open the console with a predefined text: + @ "!@" Suffixes %s. Good for things like "@mount" + # "!p!" Pipes output through a pager. For commands with output. + Note: A plain "!p!" will be translated to "!p!cat %f" For a list of other flags than "d", check chapter 2.5 of the documentation @@ -207,7 +226,7 @@ open with: 1 open it with the default handler in mode 1 open with: d open it detached with the default handler open with: p open it as usual, but pipe the output to "less" open with: totem 1 Ds open in totem in mode 1, will not detach the - process (flag D) but discard the output (flag s) + process (flag D) but discard the output (flag s) ============================================================================== diff --git a/ranger/help/index.py b/ranger/help/index.py index 794e3ea9..316e975f 100644 --- a/ranger/help/index.py +++ b/ranger/help/index.py @@ -26,6 +26,7 @@ |2?| Running Files |3?| The console |4?| File operations + |5?| Ranger invocation ============================================================================== diff --git a/ranger/help/invocation.py b/ranger/help/invocation.py new file mode 100644 index 00000000..3de574cc --- /dev/null +++ b/ranger/help/invocation.py @@ -0,0 +1,106 @@ +# Copyright (C) 2009, 2010 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/>. +""" +5. Ranger invocation + +5.1. Command Line Arguments +5.2. Python Options + + +============================================================================== +5.1. Command Line Arguments + +These options can be passed to ranger when starting it from the +command line. + +--version + Print the version and exit. + +-h, --help + Print a list of options and exit. + +-d, --debug + Activate the debug mode: Whenever an error occurs, ranger + will exit and print a full backtrace. The default behaviour + is to merely print the name of the exception in the statusbar/log + and to try to keep running. + +-c, --clean + Activate the clean mode: Ranger will not access or create any + configuration files nor will it leave any traces on your system. + This is useful when your configuration is broken, when you want + to avoid clutter, etc. + +--fail-if-run + Return the exit code 1 if ranger is used to run a file, for example + with `ranger --fail-if-run filename`. This can be useful for scripts. + +-r <dir>, --confdir=<dir> + Define a different configuration directory. The default is + $HOME/.ranger. + +-m <n>, --mode=<n> + When a filename is supplied, make it run in mode <n> |2| + +-f <flags>, --flags=<flags> + When a filename is supplied, run it with the flags <flags> |2| + +(Optional) Positional Argument + The positional argument should be a path to the directory you + want ranger to start in, or the file which you want to run. + Only one positional argument is accepted as of now. + +-- + Stop looking for options. All following arguments are treated as + positional arguments. + +Examples: + ranger episode1.avi + ranger --debug /usr/bin + ranger --confdir=~/.config/ranger --fail-if-run + + +============================================================================== +5.2. Python Options + +Ranger makes use of python optimize flags. To use them, run ranger like this: + PYTHONOPTIMIZE=1 ranger +An alternative is: + python -O `which ranger` +Or you could change the first line of the ranger script and add -O/-OO. +The first way is the recommended one. Of course you can make an alias or +a shell fuction to save typing. + +Using PYTHONOPTIMIZE=1 (-O) will make python discard assertion statements. +Assertions are little pieces of code which are helpful for finding errors, +but unless you're touching sensitive parts of ranger, you may want to +disable them to save some computing power. + +Using PYTHONOPTIMIZE=2 (-OO) will additionally discard any docstrings. +In ranger, most built-in documentation (F1/? keys) is implemented with +docstrings. Use this option if you don't need the documentation. + +Examples: + PYTHONOPTIMIZE=1 ranger episode1.avi + PYTHONOPTIMIZE=2 ranger --debug /usr/bin + python -OO `which ranger` --confdir=~/.config/ranger --fail-if-run + +Note: The author expected "-OO" to reduce the memory usage, but that +doesn't seem to happen. + + +============================================================================== +""" +# vim:tw=78:sw=8:sts=8:ts=8:ft=help diff --git a/ranger/help/movement.py b/ranger/help/movement.py index e85bf336..194dd3a9 100644 --- a/ranger/help/movement.py +++ b/ranger/help/movement.py @@ -19,10 +19,11 @@ 1.1. Move around 1.2. Browser control 1.3. Searching -1.4. Cycling +1.4. Sorting 1.5. Bookmarks 1.6. Tabs 1.7. Mouse usage +1.8. Misc keys ============================================================================== @@ -64,6 +65,15 @@ These keys work like in vim: ^B move up by one screen ^F move down by one screen +This keys can be used to make movements beyond the current directory + + ] move down in the parent directory + [ move up in the parent directory + + } traverse the directory tree, visiting each directory + { traverse in the other direction. (not implemented yet, + currently this only moves back in history) + ============================================================================== 1.2. Browser control @@ -74,6 +84,7 @@ These keys work like in vim: ^L redraw the window : open the console |3?| z toggle options + u undo certain things (unyank, unmark,...) i inspect the content of the file E edit the file @@ -88,7 +99,7 @@ of the file you're pointing at. V remove all marks By "tagging" files, you can highlight them and mark them to be -special in whatever context you want. +special in whatever context you want. Tags are persistent across sessions. t tag/untag the selection T untag the selection @@ -115,10 +126,10 @@ visible files. Pressing "n" will move you to the next occurance, "N" to the previous one. You can search for more than just strings: - ct search tagged files - cc cycle through all files by their ctime (last modification) + cc cycle through all files by their ctime (last inode change) cm cycle by mime type, connecting similar files cs cycle by size, large items first + ct search tagged files ============================================================================== @@ -139,7 +150,9 @@ be reversed. 1.5. Bookmarks Type "m<key>" to bookmark the current directory. You can re-enter this -directory by typing "`<key>". <key> can be any letter or digit. +directory by typing "`<key>". <key> can be any letter or digit. Unlike vim, +both lowercase and uppercase bookmarks are persistent. + Each time you jump to a bookmark, the special bookmark at key ` will be set to the last directory. So typing "``" gets you back to where you were before. @@ -155,8 +168,9 @@ In Ranger, tabs are very simple though and only store the directory path. gt Go to the next tab. (also TAB) gT Go to the previous tab. (also Shift+TAB) gn, ^N Create a new tab - g<N> Open a tab. N has to be a number from 0 to 9. + g<N> Open a tab. N has to be a number from 1 to 9. If the tab doesn't exist yet, it will be created. + On most terminals, Alt-1, Alt-2, etc., also work. gc, ^W Close the current tab. The last tab cannot be closed. @@ -172,5 +186,16 @@ Clicking into the preview window will usually run the file. |2?| ============================================================================== +1.8 Misc keys + + ^P Display the message log + du Display the disk usage of the current directory + cd Open the console with ":cd " + cw Open the console with ":rename " + A Open the console with ":rename <current filename>" + I Same as A, put the cursor at the beginning of the filename + + +============================================================================== """ # vim:tw=78:sw=4:sts=8:ts=8:ft=help diff --git a/ranger/help/starting.py b/ranger/help/starting.py index 8fd8b0da..069d6a04 100644 --- a/ranger/help/starting.py +++ b/ranger/help/starting.py @@ -37,7 +37,7 @@ use them. Otherwise use the file under the cursor. ============================================================================== -2.2. open with: +2.2. The "open with" prompt If the automatic filetype detection fails or starts the file in a wrong way, you can press "r" to manually tell ranger how to run it. diff --git a/ranger/shared/mimetype.py b/ranger/shared/mimetype.py index 1a7f79a0..c6577056 100644 --- a/ranger/shared/mimetype.py +++ b/ranger/shared/mimetype.py @@ -17,9 +17,7 @@ from ranger import relpath import mimetypes class MimeTypeAware(object): mimetypes = {} - __initialized = False def __init__(self): - if not MimeTypeAware.__initialized: - MimeTypeAware.mimetypes = mimetypes.MimeTypes() - MimeTypeAware.mimetypes.read(relpath('data/mime.types')) - MimeTypeAware.__initialized = True + MimeTypeAware.__init__ = lambda _: None # refuse multiple inits + MimeTypeAware.mimetypes = mimetypes.MimeTypes() + MimeTypeAware.mimetypes.read(relpath('data/mime.types')) diff --git a/test/__init__.py b/test/__init__.py index c2f9bde2..d87d1fc2 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -17,7 +17,15 @@ import os, sys __all__ = [ x[0:x.index('.')] \ for x in os.listdir(os.path.dirname(__file__)) \ - if x.startswith('tc_') ] + if x.startswith('tc_') or x.startswith('bm_')] + +def TODO(fnc): + def result(*arg, **kw): + try: + fnc(*arg, **kw) + except: + pass # failure expected + return result def init(): sys.path.append(os.path.abspath(os.path.join(sys.path[0], '..'))) diff --git a/test/bm_loader.py b/test/bm_loader.py new file mode 100644 index 00000000..4bfc2db9 --- /dev/null +++ b/test/bm_loader.py @@ -0,0 +1,167 @@ +# Copyright (C) 2009, 2010 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/>. + +from ranger.core.loader import Loader +from ranger.fsobject import Directory, File +from ranger.ext.openstruct import OpenStruct +import os.path +from ranger.shared import FileManagerAware, SettingsAware +from test import Fake +from os.path import realpath, join, dirname +from subprocess import Popen, PIPE +TESTDIR = realpath(join(dirname(__file__), '/usr/include')) + +def skip(x): + return + +def raw_load_content(self): + """ + The method which is used in a Directory object to load stuff. + Keep this up to date! + """ + + from os.path import join, isdir, basename + from os import listdir + import ranger.ext.mount_path + + self.loading = True + self.load_if_outdated() + + try: + if self.exists and self.runnable: + # 0.003s: + self.mount_path = ranger.ext.mount_path.mount_path(self.path) + + # 0.1s: + filenames = [] + for fname in listdir(self.path): + if not self.settings.show_hidden: + hfilter = self.settings.hidden_filter + if hfilter: + if isinstance(hfilter, str) and hfilter in fname: + continue + if hasattr(hfilter, 'search') and \ + hfilter.search(fname): + continue + if isinstance(self.filter, str) and self.filter \ + and self.filter not in fname: + continue + filenames.append(join(self.path, fname)) + # --- + + self.load_content_mtime = os.stat(self.path).st_mtime + + marked_paths = [obj.path for obj in self.marked_items] + + # 2.85s: + files = [] + for name in filenames: + if isdir(name): + try: + item = self.fm.env.get_directory(name) + except: + item = Directory(name) + else: + item = File(name) + item.load_if_outdated() + files.append(item) + + # 0.2s + self.disk_usage = sum(f.size for f in files if f.is_file) + + self.scroll_offset = 0 + self.filenames = filenames + self.files = files + + self._clear_marked_items() + for item in self.files: + if item.path in marked_paths: + self.mark_item(item, True) + else: + self.mark_item(item, False) + + self.sort() + + if len(self.files) > 0: + if self.pointed_obj is not None: + self.sync_index() + else: + self.move(to=0) + else: + self.filenames = None + self.files = None + + self.cycle_list = None + self.content_loaded = True + self.determine_infostring() + self.correct_pointer() + + finally: + self.loading = False + + +class benchmark_load(object): + def __init__(self): + self.loader = Loader() + fm = OpenStruct(loader=self.loader) + SettingsAware.settings = Fake() + FileManagerAware.fm = fm + self.dir = Directory(TESTDIR) + + def bm_run(self, n): + for _ in range(n): + self.dir.load_content(schedule=True) + while self.loader.has_work(): + self.loader.work() + + +class benchmark_raw_load(object): + def __init__(self): + SettingsAware.settings = Fake() + self.dir = Directory(TESTDIR) + + def bm_run(self, n): + generator = self.dir.load_bit_by_bit() + for _ in range(n): + raw_load_content(self.dir) + +def bm_loader(n): + """Do some random calculation""" + tloader = benchmark_load(N) + traw = benchmark_raw_load(N) + +class benchmark_load_varieties(object): + def bm_ls(self, n): + for _ in range(n): + Popen(["ls", '-l', TESTDIR], stdout=open(os.devnull, 'w')).wait() + + def bm_os_listdir_stat(self, n): + for _ in range(n): + for f in os.listdir(TESTDIR): + path = os.path.join(TESTDIR, f) + os.stat(path) + + def bm_os_listdir(self, n): + for _ in range(n): + for f in os.listdir(TESTDIR): + path = os.path.join(TESTDIR, f) + + def bm_os_listdir_stat_listdir(self, n): + for _ in range(n): + for f in os.listdir(TESTDIR): + path = os.path.join(TESTDIR, f) + os.stat(path) + if os.path.isdir(path): + os.listdir(path) diff --git a/test/tc_directory.py b/test/tc_directory.py index f1b204c3..024ebc9d 100644 --- a/test/tc_directory.py +++ b/test/tc_directory.py @@ -15,6 +15,7 @@ if __name__ == '__main__': from __init__ import init; init() +import sys from os.path import realpath, join, dirname from ranger import fsobject @@ -38,7 +39,8 @@ class Test1(unittest.TestCase): self.assertFalse(dir.content_loaded) self.assertEqual(dir.filenames, None) self.assertEqual(dir.files, None) - self.assertRaises(fsobject.NotLoadedYet, len, dir) + if not sys.flags.optimize: # asserts are ignored with python -O + self.assertRaises(AssertionError, len, dir) def test_after_content_loaded(self): import os @@ -79,7 +81,8 @@ class Test1(unittest.TestCase): self.assertFalse(dir.exists) self.assertFalse(dir.accessible) self.assertEqual(dir.filenames, None) - self.assertRaises(fsobject.NotLoadedYet, len, dir) + if not sys.flags.optimize: # asserts are ignored with python -O + self.assertRaises(AssertionError, len, dir) def test_load_if_outdated(self): import os diff --git a/test/tc_displayable.py b/test/tc_displayable.py index 558a20ff..50f37845 100644 --- a/test/tc_displayable.py +++ b/test/tc_displayable.py @@ -20,7 +20,7 @@ import curses from random import randint from ranger.gui.displayable import Displayable, DisplayableContainer -from test import Fake, OK, raise_ok +from test import Fake, OK, raise_ok, TODO class TestWithFakeCurses(unittest.TestCase): def setUp(self): @@ -104,6 +104,7 @@ class TestDisplayableWithCurses(unittest.TestCase): curses.echo() curses.endwin() + @TODO def test_boundaries(self): disp = self.disp hei, wid = self.env.termsize diff --git a/test/tc_loader.py b/test/tc_loader.py index 22f866ec..53ac5617 100644 --- a/test/tc_loader.py +++ b/test/tc_loader.py @@ -18,7 +18,6 @@ if __name__ == '__main__': from __init__ import init; init() import unittest import os from os.path import realpath, join, dirname -from time import time from test import Fake from ranger.shared import FileManagerAware, SettingsAware @@ -29,115 +28,6 @@ from ranger.ext.openstruct import OpenStruct TESTDIR = realpath(join(dirname(__file__), 'testdir')) #TESTDIR = "/usr/sbin" -def raw_load_content(self): - """ - The method which is used in a Directory object to load stuff. - Keep this up to date! - """ - - from os.path import join, isdir, basename - from os import listdir - import ranger.ext.mount_path - - self.loading = True - self.load_if_outdated() - - try: - if self.exists and self.runnable: - self.mount_path = ranger.ext.mount_path.mount_path(self.path) - - filenames = [] - for fname in listdir(self.path): - if not self.settings.show_hidden: - hfilter = self.settings.hidden_filter - if hfilter: - if isinstance(hfilter, str) and hfilter in fname: - continue - if hasattr(hfilter, 'search') and \ - hfilter.search(fname): - continue - if isinstance(self.filter, str) and self.filter \ - and self.filter not in fname: - continue - filenames.append(join(self.path, fname)) - - self.load_content_mtime = os.stat(self.path).st_mtime - - marked_paths = [obj.path for obj in self.marked_items] - - files = [] - for name in filenames: - if isdir(name): - try: - item = self.fm.env.get_directory(name) - except: - item = Directory(name) - else: - item = File(name) - item.load_if_outdated() - files.append(item) - - self.disk_usage = sum(f.size for f in files if f.is_file) - - self.scroll_offset = 0 - self.filenames = filenames - self.files = files - - self._clear_marked_items() - for item in self.files: - if item.path in marked_paths: - self.mark_item(item, True) - else: - self.mark_item(item, False) - - self.sort() - - if len(self.files) > 0: - if self.pointed_obj is not None: - self.sync_index() - else: - self.move(to=0) - else: - self.filenames = None - self.files = None - - self.cycle_list = None - self.content_loaded = True - self.determine_infostring() - self.last_update_time = time() - self.correct_pointer() - - finally: - self.loading = False - - -def benchmark_load(n): - loader = Loader() - fm = OpenStruct(loader=loader) - SettingsAware.settings = Fake() - FileManagerAware.fm = fm - dir = Directory(TESTDIR) - - t1 = time() - for _ in range(n): - dir.load_content(schedule=True) - while loader.has_work(): - loader.work() - t2 = time() - return t2 - t1 - - -def benchmark_raw_load(n): - SettingsAware.settings = Fake() - dir = Directory(TESTDIR) - generator = dir.load_bit_by_bit() - t1 = time() - for _ in range(n): - raw_load_content(dir) - t2 = time() - return t2 - t1 - - class Test1(unittest.TestCase): def test_loader(self): loader = Loader() @@ -165,20 +55,20 @@ class Test1(unittest.TestCase): #print(iterations) self.assertNotEqual(None, dir.files) self.assertFalse(loader.has_work()) - - def test_get_overhead_of_loader(self): - N = 5 - tloader = benchmark_load(N) - traw = benchmark_raw_load(N) - #traw1k = 250.0 - #traw = traw1k * N / 1000.0 - #print("Loader: {0}s".format(tloader)) - #print("Raw: {0}s".format(traw)) - self.assertTrue(tloader > traw) - overhead = tloader * 100 / traw - 100 - self.assertTrue(overhead < 2, "overhead of loader too high: {0}" \ - .format(overhead)) - #print("Overhead: {0:.5}%".format(overhead)) +# +# def test_get_overhead_of_loader(self): +# N = 5 +# tloader = benchmark_load(N) +# traw = benchmark_raw_load(N) +# #traw1k = 250.0 +# #traw = traw1k * N / 1000.0 +# #print("Loader: {0}s".format(tloader)) +# #print("Raw: {0}s".format(traw)) +# self.assertTrue(tloader > traw) +# overhead = tloader * 100 / traw - 100 +# self.assertTrue(overhead < 2, "overhead of loader too high: {0}" \ +# .format(overhead)) +# #print("Overhead: {0:.5}%".format(overhead)) if __name__ == '__main__': diff --git a/test/tc_newkeys.py b/test/tc_newkeys.py index 8efb707d..c7a33025 100644 --- a/test/tc_newkeys.py +++ b/test/tc_newkeys.py @@ -17,6 +17,7 @@ if __name__ == '__main__': from __init__ import init; init() from unittest import TestCase, main +from test import TODO from ranger.ext.tree import Tree from ranger.container.keymap import * from ranger.container.keybuffer import KeyBuffer @@ -423,7 +424,7 @@ class Test(PressTestCase): self.assertPressFails(kb, 'xzy') self.assertPressIncomplete(kb, 'xx') self.assertPressIncomplete(kb, 'x') - if not sys.flags.optimize: + if not sys.flags.optimize: # asserts are ignored with python -O self.assertRaises(AssertionError, simulate_press, kb, 'xxx') kb.clear() @@ -587,8 +588,18 @@ class Test(PressTestCase): self.assertEqual(5, press('gh')) self.assertEqual(5, press('agh')) # self.assertPressFails(kb, 'agh') - # TODO: Make the next line work! For now, skip it. - # self.assertEqual(1, press('agg')) + + @TODO + def test_map_collision2(self): + directions = KeyMap() + directions.map('gg', dir=Direction(down=1)) + km = KeyMap() + km.map('agh', lambda _: 1) + km.map('a<dir>', lambda _: 2) + kb = KeyBuffer(km, directions) + press = self._mkpress(kb, km) + self.assertEqual(1, press('agh')) + self.assertEqual(2, press('agg')) def test_keymap_with_dir(self): def func(arg): diff --git a/test/test.py b/test/test.py index 5e8a9b9e..d0a69e5a 100644 --- a/test/test.py +++ b/test/test.py @@ -15,6 +15,6 @@ """Workaround to allow running single test cases directly""" try: - from __init__ import init, Fake, OK, raise_ok + from __init__ import init, Fake, OK, raise_ok, TODO except: - from test import init, Fake, OK, raise_ok + from test import init, Fake, OK, raise_ok, TODO |