diff options
55 files changed, 1050 insertions, 820 deletions
diff --git a/CHANGELOG b/CHANGELOG index 578717d4..8078305d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,30 @@ +This log only documents changes between stable versions. + +From 1.2 on, odd minor version numbers (1.3, 1.5, 1.7,..) are assigned to the +fresh git snapshots while stable versions will have even minor numbers. + +1.2 -> 1.2.1: +* Fixed yy/pp bug when yanking multiple directories + +1.1.2 -> 1.2: +* !!! Changed the default configuration directory to ~/.config/ranger !!! +* Removed "Console Modes", each old mode is now a simple command +* Disabled file previews by default if ranger is used by root +* Allow to jump to specific help sections by typing two numbers, e.g. 13? +* Added keys: da, dr, ya, yr for adding and removing files from copy buffer +* Added keys: gl and gL to resolve links, see 11? +* Added key: pL to create a relative symlink +* Added %<LETTER> and %<N><LETTER> macros for the console, see 33? +* Fixed ansi codes for colors in the pager +* Use the file ~/.mime.types for mime type detection +* Several clean-ups and fixes + +1.1.1 -> 1.1.2: +* Fix crash when using scrollwheel to scroll down in some cases +* The command "ranger dir1 dir2 ..." opens multiple directories in tabs +* Removed pydoc html documentation by default, re-create it with "make doc" +* Minor fixes + 1.1.0 -> 1.1.1: * New install script, "setup.py" * New flag for running programs: "w" (waits for enter press) diff --git a/HACKING b/HACKING index 9c114e89..dd384758 100644 --- a/HACKING +++ b/HACKING @@ -56,13 +56,13 @@ assuming <self> is a "SettingsAware" object. * Changing commands, adding aliases: ranger/defaults/commands.py -or ~/.ranger/commands.py +or ~/.config/ranger/commands.py * 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 +In ranger/defaults/options.py (or ~/.config/ranger/options.py), change colorscheme = 'default' to: colorscheme = 'myscheme' @@ -91,5 +91,5 @@ Version Numbering X.Y.Z, where: * X: Major version, milestone -* Y: Minor version, odd number => stable version -* Z: Revision +* Y: Minor version, even number => stable version +* Z: Revision, may be omitted if zero diff --git a/Makefile b/Makefile index 57db7b11..fd525721 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. NAME = ranger -VERSION = $(shell cat README | grep -m 1 -o '[0-9][0-9.]\+') +VERSION = $(shell grep -m 1 -o '[0-9][0-9.]\+' README) SNAPSHOT_NAME ?= $(NAME)-$(VERSION)-$(shell git rev-parse HEAD | cut -b 1-8).tar.gz # Find suitable python version (need python >= 2.6 or 3.1): PYTHON ?= $(shell python -c 'import sys; sys.exit(sys.version < "2.6")' && \ @@ -61,7 +61,6 @@ doc: cleandoc $(PYTHON) -c 'import pydoc, sys; \ sys.path[0] = "$(CWD)"; \ pydoc.writedocs("$(CWD)")' - rm $(DOCDIR)/test* find . -name \*.html -exec sed -i 's|'$(CWD)'|../..|g' -- {} \; cleandoc: diff --git a/README b/README index d92543f7..2b44015e 100644 --- a/README +++ b/README @@ -1,13 +1,14 @@ -Ranger v.1.1.1 +Ranger v.1.2.1 ============== Ranger is a free console file manager that gives you greater flexibility and a good overview of your files without having to leave your *nix console. It visualizes the directory tree in two dimensions: the directory hierarchy on one, lists of files on the other, with a preview to the right so you know -where you'll be going. The default keys are similar to those of Vim, Emacs -and Midnight Commander, though Ranger is easily controllable with just the -arrow keys or the mouse. +where you'll be going. + +The default keys are similar to those of Vim, Emacs and Midnight Commander, +though Ranger is easily controllable with just the arrow keys or the mouse. The program is written in Python (2.6 or 3.1) and uses curses for the text-based user interface. @@ -19,7 +20,6 @@ About * Author: Roman Zimbelmann <romanz@lavabit.com> * Website: http://savannah.nongnu.org/projects/ranger * License: GNU General Public License Version 3 -* Version: 1.1.1 * Download URL of the newest stable version: http://git.savannah.gnu.org/cgit/ranger.git/snapshot/ranger-stable.tar.gz @@ -28,6 +28,15 @@ http://git.savannah.gnu.org/cgit/ranger.git/snapshot/ranger-stable.tar.gz git clone http://git.sv.gnu.org/r/ranger.git +Design Goals +------------ + +* An easily maintainable file manager in a high level language +* A quick way to switch directories and browse the file system +* Keep it small but useful, do one thing and do it well +* Console based, with smooth integration into the unix shell + + Features -------- @@ -72,10 +81,30 @@ parent directories and to the right there's a preview of the object you're pointing at. Now use the Arrow Keys to navigate, Enter to open a file or type Q to quit. -To customize ranger, copy the files from ranger/defaults/ to ~/.ranger/ +To customize ranger, copy the files from ranger/defaults/ to ~/.config/ranger/ and modify them according to your wishes. +Usage Tips +---------- + +The author of ranger uses this function (in ~/.bashrc) to start ranger: +function ranger-cd { + before="$(pwd)" + python2.6 /the/path/to/ranger/ranger.py --fail-unless-cd "$@" || return 0 + after="$(grep \^\' ~/.config/ranger/bookmarks | cut -b3-)" + if [[ "$before" != "$after" ]]; then + cd "$after" + fi +} +bind '"\C-o":"ranger-cd\C-m"' + +This changes the directory after you close ranger and adds the shortcut +<CTRL-O> for starting ranger. + +To change back to the previous directory, you can type: cd - + + Troubleshooting, Getting Help ----------------------------- diff --git a/doc/colorschemes.txt b/doc/colorschemes.txt index 905c7a3e..e7bc2c0a 100644 --- a/doc/colorschemes.txt +++ b/doc/colorschemes.txt @@ -65,7 +65,7 @@ Specify a Colorscheme --------------------- Colorschemes are searched for in these directories: -~/.ranger/colorschemes/ +~/.config/ranger/colorschemes/ /ranger/colorschemes/ To specify which colorscheme to use, define the variable "colorscheme" @@ -73,7 +73,7 @@ in your options.py: colorscheme = colorschemes.default This means, use the (one) colorscheme contained in -either ~/.ranger/colorschemes/default.py or /ranger/colorschemes/default.py. +either ~/.config/ranger/colorschemes/default.py or /ranger/colorschemes/default.py. You can define more than one colorscheme in a colorscheme file. The one named "Scheme" will be chosen in that case. If there is no colorscheme diff --git a/doc/print_keys.py b/doc/print_keys.py index f87a2a40..f87a2a40 100644..100755 --- a/doc/print_keys.py +++ b/doc/print_keys.py diff --git a/doc/ranger.1 b/doc/ranger.1 index dd343f28..03cc3d56 100644 --- a/doc/ranger.1 +++ b/doc/ranger.1 @@ -1,4 +1,4 @@ -.TH RANGER 1 ranger-1.1.1 +.TH RANGER 1 ranger-1.2.1 .SH NAME ranger - visual file manager .\"----------------------------------------- @@ -38,7 +38,8 @@ Return the exit code 1 if ranger is used to run a file, for example with `ranger --fail-unless-cd filename`. This can be useful for scripts. .TP -r \fIdir\fR, --confdir=\fIdir\fR -Define a different configuration directory. The default is $HOME/.ranger. +Define a different configuration directory. The default is +$XDG_CONFIG_HOME/ranger (which defaults to ~/.config/ranger) .TP -m \fIn\fR, --mode=\fIn\fR When a filename is supplied, make it run in mode \fIn\fR. Check the @@ -177,17 +178,17 @@ of your parent shell after exiting ranger: ranger() { command ranger --fail-unless-cd $@ && - cd "$(grep \\^\\' ~/.ranger/bookmarks | cut -b3-)" + cd "$(grep \\^\\' ~/.config/ranger/bookmarks | cut -b3-)" } .\"----------------------------------------- .SH CONFIGURATION The files in .B ranger/defaults/ can be copied into your configuration directory (by default, this is -$HOME/.ranger) and customized according to your wishes. +~/.config/ranger) and customized according to your wishes. Most files don't have to be copied completely though: Just define those settings you want to add or change and they will override the defauls. -Colorschemes can be placed in $HOME/.ranger/colorschemes. +Colorschemes can be placed in ~/.config/ranger/colorschemes. .P All configuration is done in Python. Each configuration file should contain sufficient documentation. diff --git a/ranger.py b/ranger.py index f290d796..5652ba69 100755 --- a/ranger.py +++ b/ranger.py @@ -23,7 +23,11 @@ # after you exit ranger by starting it with: source ranger ranger """": if [ $1 ]; then - $@ --fail-unless-cd && cd "$(grep \^\' ~/.ranger/bookmarks | cut -b3-)" + if [ -z "$XDG_CONFIG_HOME" ]; then + "$@" --fail-unless-cd && cd "$(grep \^\' ~/.config/ranger/bookmarks | cut -b3-)" + else + "$@" --fail-unless-cd && cd "$(grep \^\' "$XDG_CONFIG_HOME"/ranger/bookmarks | cut -b3-)" + fi else echo "usage: source path/to/ranger.py path/to/ranger.py" fi diff --git a/ranger/__init__.py b/ranger/__init__.py index 54baa3c0..7a09dbe3 100644 --- a/ranger/__init__.py +++ b/ranger/__init__.py @@ -20,7 +20,7 @@ import sys from ranger.ext.openstruct import OpenStruct __license__ = 'GPL3' -__version__ = '1.1.1' +__version__ = '1.2.1' __credits__ = 'Roman Zimbelmann' __author__ = 'Roman Zimbelmann' __maintainer__ = 'Roman Zimbelmann' @@ -31,7 +31,10 @@ Copyright (C) 2009, 2010 Roman Zimbelmann <romanz@lavabit.com> """ USAGE = '%prog [options] [path/filename]' -DEFAULT_CONFDIR = '~/.ranger' +if 'XDG_CONFIG_HOME' in os.environ and os.environ['XDG_CONFIG_HOME']: + DEFAULT_CONFDIR = os.environ['XDG_CONFIG_HOME'] + '/ranger' +else: + DEFAULT_CONFDIR = '~/.config/ranger' RANGERDIR = os.path.dirname(__file__) LOGFILE = '/tmp/errorlog' arg = OpenStruct( diff --git a/ranger/__main__.py b/ranger/__main__.py index 048e00b9..7559b43d 100644 --- a/ranger/__main__.py +++ b/ranger/__main__.py @@ -30,7 +30,17 @@ def parse_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__) + + minor_version = __version__[2:] # assumes major version number is <10 + if '.' in minor_version: + minor_version = minor_version[:minor_version.find('.')] + version_tag = ' (stable)' if int(minor_version) % 2 == 0 else ' (testing)' + if __version__.endswith('.0'): + version_string = 'ranger ' + __version__[:-2] + version_tag + else: + version_string = 'ranger ' + __version__ + version_tag + + parser = OptionParser(usage=USAGE, version=version_string) parser.add_option('-d', '--debug', action='store_true', help="activate debug mode") @@ -120,7 +130,7 @@ def load_settings(fm, clean): pass # COMPAT WARNING if hasattr(keys, 'initialize_commands'): - print("Warning: the syntax for ~/.ranger/keys.py has changed.") + print("Warning: the syntax for ~/.config/ranger/keys.py has changed.") print("Your custom keys are not loaded."\ " Please update your configuration.") allow_access_to_confdir(ranger.arg.confdir, False) @@ -188,8 +198,9 @@ def main(): SettingsAware._setup() + targets = arg.targets or ['.'] + target = targets[0] if arg.targets: - target = arg.targets[0] if target.startswith('file://'): target = target[7:] if not os.access(target, os.F_OK): @@ -202,10 +213,6 @@ def main(): load_apps(runner, ranger.arg.clean) runner(files=[File(target)], mode=arg.mode, flags=arg.flags) sys.exit(1 if arg.fail_unless_cd else 0) - else: - path = target - else: - path = '.' if not ranger.arg.clean: copy_config_files() @@ -213,9 +220,13 @@ def main(): crash_traceback = None try: # Initialize objects - EnvironmentAware._assign(Environment(path)) + EnvironmentAware._assign(Environment(target)) fm = FM() + fm.tabs = dict((n+1, os.path.abspath(path)) for n, path \ + in enumerate(targets[:9])) load_settings(fm, ranger.arg.clean) + if fm.env.username == 'root': + fm.settings.preview_files = False FileManagerAware._assign(fm) fm.ui = UI() diff --git a/ranger/api/commands.py b/ranger/api/commands.py index ca3f730d..f4e2ca76 100644 --- a/ranger/api/commands.py +++ b/ranger/api/commands.py @@ -17,7 +17,6 @@ import os from collections import deque from ranger.api import * from ranger.shared import FileManagerAware -from ranger.gui.widgets import console_mode as cmode from ranger.ext.command_parser import LazyParser as parse @@ -71,9 +70,8 @@ class Command(FileManagerAware): """Abstract command class""" name = None allow_abbrev = True - def __init__(self, line, mode): + def __init__(self, line): self.line = line - self.mode = mode def execute(self): """Override this""" diff --git a/ranger/api/keys.py b/ranger/api/keys.py index 13a4b07f..5812de39 100644 --- a/ranger/api/keys.py +++ b/ranger/api/keys.py @@ -20,7 +20,6 @@ from inspect import getargspec, ismethod from ranger import RANGERDIR from ranger.api import * -from ranger.gui.widgets import console_mode as cmode from ranger.container.bookmarks import ALLOWED_KEYS as ALLOWED_BOOKMARK_KEYS from ranger.container.keymap import KeyMap, Direction, KeyMapWithDirections diff --git a/ranger/container/history.py b/ranger/container/history.py index ba13775d..d7a45500 100644 --- a/ranger/container/history.py +++ b/ranger/container/history.py @@ -13,74 +13,118 @@ # 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 collections import deque - class HistoryEmptyException(Exception): pass class History(object): - def __init__(self, maxlen = None): - self.history = deque(maxlen = maxlen) - self.history_forward = deque(maxlen = maxlen) + def __init__(self, maxlen=None, unique=True): + self._history = [] + self._index = 0 + self.maxlen = maxlen + self.unique = unique def add(self, item): - if len(self.history) == 0 or self.history[-1] != item: - self.history.append(item) - self.history_forward.clear() + # Remove everything after index + if self._index < len(self._history) - 2: + del self._history[:self._index+1] + # Remove Duplicates + if self.unique: + try: + self._history.remove(item) + except: + pass + else: + if self._history and self._history[-1] == item: + del self._history[-1] + # Remove first if list is too long + if len(self._history) > self.maxlen - 1: + del self._history[0] + # Append the item and fast forward + self._history.append(item) + self._index = len(self._history) - 1 - def modify(self, item): + def modify(self, item, unique=False): + if self._history and unique: + try: + self._history.remove(item) + self._index -= 1 + except: + pass try: - self.history[-1] = item + self._history[self._index] = item except IndexError: - raise HistoryEmptyException + self.add(item) def __len__(self): - return len(self.history) + return len(self._history) def current(self): - try: - return self.history[-1] - except IndexError: - raise HistoryEmptyException() + if self._history: + return self._history[self._index] + else: + raise HistoryEmptyException def top(self): try: - return self.history_forward[-1] + return self._history[-1] except IndexError: - try: - return self.history[-1] - except IndexError: - raise HistoryEmptyException() + raise HistoryEmptyException() def bottom(self): try: - return self.history[0] + return self._history[0] except IndexError: raise HistoryEmptyException() def back(self): - if len(self.history) > 1: - self.history_forward.appendleft( self.history.pop() ) + self._index -= 1 + if self._index < 0: + self._index = 0 return self.current() def move(self, n): - if n > 0: - return self.forward() - if n < 0: - return self.back() + self._index += n + if self._index > len(self._history) - 1: + self._index = len(self._history) - 1 + if self._index < 0: + self._index = 0 + return self.current() + + def search(self, string, n): + if n != 0 and string: + step = n > 0 and 1 or -1 + i = self._index + steps_left = steps_left_at_start = int(abs(n)) + while steps_left: + i += step + if i >= len(self._history) or i < 0: + break + if self._history[i].startswith(string): + steps_left -= 1 + if steps_left != steps_left_at_start: + self._index = i + return self.current() def __iter__(self): - return self.history.__iter__() + return self._history.__iter__() def next(self): - return self.history.next() + return self._history.next() def forward(self): - if len(self.history_forward) > 0: - self.history.append( self.history_forward.popleft() ) + if self._history: + self._index += 1 + if self._index > len(self._history) - 1: + self._index = len(self._history) - 1 + else: + self._index = 0 return self.current() def fast_forward(self): - if self.history_forward: - self.history.extend(self.history_forward) - self.history_forward.clear() + if self._history: + self._index = len(self._history) - 1 + else: + self._index = 0 + + def _left(self): # used for unit test + return self._history[0:self._index+1] diff --git a/ranger/core/actions.py b/ranger/core/actions.py index 44061aca..b379d341 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -16,19 +16,26 @@ import os import re import shutil +import string from os.path import join, isdir from os import symlink, getcwd from inspect import cleandoc import ranger from ranger.ext.direction import Direction +from ranger.ext.relative_symlink import relative_symlink +from ranger.ext.shell_escape import shell_quote from ranger import fsobject from ranger.shared import FileManagerAware, EnvironmentAware, SettingsAware -from ranger.gui.widgets import console_mode as cmode from ranger.fsobject import File from ranger.ext import shutil_generatorized as shutil_g from ranger.core.loader import LoadableObject +class _MacroTemplate(string.Template): + """A template for substituting macros in commands""" + delimiter = '%' + idpattern = '\d?[a-z]' + class Actions(FileManagerAware, EnvironmentAware, SettingsAware): search_method = 'ctime' search_forward = False @@ -69,10 +76,79 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): """Redraw the window""" self.ui.redraw_window() - def open_console(self, mode=':', string='', prompt=None): + def open_console(self, string='', prompt=None, position=None): """Open the console if the current UI supports that""" if hasattr(self.ui, 'open_console'): - self.ui.open_console(mode, string, prompt=prompt) + self.ui.open_console(string, prompt=prompt, position=position) + + def execute_console(self, string=''): + """Execute a command for the console""" + self.open_console(string=string) + self.ui.console.line = string + self.ui.console.execute() + + def substitute_macros(self, string): + return _MacroTemplate(string).safe_substitute(self._get_macros()) + + def _get_macros(self): + macros = {} + + if self.fm.env.cf: + macros['f'] = shell_quote(self.fm.env.cf.basename) + else: + macros['f'] = '' + + macros['s'] = ' '.join(shell_quote(fl.basename) \ + for fl in self.fm.env.get_selection()) + + 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'] = '.' + + # define d/f/s macros for each tab + for i in range(1,10): + try: + tab_dir_path = self.fm.tabs[i] + except: + continue + tab_dir = self.fm.env.get_directory(tab_dir_path) + i = str(i) + macros[i + 'd'] = shell_quote(tab_dir_path) + macros[i + 'f'] = shell_quote(tab_dir.pointed_obj.path) + macros[i + 's'] = ' '.join(shell_quote(fl.path) + for fl in tab_dir.get_selection()) + + # define D/F/S for the next tab + found_current_tab = False + next_tab_path = None + first_tab = None + for tab in self.fm.tabs: + if not first_tab: + first_tab = tab + if found_current_tab: + next_tab_path = self.fm.tabs[tab] + break + if self.fm.current_tab == tab: + found_current_tab = True + if found_current_tab and not next_tab_path: + next_tab_path = self.fm.tabs[first_tab] + next_tab = self.fm.env.get_directory(next_tab_path) + + macros['D'] = shell_quote(next_tab) + macros['F'] = shell_quote(next_tab.pointed_obj.path) + macros['S'] = ' '.join(shell_quote(fl.path) + for fl in next_tab.get_selection()) + + return macros + def execute_file(self, files, **kw): """Execute a file. @@ -128,7 +204,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): selection = self.env.get_selection() if not self.env.enter_dir(cf) and selection: if self.execute_file(selection, mode=mode) is False: - self.open_console(cmode.OPEN_QUICK) + self.open_console('open_with ') elif direction.vertical(): newpos = direction.move( direction=direction.down(), @@ -291,7 +367,10 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): def search_file(self, text, regexp=True): if isinstance(text, str) and regexp: - text = re.compile(text, re.L | re.U | re.I) + try: + text = re.compile(text, re.L | re.U | re.I) + except: + return False self.env.last_search = text self.search(order='search') @@ -345,7 +424,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): # -------------------------- # -- Tags # -------------------------- - # Tags are saved in ~/.ranger/tagged and simply mark if a + # Tags are saved in ~/.config/ranger/tagged and simply mark if a # file is important to you in any context. def tag_toggle(self, movedown=None): @@ -446,15 +525,29 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): from ranger.help import get_help, get_help_by_index + scroll_to_line = 0 if narg is not None: - help_text = get_help_by_index(narg) + chapter, subchapter = int(str(narg)[0]), str(narg)[1:] + help_text = get_help_by_index(chapter) + lines = help_text.split('\n') + if chapter: + chapternumber = str(chapter) + '.' + subchapter + '. ' + skip_to_content = True + for line_number, line in enumerate(lines): + if skip_to_content: + if line[:10] == '==========': + skip_to_content = False + else: + if line.startswith(chapternumber): + scroll_to_line = line_number else: help_text = get_help(topic) + lines = help_text.split('\n') pager = self.ui.open_pager() pager.markup = 'help' - lines = help_text.split('\n') pager.set_source(lines) + pager.move(down=scroll_to_line) def display_log(self): if not hasattr(self.ui, 'open_pager'): @@ -531,8 +624,9 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): self.env.cut = False self.ui.browser.main_column.request_redraw() - def copy(self, narg=None, dirarg=None): - """Copy the selected items""" + def copy(self, mode='set', narg=None, dirarg=None): + """Copy the selected items. Modes are: 'set', 'add', 'remove'.""" + assert mode in ('set', 'add', 'remove') cwd = self.env.cwd if not narg and not dirarg: selected = (f for f in self.env.get_selection() if f in cwd.files) @@ -548,20 +642,28 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): pagesize=self.env.termsize[0], offset=offset) cwd.pointer = pos cwd.correct_pointer() - self.env.copy = set(selected) + if mode == 'set': + self.env.copy = set(selected) + elif mode == 'add': + self.env.copy.update(set(selected)) + elif mode == 'remove': + self.env.copy.difference_update(set(selected)) self.env.cut = False self.ui.browser.main_column.request_redraw() - def cut(self, narg=None, dirarg=None): - self.copy(narg=narg, dirarg=dirarg) + def cut(self, mode='set', narg=None, dirarg=None): + self.copy(mode=mode, narg=narg, dirarg=dirarg) self.env.cut = True self.ui.browser.main_column.request_redraw() - def paste_symlink(self): + def paste_symlink(self, relative=False): copied_files = self.env.copy for f in copied_files: try: - symlink(f.path, join(getcwd(), f.basename)) + if relative: + relative_symlink(f.path, join(getcwd(), f.basename)) + else: + symlink(f.path, join(getcwd(), f.basename)) except Exception as x: self.notify(x) @@ -602,7 +704,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): for f in self.env.copy: if isdir(f.path): for _ in shutil_g.copytree(src=f.path, - dst=join(self.env.cwd.path, f.basename), + dst=join(original_path, f.basename), symlinks=True, overwrite=overwrite): yield diff --git a/ranger/core/environment.py b/ranger/core/environment.py index fca2168b..61db8694 100644 --- a/ranger/core/environment.py +++ b/ranger/core/environment.py @@ -54,7 +54,7 @@ class Environment(SettingsAware, SignalDispatcher): self.keybuffer = KeyBuffer(None, None) self.keymanager = KeyManager(self.keybuffer, ALLOWED_CONTEXTS) self.copy = set() - self.history = History(self.settings.max_history_size) + self.history = History(self.settings.max_history_size, unique=False) try: self.username = pwd.getpwuid(os.geteuid()).pw_name @@ -166,7 +166,7 @@ class Environment(SettingsAware, SignalDispatcher): def history_go(self, relative): """Move relative in history""" if self.history: - self.history.move(relative).go() + self.history.move(relative).go(history=False) def enter_dir(self, path, history = True): """Enter given path""" diff --git a/ranger/defaults/apps.py b/ranger/defaults/apps.py index 1f2f751c..47eff0c9 100644 --- a/ranger/defaults/apps.py +++ b/ranger/defaults/apps.py @@ -17,13 +17,13 @@ This is the default ranger configuration file for filetype detection and application handling. -You can place this file in your ~/.ranger/ directory and it will be used +You can place this file in your ~/.config/ranger/ directory and it will be used instead of this one. Though, to minimize your effort when upgrading ranger, you may want to subclass CustomApplications rather than making a full copy. This example modifies the behaviour of "feh" and adds a custom media player: -#### start of the ~/.ranger/apps.py example +#### start of the ~/.config/ranger/apps.py example from ranger.defaults.apps import CustomApplications as DefaultApps from ranger.api.apps import * @@ -94,7 +94,7 @@ class CustomApplications(Applications): # ----------------------------------------- application definitions # Note: Trivial applications are defined at the bottom def app_pager(self, c): - return tup('less', *c) + return tup('less', '-R', *c) def app_editor(self, c): try: diff --git a/ranger/defaults/commands.py b/ranger/defaults/commands.py index 8728f9be..d3c05023 100644 --- a/ranger/defaults/commands.py +++ b/ranger/defaults/commands.py @@ -20,7 +20,7 @@ Each command is a subclass of `Command'. Several methods are defined to interface with the console: execute: call this method when the command is executed. tab: call this method when tab is pressed. - quick: call this method after each keypress in the QuickCommandConsole. + quick: call this method after each keypress. The return values for tab() can be either: None: There is no tab completion @@ -32,7 +32,7 @@ The return value for quick() can be: The return value for execute() doesn't matter. If you want to add custom commands, you can create a file -~/.ranger/commands.py, add the line: +~/.config/ranger/commands.py, add the line: from ranger.api.commands import * and write some command definitions, for example: @@ -55,6 +55,8 @@ For a list of all actions, check /ranger/core/actions.py. ''' from ranger.api.commands import * +from ranger.ext.get_executables import get_executables +from ranger.core.runner import ALLOWED_FLAGS alias('e', 'edit') alias('q', 'quit') @@ -67,9 +69,6 @@ class cd(Command): The cd command changes the directory. The command 'cd -' is equivalent to typing ``. - - In the quick console, the directory will be entered without the - need to press enter, as soon as there is one unambiguous match. """ def execute(self): @@ -87,17 +86,153 @@ class cd(Command): def tab(self): return self._tab_only_directories() - def quick(self): - from os.path import isdir, join, normpath + +class search(Command): + def execute(self): + self.fm.search_file(parse(self.line).rest(1), regexp=True) + + +class shell(Command): + def execute(self): line = parse(self.line) - cwd = self.fm.env.cwd.path + if line.chunk(1) and line.chunk(1)[0] == '-': + flags = line.chunk(1)[1:] + command = line.rest(2) + else: + flags = '' + command = line.rest(1) - rel_dest = line.rest(1) - if not rel_dest: - return False + if not command and 'p' in flags: command = 'cat %f' + if command: + if '%' in command: + command = self.fm.substitute_macros(command) + self.fm.execute_command(command, flags=flags) + + def tab(self): + line = parse(self.line) + if line.chunk(1) and line.chunk(1)[0] == '-': + flags = line.chunk(1)[1:] + command = line.rest(2) + else: + flags = '' + command = line.rest(1) + start = self.line[0:len(self.line) - len(command)] + + try: + position_of_last_space = command.rindex(" ") + except ValueError: + return (start + program + ' ' for program \ + in get_executables() if program.startswith(command)) + if position_of_last_space == len(command) - 1: + return self.line + '%s ' + else: + before_word, start_of_word = self.line.rsplit(' ', 1) + return (before_word + ' ' + file.shell_escaped_basename \ + for file in self.fm.env.cwd.files \ + if file.shell_escaped_basename.startswith(start_of_word)) + +class open_with(Command): + def execute(self): + line = parse(self.line) + app, flags, mode = self._get_app_flags_mode(line.rest(1)) + self.fm.execute_file( + files = [self.fm.env.cf], + app = app, + flags = flags, + mode = mode) + + def _get_app_flags_mode(self, string): + """ + Extracts the application, flags and mode from a string. - abs_dest = normpath(join(cwd, rel_dest)) - return rel_dest != '.' and isdir(abs_dest) + examples: + "mplayer d 1" => ("mplayer", "d", 1) + "aunpack 4" => ("aunpack", "", 4) + "p" => ("", "p", 0) + "" => None + """ + + app = '' + flags = '' + mode = 0 + split = string.split() + + if len(split) == 0: + pass + + elif len(split) == 1: + part = split[0] + if self._is_app(part): + app = part + elif self._is_flags(part): + flags = part + elif self._is_mode(part): + mode = part + + elif len(split) == 2: + part0 = split[0] + part1 = split[1] + + if self._is_app(part0): + app = part0 + if self._is_flags(part1): + flags = part1 + elif self._is_mode(part1): + mode = part1 + elif self._is_flags(part0): + flags = part0 + if self._is_mode(part1): + mode = part1 + elif self._is_mode(part0): + mode = part0 + if self._is_flags(part1): + flags = part1 + + elif len(split) >= 3: + part0 = split[0] + part1 = split[1] + part2 = split[2] + + if self._is_app(part0): + app = part0 + if self._is_flags(part1): + flags = part1 + if self._is_mode(part2): + mode = part2 + elif self._is_mode(part1): + mode = part1 + if self._is_flags(part2): + flags = part2 + elif self._is_flags(part0): + flags = part0 + if self._is_mode(part1): + mode = part1 + elif self._is_mode(part0): + mode = part0 + if self._is_flags(part1): + flags = part1 + + return app, flags, int(mode) + + def _get_tab(self): + line = parse(self.line) + data = line.rest(1) + if ' ' not in data: + all_apps = self.fm.apps.all() + if all_apps: + return (app for app in all_apps if app.startswith(data)) + + return None + + def _is_app(self, arg): + return self.fm.apps.has(arg) or \ + (not self._is_flags(arg) and arg in get_executables()) + + def _is_flags(self, arg): + return all(x in ALLOWED_FLAGS for x in arg) + + def _is_mode(self, arg): + return all(x in '0123456789' for x in arg) class find(Command): @@ -105,35 +240,21 @@ class find(Command): :find <string> The find command will attempt to find a partial, case insensitive - match in the filenames of the current directory. - - In the quick command console, once there is one unambiguous match, - the file will be run automatically. + match in the filenames of the current directory and execute the + file automatically. """ count = 0 tab = Command._tab_directory_content def execute(self): - if self.mode != cmode.COMMAND_QUICK: - self._search() - - import re - search = parse(self.line).rest(1) - search = re.escape(search) - self.fm.env.last_search = re.compile(search, re.IGNORECASE) - self.fm.search_method = 'search' - if self.count == 1: self.fm.move(right=1) self.fm.block_input(0.5) + else: + self.fm.cd(parse(self.line).rest(1)) def quick(self): - self._search() - if self.count == 1: - return True - - def _search(self): self.count = 0 line = parse(self.line) cwd = self.fm.env.cwd @@ -142,6 +263,11 @@ class find(Command): except IndexError: return False + if arg == '.': + return False + if arg == '..': + return True + deq = deque(cwd.files) deq.rotate(-cwd.pointer) i = 0 @@ -281,7 +407,7 @@ class delete(Command): and len(os.listdir(cf.path)) > 0): # better ask for a confirmation, when attempting to # delete multiple files or a non-empty directory. - return self.fm.open_console(self.mode, DELETE_WARNING) + return self.fm.open_console(DELETE_WARNING) # no need for a confirmation, just delete self.fm.delete() diff --git a/ranger/defaults/keys.py b/ranger/defaults/keys.py index c95baed9..9f0c78cb 100644 --- a/ranger/defaults/keys.py +++ b/ranger/defaults/keys.py @@ -58,7 +58,7 @@ dgg => fm.cut(foo=bar, dirarg=Direction(to=0)) 5dgg => fm.cut(foo=bar, narg=5, dirarg=Direction(to=0)) 5d3gg => fm.cut(foo=bar, narg=5, dirarg=Direction(to=3)) -Example ~/.ranger/keys.py +Example ~/.config/ranger/keys.py ------------------------- from ranger.api.keys import * @@ -130,8 +130,8 @@ map('<F3>', fm.display_file()) map('<F4>', fm.edit_file()) map('<F5>', fm.copy()) map('<F6>', fm.cut()) -map('<F7>', fm.open_console(cmode.COMMAND, 'mkdir ')) -map('<F8>', fm.open_console(cmode.COMMAND, DELETE_WARNING)) +map('<F7>', fm.open_console('mkdir ')) +map('<F8>', fm.open_console(DELETE_WARNING)) map('<F10>', fm.exit()) # =================================================================== @@ -168,12 +168,17 @@ map('u<C-V><dir>', fm.mark_in_direction(val=False)) # ------------------------------------------ file system operations map('yy', 'y<dir>', fm.copy()) +map('ya', fm.copy(mode='add')) +map('yr', fm.copy(mode='remove')) map('dd', 'd<dir>', fm.cut()) +map('da', fm.cut(mode='add')) +map('dr', fm.cut(mode='remove')) map('pp', fm.paste()) map('po', fm.paste(overwrite=True)) -map('pl', fm.paste_symlink()) +map('pl', fm.paste_symlink(relative=False)) +map('pL', fm.paste_symlink(relative=True)) map('p<bg>', fm.hint('press *p* to confirm pasting' \ - ', *o* to overwrite or *l* to create symlinks')) + ', *o*verwrite, create sym*l*inks, relative sym*L*inks')) map('u<bg>', fm.hint("un*y*ank, unbook*m*ark, unselect:*v*")) map('ud', 'uy', fm.uncut()) @@ -181,12 +186,12 @@ 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')) +map('du', fm.execute_console('shell -p du --max-depth=1 -h --apparent-size')) # -------------------------------------------------- toggle options map('z<bg>', fm.hint("[*cdfhimpPs*] show_*h*idden *p*review_files "\ "*P*review_dirs *f*ilter flush*i*nput *m*ouse")) -map('zh', '<C-h>', fm.toggle_boolean_option('show_hidden')) +map('zh', '<C-h>', '<backspace>', fm.toggle_boolean_option('show_hidden')) map('zp', fm.toggle_boolean_option('preview_files')) map('zP', fm.toggle_boolean_option('preview_directories')) map('zi', fm.toggle_boolean_option('flushinput')) @@ -194,7 +199,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 ')) +map('zf', fm.open_console('filter ')) # ------------------------------------------------------------ sort map('o<bg>', 'O<bg>', fm.hint("*s*ize *b*ase*n*ame *m*time" \ @@ -220,26 +225,28 @@ map('or', 'Or', 'oR', 'OR', lambda arg: \ @map("A") def append_to_filename(arg): command = 'rename ' + arg.fm.env.cf.basename - arg.fm.open_console(cmode.COMMAND, command) + arg.fm.open_console(command) @map("I") def insert_before_filename(arg): - append_to_filename(arg) - arg.fm.ui.console.move(right=len('rename '), absolute=True) + command = 'rename ' + arg.fm.env.cf.basename + arg.fm.open_console(command, position=len('rename ')) -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('cw', fm.open_console('rename ')) +map('cd', fm.open_console('cd ')) +map('f', fm.open_console('find ')) 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!')) +map('@', fm.open_console('shell %s', position=len('shell '))) +map('#', fm.open_console('shell -p ')) # --------------------------------------------- jump to directories map('gh', fm.cd('~')) map('ge', fm.cd('/etc')) map('gu', fm.cd('/usr')) map('gd', fm.cd('/dev')) -map('gl', fm.cd('/lib')) +map('gl', lambda arg: arg.fm.cd(os.path.realpath(arg.fm.env.cwd.path))) +map('gL', lambda arg: arg.fm.cd( + os.path.dirname(os.path.realpath(arg.fm.env.cf.path)))) map('go', fm.cd('/opt')) map('gv', fm.cd('/var')) map('gr', 'g/', fm.cd('/')) @@ -261,7 +268,7 @@ for n in range(1, 10): map('<A-' + str(n) + '>', fm.tab_open(n)) # ------------------------------------------------------- searching -map('/', fm.open_console(cmode.SEARCH)) +map('/', fm.open_console('search ')) map('n', fm.search()) map('N', fm.search(forward=False)) @@ -300,11 +307,10 @@ def ctrl_c(arg): arg.fm.notify("Aborting: " + item.get_description()) arg.fm.loader.remove(index=0) -map(':', ';', fm.open_console(cmode.COMMAND)) -map('>', fm.open_console(cmode.COMMAND_QUICK)) -map('!', fm.open_console(cmode.OPEN, prompt='!')) -map('s', fm.open_console(cmode.OPEN, prompt='$')) -map('r', fm.open_console(cmode.OPEN_QUICK)) +map(':', ';', fm.open_console('')) +map('!', fm.open_console('shell ')) +map('s', fm.open_console('shell ')) +map('r', fm.open_console('open_with ')) # =================================================================== diff --git a/ranger/defaults/options.py b/ranger/defaults/options.py index 8f8ee830..39046443 100644 --- a/ranger/defaults/options.py +++ b/ranger/defaults/options.py @@ -17,7 +17,7 @@ This is the default configuration file of ranger. There are two ways of customizing ranger. The first and recommended -method is creating a file at ~/.ranger/options.py and adding +method is creating a file at ~/.config/ranger/options.py and adding those lines you want to change. It might look like this: from ranger.api.options import * @@ -94,7 +94,7 @@ tilde_in_titlebar = True # How many directory-changes or console-commands should be kept in history? max_history_size = 20 -max_console_history_size = 20 +max_console_history_size = 50 # Try to keep so much space between the top/bottom border when scrolling: scroll_offset = 8 diff --git a/ranger/ext/human_readable.py b/ranger/ext/human_readable.py index beeaf6d3..40111b6d 100644 --- a/ranger/ext/human_readable.py +++ b/ranger/ext/human_readable.py @@ -13,24 +13,39 @@ # 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 math - -ONE_KB = 1024 -UNITS = 'BKMGTP' -MAX_EXPONENT = len(UNITS) - 1 - def human_readable(byte, seperator=' '): - if not byte: - return '0' - - exponent = int(math.log(byte, 2) / 10) - flt = round(float(byte) / (1 << (10 * exponent)), 2) + """ + Convert a large number of bytes to an easily readable format. - if exponent > MAX_EXPONENT: - return '>9000' # off scale - - if int(flt) == flt: - return '%.0f%s%s' % (flt, seperator, UNITS[exponent]) - - else: - return '%.2f%s%s' % (flt, seperator, UNITS[exponent]) + >>> human_readable(54) + "54 B" + >>> human_readable(1500) + "1.46 K" + >>> human_readable(2 ** 20 * 1023) + "1023 M" + """ + if byte <= 0: + return '0' + if byte < 2**10: + return '%d%sB' % (byte, seperator) + if byte < 2**10 * 999: + return '%.3g%sK' % (byte / 2**10.0, seperator) + if byte < 2**20: + return '%.4g%sK' % (byte / 2**10.0, seperator) + if byte < 2**20 * 999: + return '%.3g%sM' % (byte / 2**20.0, seperator) + if byte < 2**30: + return '%.4g%sM' % (byte / 2**20.0, seperator) + if byte < 2**30 * 999: + return '%.3g%sG' % (byte / 2**30.0, seperator) + if byte < 2**40: + return '%.4g%sG' % (byte / 2**30.0, seperator) + if byte < 2**40 * 999: + return '%.3g%sT' % (byte / 2**40.0, seperator) + if byte < 2**50: + return '%.4g%sT' % (byte / 2**40.0, seperator) + if byte < 2**50 * 999: + return '%.3g%sP' % (byte / 2**50.0, seperator) + if byte < 2**60: + return '%.4g%sP' % (byte / 2**50.0, seperator) + return '>9000' diff --git a/ranger/ext/relative_symlink.py b/ranger/ext/relative_symlink.py new file mode 100644 index 00000000..bba00e39 --- /dev/null +++ b/ranger/ext/relative_symlink.py @@ -0,0 +1,39 @@ +# 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 os import symlink, sep +from os.path import dirname, join + +def relative_symlink(src, dst): + common_base = get_common_base(src, dst) + symlink(get_relative_source_file(src, dst, common_base), dst) + +def get_relative_source_file(src, dst, common_base=None): + if common_base is None: + common_base = get_common_base(src, dst) + return '../' * dst.count('/', len(common_base)) + src[len(common_base):] + +def get_common_base(src, dst): + if not src or not dst: + return '/' + i = 0 + while True: + new_i = src.find(sep, i + 1) + if new_i == -1: + break + if not dst.startswith(src[:new_i + 1]): + break + i = new_i + return src[:i + 1] diff --git a/ranger/fsobject/directory.py b/ranger/fsobject/directory.py index f1bcd9a3..9bdb4caa 100644 --- a/ranger/fsobject/directory.py +++ b/ranger/fsobject/directory.py @@ -63,7 +63,6 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): filter = None marked_items = None scroll_begin = 0 - scroll_offset = 0 mount_path = '/' disk_usage = 0 @@ -213,7 +212,6 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): yield self.disk_usage = disk_usage - self.scroll_offset = 0 self.filenames = filenames self.files = files @@ -420,10 +418,10 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): return True return self.last_used + seconds < time() - def go(self): + def go(self, history=True): """enter the directory if the filemanager is running""" if self.fm: - return self.fm.enter_dir(self.path) + return self.fm.enter_dir(self.path, history=history) return False def empty(self): diff --git a/ranger/fsobject/file.py b/ranger/fsobject/file.py index 4acad100..fad22a1f 100644 --- a/ranger/fsobject/file.py +++ b/ranger/fsobject/file.py @@ -76,7 +76,9 @@ class File(FileSystemObject): def has_preview(self): if not self.fm.settings.preview_files: return False - if not self.accessible or self.is_fifo or self.is_device: + if self.is_socket or self.is_fifo or self.is_device: + return False + if not self.accessible: return False if self.image or self.container: return True diff --git a/ranger/gui/colorscheme.py b/ranger/gui/colorscheme.py index 501b8788..5b317acb 100644 --- a/ranger/gui/colorscheme.py +++ b/ranger/gui/colorscheme.py @@ -24,7 +24,7 @@ The values are specified in ranger.gui.color. A colorscheme must... 1. be inside either of these directories: -~/.ranger/colorschemes/ +~/.config/ranger/colorschemes/ path/to/ranger/colorschemes/ 2. be a subclass of ranger.gui.colorscheme.ColorScheme @@ -121,7 +121,7 @@ class ColorScheme(SettingsAware): return fg, -1, attr def _colorscheme_name_to_class(signal): - # Find the colorscheme. First look for it at ~/.ranger/colorschemes, + # Find the colorscheme. First look for it at ~/.config/ranger/colorschemes, # then at RANGERDIR/colorschemes. If the file contains a class # named Scheme, it is used. Otherwise, an arbitrary other class # is picked. @@ -139,7 +139,7 @@ def _colorscheme_name_to_class(signal): except: return False - # create ~/.ranger/colorschemes/__init__.py if it doesn't exist + # create ~/.config/ranger/colorschemes/__init__.py if it doesn't exist if usecustom: if os.path.exists(ranger.relpath_conf('colorschemes')): initpy = ranger.relpath_conf('colorschemes', '__init__.py') diff --git a/ranger/gui/defaultui.py b/ranger/gui/defaultui.py index 4baea756..434e6d45 100644 --- a/ranger/gui/defaultui.py +++ b/ranger/gui/defaultui.py @@ -92,8 +92,8 @@ class DefaultUI(UI): def close_embedded_pager(self): self.browser.close_pager() - def open_console(self, mode, string='', prompt=None): - if self.console.open(mode, string, prompt=prompt): + def open_console(self, string='', prompt=None, position=None): + if self.console.open(string, prompt=prompt, position=position): self.status.msg = None self.console.on_close = self.close_console self.console.visible = True diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py index 1ee7ebc0..57264292 100644 --- a/ranger/gui/widgets/console.py +++ b/ranger/gui/widgets/console.py @@ -18,89 +18,63 @@ The Console widget implements a vim-like console for entering commands, searching and executing files. """ -import string import curses +import re from collections import deque from . import Widget -from ranger.gui.widgets.console_mode import is_valid_mode, mode_to_class from ranger import log, relpath_conf -from ranger.core.runner import ALLOWED_FLAGS -from ranger.ext.shell_escape import shell_quote -from ranger.ext.utfwidth import uwid from ranger.container.keymap import CommandArgs -from ranger.ext.get_executables import get_executables from ranger.ext.direction import Direction from ranger.ext.utfwidth import uwid, uchars from ranger.container import History from ranger.container.history import HistoryEmptyException import ranger -DEFAULT_HISTORY = 0 -SEARCH_HISTORY = 1 -QUICKOPEN_HISTORY = 2 -OPEN_HISTORY = 3 - -class _CustomTemplate(string.Template): - """A string.Template subclass for use in the OpenConsole""" - delimiter = '%' - idpattern = '[a-z]' - - class Console(Widget): - mode = None visible = False last_cursor_mode = None + history_search_pattern = None prompt = ':' copy = '' tab_deque = None original_line = None history = None - histories = None override = None allow_close = False - historypaths = [] + historypath = None def __init__(self, win): Widget.__init__(self, win) self.clear() - self.histories = [] - # load histories from files - if ranger.arg.clean: - for i in range(4): - self.histories.append( - History(self.settings.max_console_history_size)) - else: - self.historypaths = [relpath_conf(x) for x in \ - ('history', 'history_search', 'history_qopen', 'history_open')] - for i, path in enumerate(self.historypaths): - hist = History(self.settings.max_console_history_size) - self.histories.append(hist) - if ranger.arg.clean: continue - try: f = open(path, 'r') - except: continue + self.history = History(self.settings.max_console_history_size) + # load history from files + if not ranger.arg.clean: + self.historypath = relpath_conf('history') + try: + f = open(self.historypath, 'r') + except: + pass + else: for line in f: - hist.add(line[:-1]) + self.history.add(line[:-1]) f.close() def destroy(self): - # save histories from files + # save history to files if ranger.arg.clean or not self.settings.save_console_history: return - for i, path in enumerate(self.historypaths): - try: f = open(path, 'w') - except: continue - for entry in self.histories[i]: - f.write(entry + '\n') - f.close() - - def init(self): - """override this. Called directly after class change""" + if self.historypath: + try: + f = open(self.historypath, 'w') + except: + pass + else: + for entry in self.history: + f.write(entry + '\n') + f.close() def draw(self): - if self.mode is None: - return - self.win.erase() self.addstr(0, 0, self.prompt) overflow = -self.wid + len(self.prompt) + uwid(self.line) + 1 @@ -117,32 +91,28 @@ class Console(Widget): except: pass - def open(self, mode, string='', prompt=None): - if not is_valid_mode(mode): - return False + def open(self, string='', prompt=None, position=None): if prompt is not None: assert isinstance(prompt, str) self.prompt = prompt elif 'prompt' in self.__dict__: del self.prompt - cls = mode_to_class(mode) - if self.last_cursor_mode is None: try: self.last_cursor_mode = curses.curs_set(1) except: pass - self.mode = mode - self.__class__ = cls - self.history = self.histories[DEFAULT_HISTORY] - self.init() self.allow_close = False self.tab_deque = None self.focused = True self.visible = True self.line = string + self.history_search_pattern = self.line self.pos = len(string) + if position is not None: + self.pos = min(self.pos, position) + self.history.fast_forward() self.history.add('') return True @@ -215,7 +185,10 @@ class Console(Widget): else: if self.line != current and self.line != self.history.top(): self.history.modify(self.line) - self.history.move(n) + if self.history_search_pattern: + self.history.search(self.history_search_pattern, n) + else: + self.history.move(n) current = self.history.current() if self.line != current: self.line = self.history.current() @@ -223,7 +196,7 @@ class Console(Widget): def add_to_history(self): self.history.fast_forward() - self.history.modify(self.line) + self.history.modify(self.line, unique=True) def move(self, **keywords): direction = Direction(keywords) @@ -258,15 +231,15 @@ class Console(Widget): self.on_line_change() def delete_word(self): - self.tab_deque = None - try: - i = self.line.rindex(' ', 0, self.pos - 1) + 1 - self.line = self.line[:i] + self.line[self.pos:] + if self.line: + self.tab_deque = None + i = len(self.line) - 2 + while i >= 0 and re.match(r'[\w\d]', self.line[i], re.U): + i -= 1 + self.copy = self.line[i + 1:] + self.line = self.line[:i + 1] self.pos = len(self.line) - except ValueError: - self.line = '' - self.pos = 0 - self.on_line_change() + self.on_line_change() def delete(self, mod): self.tab_deque = None @@ -282,56 +255,6 @@ class Console(Widget): self.line = left_part + ''.join(uc[upos+1:]) self.on_line_change() - def execute(self): - pass - - def tab(self): - pass - - def on_line_change(self): - pass - - -class ConsoleWithTab(Console): - def tab(self, n=1): - if self.tab_deque is None: - tab_result = self._get_tab() - - if isinstance(tab_result, str): - self.line = tab_result - self.pos = len(tab_result) - self.on_line_change() - - elif tab_result == None: - pass - - elif hasattr(tab_result, '__iter__'): - self.tab_deque = deque(tab_result) - self.tab_deque.appendleft(self.line) - - if self.tab_deque is not None: - self.tab_deque.rotate(-n) - self.line = self.tab_deque[0] - self.pos = len(self.line) - self.on_line_change() - - def _get_tab(self): - """ - Override this function in the subclass! - - It should return either a string, an iterable or None. - If a string is returned, tabbing will result in the line turning - into that string. - If another iterable is returned, each tabbing will cycle through - the elements of the iterable (which have to be strings). - If None is returned, nothing will happen. - """ - - return None - - -class CommandConsole(ConsoleWithTab): - prompt = ':' def execute(self, cmd=None): self.allow_close = True @@ -355,7 +278,7 @@ class CommandConsole(ConsoleWithTab): except: return None else: - return command_class(self.line, self.mode) + return command_class(self.line) def _get_cmd_class(self): return self.fm.commands.get_command(self.line.split()[0]) @@ -370,277 +293,35 @@ class CommandConsole(ConsoleWithTab): return self.fm.commands.command_generator(self.line) + def tab(self, n=1): + if self.tab_deque is None: + tab_result = self._get_tab() + + if isinstance(tab_result, str): + self.line = tab_result + self.pos = len(tab_result) + self.on_line_change() + + elif tab_result == None: + pass + + elif hasattr(tab_result, '__iter__'): + self.tab_deque = deque(tab_result) + self.tab_deque.appendleft(self.line) + + if self.tab_deque is not None: + self.tab_deque.rotate(-n) + self.line = self.tab_deque[0] + self.pos = len(self.line) + self.on_line_change() -class QuickCommandConsole(CommandConsole): - """ - The QuickCommandConsole is essentially the same as the - CommandConsole, and includes one additional feature: - After each letter you type, it checks whether the command as it - stands there could be executed in a meaningful way, and if it does, - run it right away. - - Example: - >cd .. - As you type the last dot, The console will recognize what you mean - and enter the parent directory saving you the time of pressing enter. - """ - prompt = '>' def on_line_change(self): + self.history_search_pattern = self.line try: cls = self._get_cmd_class() except (KeyError, ValueError, IndexError): pass else: - cmd = cls(self.line, self.mode) + cmd = cls(self.line) if cmd and cmd.quick(): self.execute(cmd) - - -class SearchConsole(Console): - prompt = '/' - - def init(self): - self.history = self.histories[SEARCH_HISTORY] - - def execute(self): - self.fm.search_file(self.line, regexp=True) - self.close() - - -class OpenConsole(ConsoleWithTab): - """ - 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 - - There is a special syntax for more control: - - !d! mplayer will run mplayer with flags (d means detached) - !@ mplayer will open the selected files with mplayer - (equivalent to !mplayer %s) - - Those two can be combinated: - - !d!@mplayer will open the selection with a detached mplayer - (again, this is equivalent to !d!mplayer %s) - - For a list of other flags than "d", check chapter 2.5 of the documentation - """ - prompt = '!' - - def init(self): - self.history = self.histories[OPEN_HISTORY] - - def execute(self): - command, flags = self._parse() - if not command and 'p' in flags: - command = 'cat %f' - if command: - if _CustomTemplate.delimiter in command: - command = self._substitute_metachars(command) - self.fm.execute_command(command, flags=flags) - self.close() - - def _get_tab(self): - try: - i = self.line.index('!')+1 - except ValueError: - line = self.line - start = '' - else: - line = self.line[i:] - start = self.line[:i] - - try: - position_of_last_space = line.rindex(" ") - except ValueError: - return (start + program + ' ' for program \ - in get_executables() if program.startswith(line)) - if position_of_last_space == len(line) - 1: - return self.line + '%s ' - else: - before_word, start_of_word = self.line.rsplit(' ', 1) - return (before_word + ' ' + file.shell_escaped_basename \ - for file in self.fm.env.cwd.files \ - if file.shell_escaped_basename.startswith(start_of_word)) - - def _substitute_metachars(self, command): - macros = {} - - if self.fm.env.cf: - macros['f'] = shell_quote(self.fm.env.cf.basename) - else: - macros['f'] = '' - - macros['s'] = ' '.join(shell_quote(fl.basename) \ - for fl in self.fm.env.get_selection()) - - 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: - flags, cmd = self.line.split('!', 1) - else: - flags, cmd = '', self.line - - add_selection = False - if cmd.startswith('@'): - cmd = cmd[1:] - add_selection = True - elif flags.startswith('@'): - flags = flags[1:] - add_selection = True - - if add_selection: - cmd += ' ' + ' '.join(shell_quote(fl.basename) \ - for fl in self.env.get_selection()) - - return (cmd, flags) - - -class QuickOpenConsole(ConsoleWithTab): - """ - The Quick Open Console allows you to open files with predefined programs - and modes very quickly. By adding flags to the command, you can specify - precisely how the program is run, e.g. the d-flag will run it detached - from the file manager. - - For a list of other flags than "d", check chapter 2.5 of the documentation - - The syntax is "open with: <application> <mode> <flags>". - The parsing of the arguments is very flexible. You can leave out one or - more arguments (or even all of them) and it will fall back to default - values. You can switch the order as well. - There is just one rule: - - If you supply the <application>, it has to be the first argument. - - Examples: - - open with: mplayer D open the selection in mplayer, but not detached - 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) - """ - - prompt = 'open with: ' - - def init(self): - self.history = self.histories[QUICKOPEN_HISTORY] - - def execute(self): - split = self.line.split() - app, flags, mode = self._get_app_flags_mode() - self.fm.execute_file( - files = [self.env.cf], - app = app, - flags = flags, - mode = mode ) - self.close() - - def _get_app_flags_mode(self): - """ - Extracts the application, flags and mode from - a string entered into the "openwith_quick" console. - """ - # examples: - # "mplayer d 1" => ("mplayer", "d", 1) - # "aunpack 4" => ("aunpack", "", 4) - # "p" => ("", "p", 0) - # "" => None - - app = '' - flags = '' - mode = 0 - split = self.line.split() - - if len(split) == 0: - pass - - elif len(split) == 1: - part = split[0] - if self._is_app(part): - app = part - elif self._is_flags(part): - flags = part - elif self._is_mode(part): - mode = part - - elif len(split) == 2: - part0 = split[0] - part1 = split[1] - - if self._is_app(part0): - app = part0 - if self._is_flags(part1): - flags = part1 - elif self._is_mode(part1): - mode = part1 - elif self._is_flags(part0): - flags = part0 - if self._is_mode(part1): - mode = part1 - elif self._is_mode(part0): - mode = part0 - if self._is_flags(part1): - flags = part1 - - elif len(split) >= 3: - part0 = split[0] - part1 = split[1] - part2 = split[2] - - if self._is_app(part0): - app = part0 - if self._is_flags(part1): - flags = part1 - if self._is_mode(part2): - mode = part2 - elif self._is_mode(part1): - mode = part1 - if self._is_flags(part2): - flags = part2 - elif self._is_flags(part0): - flags = part0 - if self._is_mode(part1): - mode = part1 - elif self._is_mode(part0): - mode = part0 - if self._is_flags(part1): - flags = part1 - - return app, flags, int(mode) - - def _get_tab(self): - if ' ' not in self.line: - all_apps = self.fm.apps.all() - if all_apps: - return (app for app in all_apps if app.startswith(self.line)) - - return None - - def _is_app(self, arg): - return self.fm.apps.has(arg) or \ - (not self._is_flags(arg) and arg in get_executables()) - - def _is_flags(self, arg): - return all(x in ALLOWED_FLAGS for x in arg) - - def _is_mode(self, arg): - return all(x in '0123456789' for x in arg) diff --git a/ranger/gui/widgets/console_mode.py b/ranger/gui/widgets/console_mode.py deleted file mode 100644 index c29f9959..00000000 --- a/ranger/gui/widgets/console_mode.py +++ /dev/null @@ -1,55 +0,0 @@ -# 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/>. - -DEFAULT = 0 -COMMAND = 1 -COMMAND_QUICK = 2 -OPEN = 3 -OPEN_QUICK = 4 -SEARCH = 5 - -def is_valid_mode(mode): - """ - Returns True or False depending on whether the mode is valid or not. - """ - return isinstance(mode, int) and mode >= 0 and mode <= 5 - -def all_modes(mode): - """ - Returns a generator containing all valid modes. - """ - return range(6) - -def mode_to_class(mode): - """ - Associates modes with the actual classes - from ranger.gui.widgets.console. - """ - from .console import Console, CommandConsole, OpenConsole, \ - QuickOpenConsole, QuickCommandConsole, SearchConsole - - if mode == DEFAULT: - return Console - if mode == COMMAND: - return CommandConsole - if mode == OPEN: - return OpenConsole - if mode == OPEN_QUICK: - return QuickOpenConsole - if mode == COMMAND_QUICK: - return QuickCommandConsole - if mode == SEARCH: - return SearchConsole - raise ValueError diff --git a/ranger/help/console.py b/ranger/help/console.py index d1764841..f03491db 100644 --- a/ranger/help/console.py +++ b/ranger/help/console.py @@ -13,59 +13,33 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. """ -3. Basic movement and browsing +3. The Console -3.1. Overview of all the Console Modes +3.1. General Information 3.2. List of Commands -3.3. The (Quick) Command Console -3.4. The Open Console -3.5. The Quick Open Console - +3.3. Macros +3.4. The more complicated Commands in Detail ============================================================================== -3.1. Overview of all the Console Modes: - -There are, as of now, five different types of consoles: - -3.1.1. The Command Console, opened by pressing ":" - Usable for entering commands like you know it from vim. - Use the tab key to cycle through all available commands - and press F1 to view the docstring of the current command. - -3.1.2. The Quick Command Console, opened with ">" - Works just like 3.1.1, but it saves you pressing <RETURN> by checking - whether the current input can be executed in a meaningful way - and doing so automatically. (works with commands like cd, find.) - -3.1.3. The Search Console, opened with "/" - Very much like the console you get in vim after pressing "/". - The current directory will be searched for files whose names - match the given regular expression. +3.1. General Information -3.1.4. The Open Console, activated with "!" - Similar to ":!..." in vim. The given command will be executed, - "%s" will be replaced with all the files in the selection, - "%f" is replaced with only the current highlighted file. - There are further options to tweak how the command is executed. +The console is opened by pressing ":". Press <TAB> to cycle through all +available commands and press <F1> to view help about the current command. -3.1.5. The Quick Open Console, opened by pressing "r" - Rather than having to enter the full command, this console gives - you a higher level interface to running files. - You can define the application, the mode and the flags separated - with spaces. +All commands are defined in the file ranger/defaults/commands.py, which +also contains a detailed specification. ============================================================================== 3.2. List of Commands -This is a list of the few commands which are implemented by default. -All commands except for ":delete" can be abbreviated to the shortest +All commands except for ":delete" can be abbreviated with the shortest unambiguous name, e.g. ":chmod" can be written as ":ch" but not as ":c" since it conflicts with ":cd". :cd <dirname> - Changes the directory to <dirname> * + Changes the directory to <dirname> :chmod <octal_number> Sets the permissions of the selection to the octal number. @@ -86,10 +60,9 @@ it conflicts with ":cd". :filter <string> Displays only files which contain <string> in their basename. -:find <string> - Look for a partial, case insensitive match in the filenames - of the current directory and execute it if there is only - one match. * +:find <regexp> + Quickly find files that match the regexp and execute the first + unambiguous match. :grep <string> Looks for a string in all marked files or directory. @@ -104,65 +77,34 @@ it conflicts with ":cd". :mkdir <dirname> Creates a directory with the name <dirname> +:open_with [<program>] [<flags>] [<mode>] + Open the current file with the program, flags and mode. |24?| |25?| + All arguments are optional. If none is given, its equivalent to + pressing <Enter> + :quit Exits ranger :rename <newname> Changes the name of the currently highlighted file to <newname> +:search <regexp> + Search for a regexp in all file names, like the / key in vim. + +:shell [-<flags>] <command> + Run the command, optionally with some flags. |25?| + Example: shell -d firefox -safe-mode %s + opens (detached from ranger) the selection in firefox' safe-mode + :terminal Spawns "x-terminal-emulator" starting in the current directory. :touch <filename> Creates a file with the name <filename> -* implements handler for the Quick Command Console. - ============================================================================== -3.3. The (Quick) Command Console - -Open these consoles by pressing ":" or ">" - -The Command Console and the "Quick" Command Console are mostly identical -since they share the commands. As explained in 3.1.2, the point in using -the Quick Command Console is that the command is executed without the -need to press <RETURN> as soon as ranger thinks you want it executed. - -Take the "find" command, for example. It will attempt to find a file -or directory which matches the given input, and if there is one unambiguous -match, that file will be executed or that directory will be entered. -If you use the "find" command in the quick console, as soon as there is -one unambiguous match, <RETURN> will be pressed for you, giving you a -very fast way to browse your files. - - -All commands are defined in ranger/defaults/commands.py. You can refer to this -file for a list of commands. Implementing new commands should be intuitive: -Create a new class, a subclass of Command, and define the execute method -is usually enough. For parsing command input, the command parser in -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() method will be specially -treated by the Quick Command Console. For the rest, both consoles are equal. -quick() is called after each key press and if it returns True, the -command will be executed immediately. - - -Pressing F1 inside the console displays the docstring of the current command -in the pager if docstrings are available (i.e. unless python is run with -the flag "-OO" which removes all docstrings.) - - -============================================================================== -3.4. The Open Console - -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. +3.3. Macros Like in similar filemanagers there are some macros. Use them in commands and they will be replaced with a list of files. @@ -173,61 +115,41 @@ commands and they will be replaced with a list of files. %t all tagged files in the current directory %c the full paths of the currently copied/cut files +The macros %f, %d and %s also have upper case variants, %F, %D and %S, +which refer to the next tab. To refer to specific tabs, add a number in +between. Examples: + %D The path of the directory in the next tab + %7s The selection of the seventh tab + %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: - -!d! mplayer will run mplayer with flags (d means detached) -!@ mplayer will open the selected files with mplayer - (equivalent to !mplayer %s) - -Those two can be combinated: - -!d!@mplayer will open the selection with a detached mplayer - (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 + :shell -p diff %c %f ============================================================================== -3.5. The Quick Open Console - -Open this console by pressing "r" - -The Quick Open Console allows you to open files with predefined programs -and modes very quickly. By adding flags to the command, you can specify -precisely how the program is run, e.g. the d-flag will run it detached -from the file manager. - -For a list of other flags than "d", check chapter 2.5 of the documentation - -The syntax is "open with: <application> <mode> <flags>". -The parsing of the arguments is very flexible. You can leave out one or -more arguments (or even all of them) and it will fall back to default -values. You can switch the order as well. -There is just one rule: - -If you supply the <application>, it has to be the first argument. - -Examples: - -open with: mplayer D open the selection in mplayer, but not detached -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) - +3.4. The more complicated Commands in Detail + +3.3.1. "find" +The find command is different than others: it doesn't require you to +press <RETURN>. To speed things up, it tries to guess when you're +done typing and executes the command right away. +The key "f" opens the console with ":find " + +3.3.2. "shell" +The shell command accepts flags |25?| as the first argument. This example +will use the "p"-flag, which pipes the output to the pager: + :shell -p cat somefile.txt + +There are some shortcuts which open the console with the shell command: + "!" opens ":shell " + "@" opens ":shell %s" + "#" opens ":shell -p " + +3.3.3. "open_with" +The open_with command is explained in detail in chapter 2.2. |22?| ============================================================================== """ diff --git a/ranger/help/fileop.py b/ranger/help/fileop.py index 53ce9ff8..ac23c6d4 100644 --- a/ranger/help/fileop.py +++ b/ranger/help/fileop.py @@ -31,7 +31,7 @@ harm your files: :chmod <number> Change the rights of the selection :delete DELETES ALL FILES IN THE SELECTION :rename <newname> Change the name of the current file -pp, pl, po Pastes the copied files in different ways +pp, pl, pL, po Pastes the copied files in different ways Think twice before using these commands or key combinations. @@ -60,10 +60,14 @@ The "highlighted file", or the "current file", is the one below the cursor. yy copy the selection dd cut the selection + ya, da add the selection to the copied/cut files + yr, dr remove the selection from the copied/cut files + pp paste the copied/cut files. No file will be overwritten. Instead, a "_" character will be appended to the new filename. po paste the copied/cut files. Existing files are overwritten. pl create symbolic links to the copied/cut files. + pL create relative symbolic links to the copied/cut files. The difference between copying and cutting should be intuitive: @@ -75,6 +79,9 @@ If renaming is not possible because the source and the destination are on separate devices, it will be copied and eventually the source is deleted. This implies that a file can only be cut + pasted once. +The files are either copied or cut, never mixed even if you mix "da" and "ya" +keys (in which case the last command is decisive about whether they are copied +or cut.) ============================================================================== 4.4. Task View diff --git a/ranger/help/movement.py b/ranger/help/movement.py index 3abec359..564b226b 100644 --- a/ranger/help/movement.py +++ b/ranger/help/movement.py @@ -74,6 +74,9 @@ This keys can be used to make movements beyond the current directory { traverse in the other direction. (not implemented yet, currently this only moves back in history) + gl move to the real path of the current directory (resolving symlinks) + gL move to the real path of the selected file or directory + ============================================================================== 1.2. Browser control diff --git a/ranger/help/starting.py b/ranger/help/starting.py index 99cfc45e..1796f83d 100644 --- a/ranger/help/starting.py +++ b/ranger/help/starting.py @@ -17,7 +17,7 @@ 2. Running Files 2.1. How to run files -2.2. The "open with" prompt +2.2. The "open_with" command 2.2. Programs 2.4. Modes 2.5. Flags @@ -30,40 +30,47 @@ While highlighting a file, press the "l" key to fire up the automatic filetype detection mechanism and attempt to start the file. l run the selection - r open the "open with" prompt + r open the console with ":open_with" Note: The selection means, if there are marked files in this directory, use them. Otherwise use the file under the cursor. ============================================================================== -2.2. The "open with" prompt +2.2. The "open_with" command 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. -Syntax: open with: <program> <flags> <mode> +The programs and modes can be defined in the apps.py, giving you a +high level interface for running files. + +Syntax: :open_with <program> <flags> <mode> +You can leave out parameters or change the order. Examples: Open this file with vim: - open with: vim + :open_with vim Run this file like with "./file": - open with: self + :open_with self +Open this file as usual but pipe the output to "less" + :open_with p Open this file with mplayer with the "detached" flag: - open with: mplayer d + :open_with mplayer d +Open this file with totem in mode 1, will not detach the process (flag D) +but discard the output (flag s). + :open_with totem 1 Ds The parameters <program>, <flags> and <mode> are explained in the following paragraphs -Note: The "open with" console is named QuickOpenConsole in the source code. - ============================================================================== 2.3. Programs Programs have to be defined in ranger/defaults/apps.py. Each function in the class CustomApplications which starts with "app_" can be used -as a program in the "open with" prompt. +as a program in the "open_with" command. You're encouraged to add your own program definitions to the list. Refer to the existing examples in the apps.py, it should be easy to adapt it for your @@ -81,8 +88,8 @@ gives you 2 ways of opening a video (by default): By specifying a mode, you can select one of those. The "l" key will start a file in mode 0. "4l" will start the file in mode 4 etc. -You can specify a mode in the "open with" console by simply adding -the number. Eg: "open with: mplayer 1" or "open with: 1" +You can specify a mode in the "open_with" command by simply adding +the number. Eg: ":open_with mplayer 1" or ":open_with 1" For a list of all programs and modes, see ranger/defaults/apps.py @@ -97,11 +104,11 @@ Flags give you a way to modify the behaviour of the spawned process. p Redirect output to the pager w Wait for an enter-press when the process is done -For example, "open with: p" will pipe the output of that process into +For example, ":open_with p" will pipe the output of that process into the pager. An uppercase flag has the opposite effect. If a program will be detached by -default, use "open with: D" to not detach it. +default, use ":open_with D" to not detach it. Note: Some combinations don't make sense, eg: "vim d" would open the file in vim and detach it. Since vim is a console application, you loose grip diff --git a/ranger/shared/mimetype.py b/ranger/shared/mimetype.py index c6577056..da6fcd10 100644 --- a/ranger/shared/mimetype.py +++ b/ranger/shared/mimetype.py @@ -15,9 +15,12 @@ from ranger import relpath import mimetypes +import os.path + class MimeTypeAware(object): mimetypes = {} def __init__(self): MimeTypeAware.__init__ = lambda _: None # refuse multiple inits + mimetypes.knownfiles.append(os.path.expanduser('~/.mime.types')) MimeTypeAware.mimetypes = mimetypes.MimeTypes() MimeTypeAware.mimetypes.read(relpath('data/mime.types')) diff --git a/ranger/shared/settings.py b/ranger/shared/settings.py index 24aea39c..41334ada 100644 --- a/ranger/shared/settings.py +++ b/ranger/shared/settings.py @@ -20,36 +20,36 @@ from ranger.ext.signal_dispatcher import SignalDispatcher from ranger.ext.openstruct import OpenStruct ALLOWED_SETTINGS = { - 'show_hidden': bool, - 'show_hidden_bookmarks': bool, - 'show_cursor': bool, 'autosave_bookmarks': bool, - 'save_console_history': bool, 'collapse_preview': bool, + 'colorscheme_overlay': (type(None), type(lambda:0)), + 'colorscheme': str, 'column_ratios': (tuple, list, set), + 'dirname_in_tabs': bool, 'display_size_in_main_column': bool, 'display_size_in_status_bar': bool, - 'draw_borders': bool, 'draw_bookmark_borders': bool, - 'dirname_in_tabs': bool, - 'sort': str, - 'sort_reverse': bool, - 'sort_case_insensitive': bool, - 'sort_directories_first': bool, - 'update_title': bool, - 'shorten_title': int, # Note: False is an instance of int - 'tilde_in_titlebar': bool, - 'max_history_size': (int, type(None)), + 'draw_borders': bool, + 'flushinput': bool, + 'hidden_filter': lambda x: isinstance(x, str) or hasattr(x, 'match'), 'max_console_history_size': (int, type(None)), + 'max_history_size': (int, type(None)), + 'mouse_enabled': bool, + 'preview_directories': bool, + 'preview_files': bool, 'preview_script': (str, type(None)), + 'save_console_history': bool, 'scroll_offset': int, - 'preview_files': bool, - 'preview_directories': bool, - 'mouse_enabled': bool, - 'flushinput': bool, - 'colorscheme': str, - 'colorscheme_overlay': (type(None), type(lambda:0)), - 'hidden_filter': lambda x: isinstance(x, str) or hasattr(x, 'match'), + 'shorten_title': int, # Note: False is an instance of int + 'show_cursor': bool, + 'show_hidden_bookmarks': bool, + 'show_hidden': bool, + 'sort_case_insensitive': bool, + 'sort_directories_first': bool, + 'sort_reverse': bool, + 'sort': str, + 'tilde_in_titlebar': bool, + 'update_title': bool, 'xterm_alt_key': bool, } @@ -67,7 +67,9 @@ class SettingObject(SignalDispatcher): if name[0] == '_': self.__dict__[name] = value else: - assert name in self._settings, "No such setting: {0}!".format(name) + assert name in ALLOWED_SETTINGS, "No such setting: {0}!".format(name) + if name not in self._settings: + getattr(self, name) assert self._check_type(name, value) kws = dict(setting=name, value=value, previous=self._settings[name]) diff --git a/setup.py b/setup.py index 17cfc660..587b52c0 100755 --- a/setup.py +++ b/setup.py @@ -17,26 +17,27 @@ import distutils.core import ranger -distutils.core.setup( - name='ranger', - description='Vim-like file manager', - version=ranger.__version__, - author=ranger.__author__, - author_email=ranger.__email__, - license=ranger.__license__, - url='http://savannah.nongnu.org/projects/ranger', - scripts=['scripts/ranger'], - data_files=[('share/man/man1', ['doc/ranger.1'])], - package_data={'ranger': ['data/*']}, - packages=('ranger', - 'ranger.api', - 'ranger.colorschemes', - 'ranger.container', - 'ranger.core', - 'ranger.defaults', - 'ranger.ext', - 'ranger.fsobject', - 'ranger.gui', - 'ranger.gui.widgets', - 'ranger.help', - 'ranger.shared')) +if __name__ == '__main__': + distutils.core.setup( + name='ranger', + description='Vim-like file manager', + version=ranger.__version__, + author=ranger.__author__, + author_email=ranger.__email__, + license=ranger.__license__, + url='http://savannah.nongnu.org/projects/ranger', + scripts=['scripts/ranger'], + data_files=[('share/man/man1', ['doc/ranger.1'])], + package_data={'ranger': ['data/*']}, + packages=('ranger', + 'ranger.api', + 'ranger.colorschemes', + 'ranger.container', + 'ranger.core', + 'ranger.defaults', + 'ranger.ext', + 'ranger.fsobject', + 'ranger.gui', + 'ranger.gui.widgets', + 'ranger.help', + 'ranger.shared')) diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/test/__init__.py +++ /dev/null diff --git a/test/all_benchmarks.py b/test/all_benchmarks.py index 20f11ad8..a3612701 100755 --- a/test/all_benchmarks.py +++ b/test/all_benchmarks.py @@ -19,9 +19,13 @@ Run all the benchmarks inside this directory. Usage: ./all_benchmarks.py [count] [regexp-filters...] """ -import os -import re +import os.path import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + +import re import time if __name__ == '__main__': diff --git a/test/all_tests.py b/test/all_tests.py index 7cfc855f..0c184df5 100755 --- a/test/all_tests.py +++ b/test/all_tests.py @@ -19,8 +19,12 @@ Run all the tests inside this directory as a test suite. Usage: ./all_tests.py [verbosity] """ -import os +import os.path import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + import unittest if __name__ == '__main__': diff --git a/test/bm_human_readable.py b/test/bm_human_readable.py new file mode 100644 index 00000000..ef400774 --- /dev/null +++ b/test/bm_human_readable.py @@ -0,0 +1,51 @@ +# 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/>. + +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + +from ranger.ext.human_readable import * + +# The version before 2010/06/24: +import math +UNITS = 'BKMGTP' +MAX_EXPONENT = len(UNITS) - 1 +def human_readable_old(byte, seperator=' '): + if not byte: + return '0' + + exponent = int(math.log(byte, 2) / 10) + flt = round(float(byte) / (1 << (10 * exponent)), 2) + + if exponent > MAX_EXPONENT: + return '>9000' # off scale + + if int(flt) == flt: + return '%.0f%s%s' % (flt, seperator, UNITS[exponent]) + + else: + return '%.2f%s%s' % (flt, seperator, UNITS[exponent]) + +class benchmark_human_readable(object): + def bm_current(self, n): + for i in range(n): + human_readable((128 * i) % 2**50) + + def bm_old(self, n): + for i in range(n): + human_readable_old((128 * i) % 2**50) diff --git a/test/bm_loader.py b/test/bm_loader.py index 968640a5..552954a7 100644 --- a/test/bm_loader.py +++ b/test/bm_loader.py @@ -13,6 +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/>. +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + from ranger.core.loader import Loader from ranger.fsobject import Directory, File from ranger.ext.openstruct import OpenStruct diff --git a/test/ranger b/test/ranger deleted file mode 120000 index 5459458c..00000000 --- a/test/ranger +++ /dev/null @@ -1 +0,0 @@ -../ranger \ No newline at end of file diff --git a/test/tc_bookmarks.py b/test/tc_bookmarks.py index 9b41f1c6..59435f06 100644 --- a/test/tc_bookmarks.py +++ b/test/tc_bookmarks.py @@ -13,6 +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/>. +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + from os.path import realpath, join, dirname import unittest import os diff --git a/test/tc_colorscheme.py b/test/tc_colorscheme.py index 8d6adee6..eefb1e4f 100644 --- a/test/tc_colorscheme.py +++ b/test/tc_colorscheme.py @@ -13,6 +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/>. +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + from unittest import TestCase, main import random import ranger.colorschemes diff --git a/test/tc_direction.py b/test/tc_direction.py index 5c1776cf..16c26dab 100644 --- a/test/tc_direction.py +++ b/test/tc_direction.py @@ -13,6 +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/>. +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + import unittest from ranger.ext.direction import Direction from ranger.ext.openstruct import OpenStruct diff --git a/test/tc_directory.py b/test/tc_directory.py index a702c4db..754253b3 100644 --- a/test/tc_directory.py +++ b/test/tc_directory.py @@ -13,7 +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/>. +import os.path import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + from os.path import realpath, join, dirname from ranger import fsobject diff --git a/test/tc_displayable.py b/test/tc_displayable.py index 1c66a40e..72e0507d 100644 --- a/test/tc_displayable.py +++ b/test/tc_displayable.py @@ -13,6 +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/>. +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + import unittest import curses from random import randint diff --git a/test/tc_ext.py b/test/tc_ext.py index 919f35a2..495591a1 100644 --- a/test/tc_ext.py +++ b/test/tc_ext.py @@ -13,6 +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/>. +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + import unittest from collections import deque diff --git a/test/tc_history.py b/test/tc_history.py index 33784e14..02a8bb9f 100644 --- a/test/tc_history.py +++ b/test/tc_history.py @@ -13,6 +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/>. +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + from ranger.container import History from unittest import TestCase, main import unittest @@ -27,13 +33,13 @@ class Test(TestCase): hist.back() self.assertEqual(4, hist.current()) - self.assertEqual([3,4], list(hist)) + self.assertEqual([3,4], list(hist._left())) self.assertEqual(5, hist.top()) hist.back() self.assertEqual(3, hist.current()) - self.assertEqual([3], list(hist)) + self.assertEqual([3], list(hist._left())) # no change if current == bottom self.assertEqual(hist.current(), hist.bottom()) @@ -46,12 +52,31 @@ class Test(TestCase): hist.forward() hist.forward() self.assertEqual(5, hist.current()) - self.assertEqual([3,4,5], list(hist)) + self.assertEqual([3,4,5], list(hist._left())) self.assertEqual(3, hist.bottom()) hist.add(6) self.assertEqual(4, hist.bottom()) - self.assertEqual([4,5,6], list(hist)) + self.assertEqual([4,5,6], list(hist._left())) + + hist.back() + hist.fast_forward() + self.assertEqual([4,5,6], list(hist._left())) + hist.back() + hist.back() + hist.fast_forward() + self.assertEqual([4,5,6], list(hist._left())) + hist.back() + hist.back() + hist.back() + hist.fast_forward() + self.assertEqual([4,5,6], list(hist._left())) + hist.back() + hist.back() + hist.back() + hist.back() + hist.fast_forward() + self.assertEqual([4,5,6], list(hist._left())) if __name__ == '__main__': main() diff --git a/test/tc_human_readable.py b/test/tc_human_readable.py new file mode 100644 index 00000000..493e6d3a --- /dev/null +++ b/test/tc_human_readable.py @@ -0,0 +1,51 @@ +# 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/>. + +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + +import unittest +from ranger.ext.human_readable import human_readable as hr + +class HumanReadableTest(unittest.TestCase): + def test_basic(self): + self.assertEqual("0", hr(0)) + self.assertEqual("1 B", hr(1)) + self.assertEqual("1 K", hr(2 ** 10)) + self.assertEqual("1 M", hr(2 ** 20)) + self.assertEqual("1 G", hr(2 ** 30)) + self.assertEqual(">9000", hr(2 ** 100)) + + def test_big(self): + self.assertEqual("1023 G", hr(2 ** 30 * 1023)) + self.assertEqual("1024 G", hr(2 ** 40 - 1)) + self.assertEqual("1 T", hr(2 ** 40)) + + def test_small(self): + self.assertEqual("1000 B", hr(1000)) + self.assertEqual("1.66 M", hr(1.66 * 2 ** 20)) + self.assertEqual("1.46 K", hr(1500)) + self.assertEqual("1.5 K", hr(2 ** 10 + 2 ** 9)) + self.assertEqual("1.5 K", hr(2 ** 10 + 2 ** 9 - 1)) + + def test_no_exponent(self): + for i in range(2 ** 10, 2 ** 20, 512): + self.assertTrue('e' not in hr(i), "%d => %s" % (i, hr(i))) + +if __name__ == '__main__': + unittest.main() diff --git a/test/tc_keyapi.py b/test/tc_keyapi.py index 48282a7d..79d89fa5 100644 --- a/test/tc_keyapi.py +++ b/test/tc_keyapi.py @@ -13,6 +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/>. +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + from unittest import TestCase, main class Test(TestCase): diff --git a/test/tc_loader.py b/test/tc_loader.py index 9f7f7322..5a2e5a68 100644 --- a/test/tc_loader.py +++ b/test/tc_loader.py @@ -13,6 +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/>. +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + import unittest import os from os.path import realpath, join, dirname diff --git a/test/tc_newkeys.py b/test/tc_newkeys.py index fd856f17..c9597201 100644 --- a/test/tc_newkeys.py +++ b/test/tc_newkeys.py @@ -12,7 +12,13 @@ # 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 +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] +sys.path[1:1] = ['..'] from unittest import TestCase, main @@ -22,8 +28,6 @@ from ranger.container.keymap import * from ranger.container.keybuffer import KeyBuffer from ranger.ext.keybinding_parser import parse_keybinding -import sys - def simulate_press(self, string): for char in parse_keybinding(string): self.add(char) diff --git a/test/tc_relative_symlink.py b/test/tc_relative_symlink.py new file mode 100644 index 00000000..a202513d --- /dev/null +++ b/test/tc_relative_symlink.py @@ -0,0 +1,47 @@ +# 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/>. + +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + +import unittest +from ranger.ext.relative_symlink import * +rel = get_relative_source_file + +class Test(unittest.TestCase): + def test_foo(self): + self.assertEqual('../foo', rel('/foo', '/x/bar')) + self.assertEqual('../../foo', rel('/foo', '/x/y/bar')) + self.assertEqual('../../a/b/foo', rel('/a/b/foo', '/x/y/bar')) + self.assertEqual('../../x/b/foo', rel('/x/b/foo', '/x/y/bar', + common_base='/')) + self.assertEqual('../b/foo', rel('/x/b/foo', '/x/y/bar')) + self.assertEqual('../b/foo', rel('/x/b/foo', '/x/y/bar')) + + def test_get_common_base(self): + self.assertEqual('/', get_common_base('', '')) + self.assertEqual('/', get_common_base('', '/')) + self.assertEqual('/', get_common_base('/', '')) + self.assertEqual('/', get_common_base('/', '/')) + self.assertEqual('/', get_common_base('/bla/bar/x', '/foo/bar/a')) + self.assertEqual('/foo/bar/', get_common_base('/foo/bar/x', '/foo/bar/a')) + self.assertEqual('/foo/', get_common_base('/foo/bar/x', '/foo/baz/a')) + self.assertEqual('/foo/', get_common_base('/foo/bar/x', '/foo/baz/a')) + self.assertEqual('/', get_common_base('//foo/bar/x', '/foo/baz/a')) + +if __name__ == '__main__': unittest.main() diff --git a/test/tc_signal.py b/test/tc_signal.py index f31681f4..3b1bac40 100644 --- a/test/tc_signal.py +++ b/test/tc_signal.py @@ -13,6 +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/>. +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + import unittest import gc from ranger.ext.signal_dispatcher import * diff --git a/test/tc_ui.py b/test/tc_ui.py index dc8d669d..fa2bdcac 100644 --- a/test/tc_ui.py +++ b/test/tc_ui.py @@ -13,6 +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/>. +import os.path +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] + import unittest import curses diff --git a/test/tc_utfwidth.py b/test/tc_utfwidth.py index 2aa5fa6d..0288c17b 100644 --- a/test/tc_utfwidth.py +++ b/test/tc_utfwidth.py @@ -12,7 +12,13 @@ # 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 +import sys +rangerpath = os.path.join(os.path.dirname(__file__), '..') +if sys.path[1] != rangerpath: + sys.path[1:1] = [rangerpath] +sys.path[1:1] = ['..'] from unittest import TestCase, main from ranger.ext.utfwidth import * |