diff options
author | hut <hut@lavabit.com> | 2010-09-16 18:19:46 +0200 |
---|---|---|
committer | hut <hut@lavabit.com> | 2010-09-16 18:19:46 +0200 |
commit | 4341846741b6623a1ead43485cfd5cf633453bb6 (patch) | |
tree | fdfdd0b13329ad457ad9a6a0c0091019396ae64a /ranger | |
parent | 37a60686b340f030a2fc37e7ac9d19a701de9e6b (diff) | |
parent | 0c0849c3d8bf57a8b0d0bd9d6113639c58a28fd2 (diff) | |
download | ranger-4341846741b6623a1ead43485cfd5cf633453bb6.tar.gz |
Merge branch 'master' into cp
Conflicts: ranger/__main__.py ranger/core/actions.py
Diffstat (limited to 'ranger')
46 files changed, 1195 insertions, 1057 deletions
diff --git a/ranger/__init__.py b/ranger/__init__.py index f46a1e76..1f8cc324 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.0.4' +__version__ = '1.3.0' __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 c232b489..a9a18537 100644 --- a/ranger/__main__.py +++ b/ranger/__main__.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # coding=utf-8 # # Copyright (C) 2009, 2010 Roman Zimbelmann <romanz@lavabit.com> @@ -21,7 +21,8 @@ # (ImportError will imply that this module can't be found) # convenient exception handling in ranger.py (ImportError) -import os +import locale +import os.path import sys def parse_arguments(): @@ -29,13 +30,25 @@ 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") parser.add_option('-c', '--clean', action='store_true', help="don't touch/require any config files. ") - parser.add_option('--fail-if-run', action='store_true', + parser.add_option('--fail-if-run', action='store_true', # COMPAT + help=SUPPRESS_HELP) + parser.add_option('--fail-unless-cd', action='store_true', help="experimental: return the exit code 1 if ranger is" \ "used to run a file (with `ranger filename`)") parser.add_option('-r', '--confdir', type='string', @@ -50,6 +63,9 @@ def parse_arguments(): options, positional = parser.parse_args() arg = OpenStruct(options.__dict__, targets=positional) arg.confdir = os.path.expanduser(arg.confdir) + if arg.fail_if_run: + arg.fail_unless_cd = arg.fail_if_run + del arg['fail_if_run'] return arg @@ -107,7 +123,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) @@ -147,7 +163,18 @@ def main(): print(errormessage) print('ranger requires the python curses module. Aborting.') sys.exit(1) - from locale import getdefaultlocale, setlocale, LC_ALL + + try: locale.setlocale(locale.LC_ALL, '') + except: print("Warning: Unable to set locale. Expect encoding problems.") + + if not 'SHELL' in os.environ: + os.environ['SHELL'] = 'bash' + + arg = parse_arguments() + if arg.clean: + sys.dont_write_bytecode = True + + # Need to decide whether to write bytecode or not before importing. import ranger from ranger.ext import curses_interrupt_handler from ranger.core.runner import Runner @@ -158,28 +185,15 @@ def main(): from ranger.shared import (EnvironmentAware, FileManagerAware, SettingsAware) - # Ensure that a utf8 locale is set. - try: - if getdefaultlocale()[1] not in ('utf8', 'UTF-8'): - for locale in ('en_US.utf8', 'en_US.UTF-8'): - try: setlocale(LC_ALL, locale) - except: pass - else: break - else: setlocale(LC_ALL, '') - else: setlocale(LC_ALL, '') - except: - print("Warning: Unable to set locale. Expect encoding problems.") - - arg = parse_arguments() - ranger.arg = arg - - if not ranger.arg.debug: + if not arg.debug: curses_interrupt_handler.install_interrupt_handler() + ranger.arg = arg 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): @@ -191,18 +205,18 @@ def main(): runner = Runner(logfunc=print_function) load_apps(runner, ranger.arg.clean) runner(files=[File(target)], mode=arg.mode, flags=arg.flags) - sys.exit(1 if arg.fail_if_run else 0) - else: - path = target - else: - path = '.' + sys.exit(1 if arg.fail_unless_cd else 0) - # Initialize objects - EnvironmentAware._assign(Environment(path)) - fm = FM() - crash_exception = None + crash_traceback = None try: + # Initialize objects + 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() @@ -210,21 +224,23 @@ def main(): fm.initialize() fm.ui.initialize() fm.loop() - except Exception as e: - crash_exception = e - if not (arg.debug or arg.clean): - import traceback - dumpname = ranger.relpath_conf('traceback') - traceback.print_exc(file=open(dumpname, 'w')) + except Exception: + import traceback + crash_traceback = traceback.format_exc() + except SystemExit as error: + return error.args[0] finally: - fm.destroy() - if crash_exception: - print("Fatal: " + str(crash_exception)) - if arg.debug or arg.clean: - raise crash_exception - else: - print("A traceback has been saved to " + dumpname) - print("Please include it in a bugreport.") + try: + fm.destroy() + except (AttributeError, NameError): + pass + if crash_traceback: + print(crash_traceback) + print("Ranger crashed. " \ + "Please report this (including the traceback) at:") + print("http://savannah.nongnu.org/bugs/?group=ranger&func=additem") + return 1 + return 0 if __name__ == '__main__': diff --git a/ranger/api/apps.py b/ranger/api/apps.py index d2e9ac4f..91aae357 100644 --- a/ranger/api/apps.py +++ b/ranger/api/apps.py @@ -121,7 +121,8 @@ class Applications(FileManagerAware): flags = 'flags' in keywords and keywords['flags'] or "" for name in args: assert isinstance(name, str) - setattr(cls, "app_" + name, _generic_wrapper(name, flags=flags)) + if not hasattr(cls, "app_" + name): + setattr(cls, "app_" + name, _generic_wrapper(name, flags=flags)) def tup(*args): 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/colorschemes/default88.py b/ranger/colorschemes/default88.py index 9af6dca7..9c00d9ff 100644 --- a/ranger/colorschemes/default88.py +++ b/ranger/colorschemes/default88.py @@ -20,9 +20,7 @@ For now, just map each of the 8 base colors to new ones for brighter blue, etc. and do some minor modifications. """ -from ranger.gui.colorscheme import ColorScheme from ranger.gui.color import * - from ranger.colorschemes.default import Default import curses diff --git a/ranger/colorschemes/jungle.py b/ranger/colorschemes/jungle.py index 376091c0..af10a404 100644 --- a/ranger/colorschemes/jungle.py +++ b/ranger/colorschemes/jungle.py @@ -13,7 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -from ranger.gui.colorscheme import ColorScheme from ranger.gui.color import * from ranger.colorschemes.default import Default diff --git a/ranger/colorschemes/texas.py b/ranger/colorschemes/texas.py index 93fd4791..26e8916d 100644 --- a/ranger/colorschemes/texas.py +++ b/ranger/colorschemes/texas.py @@ -17,9 +17,7 @@ Some experimental colorscheme. """ -from ranger.gui.colorscheme import ColorScheme from ranger.gui.color import * - from ranger.colorschemes.default import Default import curses diff --git a/ranger/container/bookmarks.py b/ranger/container/bookmarks.py index f06b7498..1e801638 100644 --- a/ranger/container/bookmarks.py +++ b/ranger/container/bookmarks.py @@ -156,7 +156,10 @@ class Bookmarks(object): for key, value in self.dct.items(): if type(key) == str\ and key in ALLOWED_KEYS: - f.write("{0}:{1}\n".format(str(key), str(value))) + try: + f.write("{0}:{1}\n".format(str(key), str(value))) + except: + pass f.close() self._update_mtime() 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 16978591..1493d84d 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -16,18 +16,25 @@ 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.core.loader import CommandLoader +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 @@ -68,10 +75,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. @@ -127,7 +203,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(), @@ -138,9 +214,11 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): cwd.move(to=newpos) def move_parent(self, n): - self.enter_dir('..') - self.move(down=n) - self.move(right=0) + parent = self.env.at_level(-1) + try: + self.env.enter_dir(parent.files[parent.pointer+n]) + except IndexError: + pass def history_go(self, relative): """Move back and forth in the history""" @@ -272,13 +350,26 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): if hasattr(self.ui, 'status'): self.ui.status.need_redraw = True + def mark_in_direction(self, val=True, dirarg=None): + cwd = self.env.cwd + direction = Direction(dirarg) + pos, selected = direction.select(lst=cwd.files, current=cwd.pointer, + pagesize=self.env.termsize[0]) + cwd.pointer = pos + cwd.correct_pointer() + for item in selected: + cwd.mark_item(item, val) + # -------------------------- # -- Searching # -------------------------- 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') @@ -332,7 +423,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): @@ -433,15 +524,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'): @@ -523,8 +628,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) @@ -538,22 +644,30 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): pos, selected = direction.select( override=narg, lst=cwd.files, current=cwd.pointer, pagesize=self.env.termsize[0], offset=offset) - self.env.cwd.pointer = pos - self.env.cwd.correct_pointer() - self.env.copy = set(selected) + cwd.pointer = pos + cwd.correct_pointer() + 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) diff --git a/ranger/core/environment.py b/ranger/core/environment.py index bb6abb36..61db8694 100644 --- a/ranger/core/environment.py +++ b/ranger/core/environment.py @@ -28,8 +28,8 @@ ALLOWED_CONTEXTS = ('browser', 'pager', 'embedded_pager', 'taskview', 'console') class Environment(SettingsAware, SignalDispatcher): - """A collection of data which is relevant for more than - one class. + """ + A collection of data which is relevant for more than one class. """ cwd = None # current directory @@ -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/core/runner.py b/ranger/core/runner.py index 26424881..f35f99b7 100644 --- a/ranger/core/runner.py +++ b/ranger/core/runner.py @@ -37,10 +37,21 @@ from subprocess import Popen, PIPE from ranger.ext.waitpid_no_intr import waitpid_no_intr -ALLOWED_FLAGS = 'sdpSDP' +ALLOWED_FLAGS = 'sdpwSDPW' devnull = open(os.devnull, 'a') +def press_enter(): + """Wait for an ENTER-press""" + sys.stdout.write("Press ENTER to continue") + try: + waitfnc = raw_input + except NameError: + # "raw_input" not available in python3 + waitfnc = input + waitfnc() + + class Context(object): """ A context object contains data on how to run a process. @@ -144,6 +155,7 @@ class Runner(object): toggle_ui = True pipe_output = False + wait_for_enter = False popen_kws['args'] = action if 'shell' not in popen_kws: @@ -168,6 +180,9 @@ class Runner(object): if 'd' in context.flags: toggle_ui = False context.wait = False + if 'w' in context.flags: + if not pipe_output and context.wait: # <-- sanity check + wait_for_enter = True # Finally, run it @@ -182,6 +197,8 @@ class Runner(object): else: if context.wait: waitpid_no_intr(process.pid) + if wait_for_enter: + press_enter() finally: if toggle_ui: self._activate_ui(True) diff --git a/ranger/defaults/apps.py b/ranger/defaults/apps.py index 4dd3bde5..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 * @@ -55,29 +55,29 @@ class CustomApplications(Applications): f = c.file if f.basename.lower() == 'makefile': - return self.app_make(c) + return self.either(c, 'make') if f.extension is not None: if f.extension in ('pdf', ): c.flags += 'd' return self.either(c, 'evince', 'zathura', 'apvlv') if f.extension in ('xml', ): - return self.app_editor(c) + return self.either(c, 'editor') if f.extension in ('html', 'htm', 'xhtml'): return self.either(c, 'firefox', 'opera', 'elinks') if f.extension in ('swf', ): return self.either(c, 'firefox', 'opera') if f.extension == 'nes': - return self.app_fceux(c) + return self.either(c, 'fceux') if f.extension in ('swc', 'smc'): - return self.app_zsnes(c) + return self.either(c, 'zsnes') if f.mimetype is not None: if INTERPRETED_LANGUAGES.match(f.mimetype): - return self.app_edit_or_run(c) + return self.either(c, 'edit_or_run') if f.container: - return self.app_aunpack(c) + return self.either(c, 'aunpack', 'file_roller') if f.video or f.audio: if f.video: @@ -85,16 +85,16 @@ class CustomApplications(Applications): return self.either(c, 'mplayer', 'totem') if f.image: - return self.either(c, 'feh', 'eye_of_gnome', 'mirage') + return self.either(c, 'feh', 'eog', 'mirage') if f.document or f.filetype.startswith('text'): - return self.app_editor(c) + return self.either(c, 'editor') # ----------------------------------------- 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: @@ -109,7 +109,6 @@ class CustomApplications(Applications): return self.either(c, 'vim', 'emacs', 'nano') - @depends_on(app_editor, Applications.app_self) def app_edit_or_run(self, c): if c.mode is 1: return self.app_self(c) @@ -138,25 +137,12 @@ class CustomApplications(Applications): c.flags += 'd' - if c.mode in arg: + if c.mode in arg: # mode 1, 2 and 3 will set the image as the background return tup('feh', arg[c.mode], c.file.path) - if c.mode is 4: - return self.app_gimp(c) - if len(c.files) > 1: - return tup('feh', *c) - - try: - from collections import deque - - directory = self.fm.env.get_directory(c.file.dirname) - images = [f.path for f in directory.files if f.image] - position = images.index(c.file.path) - deq = deque(images) - deq.rotate(-position) - - return tup('feh', *deq) - except: - return tup('feh', *c) + if c.mode is 11 and len(c.files) is 1: # view all files in the cwd + images = (f.basename for f in self.fm.env.cwd.files if f.image) + return tup('feh', '--start-at', c.file.basename, *images) + return tup('feh', *c) @depends_on('aunpack') def app_aunpack(self, c): @@ -165,6 +151,11 @@ class CustomApplications(Applications): return tup('aunpack', '-l', c.file.path) return tup('aunpack', c.file.path) + @depends_on('file-roller') + def app_file_roller(self, c): + c.flags += 'd' + return tup('file-roller', c.file.path) + @depends_on('make') def app_make(self, c): if c.mode is 0: 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 979d77e5..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()) # =================================================================== @@ -163,15 +163,22 @@ map('T', fm.tag_remove()) map(' ', fm.mark(toggle=True)) map('v', fm.mark(all=True, toggle=True)) map('V', 'uv', fm.mark(all=True, val=False)) +map('<C-V><dir>', fm.mark_in_direction(val=True)) +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('p<bg>', fm.hint('press *p* once again to confirm pasting' \ - ', or *l* to create symlinks')) +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*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()) @@ -179,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')) @@ -192,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" \ @@ -218,31 +225,33 @@ 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('/')) map('gm', fm.cd('/media')) -map('gn', fm.cd('/mnt')) +map('gM', fm.cd('/mnt')) map('gs', fm.cd('/srv')) map('gR', fm.cd(RANGERDIR)) @@ -259,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)) @@ -298,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 ')) # =================================================================== @@ -313,8 +321,8 @@ map.merge(global_keys) map.merge(vim_aliases) # -------------------------------------------------------- movement -map('<left>', wdg.move(left=4)) -map('<right>', wdg.move(right=4)) +map('<left>', 'h', wdg.move(left=4)) +map('<right>', 'l', wdg.move(right=4)) map('<C-D>', 'd', wdg.move(down=0.5, pages=True)) map('<C-U>', 'u', wdg.move(up=0.5, pages=True)) map('<C-F>', 'f', '<pagedown>', wdg.move(down=1, pages=True)) @@ -363,16 +371,16 @@ map.merge(readline_aliases) map('<up>', '<C-P>', wdg.history_move(-1)) map('<down>', '<C-N>', wdg.history_move(1)) -map('<home>', wdg.move(right=0, absolute=True)) -map('<end>', wdg.move(right=-1, absolute=True)) +map('<home>', '<C-A>', wdg.move(right=0, absolute=True)) +map('<end>', '<C-E>', wdg.move(right=-1, absolute=True)) map('<tab>', wdg.tab()) map('<s-tab>', wdg.tab(-1)) map('<C-C>', '<C-D>', '<ESC>', wdg.close()) map('<CR>', '<c-j>', wdg.execute()) map('<F1>', lambda arg: arg.fm.display_command_help(arg.wdg)) -map('<backspace>', wdg.delete(-1)) -map('<delete>', wdg.delete(0)) +map('<backspace>', '<C-H>', wdg.delete(-1)) +map('<delete>', '<C-D>', wdg.delete(0)) map('<C-W>', wdg.delete_word()) map('<C-K>', wdg.delete_rest(1)) map('<C-U>', wdg.delete_rest(-1)) diff --git a/ranger/defaults/options.py b/ranger/defaults/options.py index d93d7685..126d4bad 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 * @@ -36,7 +36,7 @@ from ranger.api.options import * # Which files should be hidden? Toggle this by typing `zh' or # changing the setting `show_hidden' hidden_filter = regexp( - r'lost\+found|^\.|~$|\.(:?pyc|pyo|bak|swp)$') + r'^\.|\.(?:pyc|pyo|bak|swp)$|~$|lost\+found') show_hidden = False # Show dotfiles in the bookmark preview box? @@ -85,7 +85,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/direction.py b/ranger/ext/direction.py index b9fbcac9..f36e22a6 100644 --- a/ranger/ext/direction.py +++ b/ranger/ext/direction.py @@ -134,7 +134,7 @@ class Direction(dict): pos += current return int(max(min(pos, maximum + offset - 1), minimum)) - def select(self, lst, override, current, pagesize, offset=1): + def select(self, lst, current, pagesize, override=None, offset=1): dest = self.move(direction=self.down(), override=override, current=current, pagesize=pagesize, minimum=0, maximum=len(lst)) selection = lst[min(current, dest):max(current, dest) + offset] 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/lazy_property.py b/ranger/ext/lazy_property.py new file mode 100644 index 00000000..320a1993 --- /dev/null +++ b/ranger/ext/lazy_property.py @@ -0,0 +1,25 @@ +# From http://blog.pythonisito.com/2008/08/lazy-descriptors.html + +class lazy_property(object): + """ + A @property-like decorator with lazy evaluation + + Example: + class Foo: + @lazy_property + def bar(self): + result = [...] + return result + """ + + def __init__(self, method): + self._method = method + self.__name__ = method.__name__ + self.__doc__ = method.__doc__ + + def __get__(self, obj, cls=None): + if obj is None: # to fix issues with pydoc + return None + result = self._method(obj) + obj.__dict__[self.__name__] = result + return result 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/ext/shell_escape.py b/ranger/ext/shell_escape.py index f8393f2a..ec9e4b12 100644 --- a/ranger/ext/shell_escape.py +++ b/ranger/ext/shell_escape.py @@ -14,7 +14,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. """ -A function to escape metacharacters of arguments for shell commands. +Functions to escape metacharacters of arguments for shell commands. """ META_CHARS = (' ', "'", '"', '`', '&', '|', ';', diff --git a/ranger/ext/utfwidth.py b/ranger/ext/utfwidth.py new file mode 100644 index 00000000..a506c676 --- /dev/null +++ b/ranger/ext/utfwidth.py @@ -0,0 +1,109 @@ +# -*- encoding: utf8 -*- +# Copyright (C) 2009, 2010 Roman Zimbelmann <romanz@lavabit.com> +# Copyright (C) 2004, 2005 Timo Hirvonen +# +# 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/>. +# +# ---- +# This file contains portions of code from cmus (uchar.c). + +NARROW = 1 +WIDE = 2 + +def uwid(string): + """Return the width of a string""" + end = len(string) + i = 0 + width = 0 + while i < end: + bytelen = utf_byte_length(string[i:]) + width += utf_char_width(string[i:i+bytelen]) + i += bytelen + return width + +def uchars(string): + """Return a list with one string for each character""" + end = len(string) + i = 0 + result = [] + while i < end: + bytelen = utf_byte_length(string[i:]) + result.append(string[i:i+bytelen]) + i += bytelen + return result + +def utf_byte_length(string): + """Return the byte length of one utf character""" + firstord = ord(string[0]) + if firstord < 0b01111111: + return 1 + if firstord < 0b10111111: + return 1 # invalid + if firstord < 0b11011111: + return 2 + if firstord < 0b11101111: + return 3 + if firstord < 0b11110100: + return 4 + return 1 # invalid + +def utf_char_width(string): + """Return the width of a single character""" + u = _utf_char_to_int(string) + if u < 0x1100: + return NARROW + # Hangul Jamo init. constonants + if u <= 0x115F: + return WIDE + # Angle Brackets + if u == 0x2329 or u == 0x232A: + return WIDE + if u < 0x2e80: + return NARROW + # CJK ... Yi + if u < 0x302A: + return WIDE + if u <= 0x302F: + return NARROW + if u == 0x303F or u == 0x3099 or u == 0x309a: + return NARROW + # CJK ... Yi + if u <= 0xA4CF: + return WIDE + # Hangul Syllables + if u >= 0xAC00 and u <= 0xD7A3: + return WIDE + # CJK Compatibility Ideographs + if u >= 0xF900 and u <= 0xFAFF: + return WIDE + # CJK Compatibility Forms + if u >= 0xFE30 and u <= 0xFE6F: + return WIDE + # Fullwidth Forms + if u >= 0xFF00 and u <= 0xFF60 or u >= 0xFFE0 and u <= 0xFFE6: + return WIDE + # CJK Extra Stuff + if u >= 0x20000 and u <= 0x2FFFD: + return WIDE + # ? + if u >= 0x30000 and u <= 0x3FFFD: + return WIDE + return NARROW # invalid (?) + +def _utf_char_to_int(string): + # Squash the last 6 bits of each byte together to an integer + u = 0 + for c in string: + u = (u << 6) | (ord(c) & 0b00111111) + return u diff --git a/ranger/fsobject/directory.py b/ranger/fsobject/directory.py index bab650c6..2ac56120 100644 --- a/ranger/fsobject/directory.py +++ b/ranger/fsobject/directory.py @@ -16,6 +16,7 @@ import os.path import stat from stat import S_ISLNK, S_ISDIR +from os import stat as os_stat, lstat as os_lstat from os.path import join, isdir, basename from collections import deque from time import time @@ -63,7 +64,6 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware): filter = None marked_items = None scroll_begin = 0 - scroll_offset = 0 mount_path = '/' disk_usage = 0 @@ -98,6 +98,7 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware): for opt in ('hidden_filter', 'show_hidden'): self.settings.signal_bind('setopt.' + opt, self.request_reload, weak=True) + self.use() def request_resort(self): self.order_outdated = True @@ -165,49 +166,53 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware): self.load_if_outdated() try: - if self.exists and self.runnable: + if self.runnable: yield - self.mount_path = mount_path(self.path) + mypath = self.path + + self.mount_path = mount_path(mypath) hidden_filter = not self.settings.show_hidden \ and self.settings.hidden_filter - filenames = [join(self.path, fname) \ - for fname in os.listdir(self.path) \ + filenames = [mypath + (mypath == '/' and fname or '/' + fname)\ + for fname in os.listdir(mypath) \ if accept_file(fname, hidden_filter, self.filter)] yield - self.load_content_mtime = os.stat(self.path).st_mtime + self.load_content_mtime = os.stat(mypath).st_mtime marked_paths = [obj.path for obj in self.marked_items] files = [] + disk_usage = 0 for name in filenames: try: - file_lstat = os.lstat(name) - if S_ISLNK(file_lstat.st_mode): - file_stat = os.stat(name) + file_lstat = os_lstat(name) + if file_lstat.st_mode & 0o170000 == 0o120000: + file_stat = os_stat(name) else: file_stat = file_lstat stats = (file_stat, file_lstat) - is_a_dir = S_ISDIR(file_stat.st_mode) + is_a_dir = file_stat.st_mode & 0o170000 == 0o040000 except: stats = None is_a_dir = False if is_a_dir: try: item = self.fm.env.get_directory(name) + item.load_if_outdated() except: item = Directory(name, preload=stats, path_is_abs=True) + item.load() else: item = File(name, preload=stats, path_is_abs=True) - item.load_if_outdated() + item.load() + disk_usage += item.size files.append(item) yield + self.disk_usage = disk_usage - self.disk_usage = sum(f.size for f in files if f.is_file) - - self.scroll_offset = 0 self.filenames = filenames self.files = files @@ -221,7 +226,7 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware): self.sort() - if len(self.files) > 0: + if files: if self.pointed_obj is not None: self.sync_index() else: @@ -401,6 +406,25 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware): return True return False + def get_description(self): + return "Loading " + str(self) + + def use(self): + """mark the filesystem-object as used at the current time""" + self.last_used = time() + + def is_older_than(self, seconds): + """returns whether this object wasn't use()d in the last n seconds""" + if seconds < 0: + return True + return self.last_used + seconds < time() + + def go(self, history=True): + """enter the directory if the filemanager is running""" + if self.fm: + return self.fm.enter_dir(self.path, history=history) + return False + def empty(self): """Is the directory empty?""" return self.files is None or len(self.files) == 0 diff --git a/ranger/fsobject/file.py b/ranger/fsobject/file.py index 4618df33..5e79c2d1 100644 --- a/ranger/fsobject/file.py +++ b/ranger/fsobject/file.py @@ -13,11 +13,45 @@ # 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 re +import zipfile +from ranger.fsobject import FileSystemObject + N_FIRST_BYTES = 20 control_characters = set(chr(n) for n in set(range(0, 9)) | set(range(14, 32))) -from ranger.fsobject import FileSystemObject +# Don't even try to preview files which mach this regular expression: +PREVIEW_BLACKLIST = re.compile(r""" + # look at the extension: + \.( + # one character extensions: + [oa] + # media formats: + | avi | [mj]pe?g | mp\d | og[gmv] | wm[av] | mkv | flv + | png | bmp | vob | wav | mpc | flac | divx? | xcf | pdf + # binary files: + | torrent | class | so | img | py[co] | dmg + # containers: + | iso | rar | 7z | tar | gz | bz2 | tgz + ) + # ignore filetype-independent suffixes: + (\.part|\.bak|~)? + # ignore fully numerical file extensions: + (\.\d+)*? + $ +""", re.VERBOSE | re.IGNORECASE) + +# Preview these files (almost) always: +PREVIEW_WHITELIST = re.compile(r""" + \.( + txt | py | c + ) + # ignore filetype-independent suffixes: + (\.part|\.bak|~)? + $ +""", re.VERBOSE | re.IGNORECASE) + class File(FileSystemObject): is_file = True @@ -38,3 +72,25 @@ class File(FileSystemObject): if self.firstbytes and control_characters & set(self.firstbytes): return True return False + + def has_preview(self): + if not self.fm.settings.preview_files: + return False + if self.is_socket or self.is_fifo or self.is_device: + return False + if not self.accessible: + return False + if PREVIEW_WHITELIST.search(self.basename): + return True + if PREVIEW_BLACKLIST.search(self.basename): + return False + if self.path == '/dev/core' or self.path == '/proc/kcore': + return False + if self.extension not in ('zip',) and self.is_binary(): + return False + return True + + def get_preview_source(self): + if self.extension == 'zip': + return '\n'.join(zipfile.ZipFile(self.path).namelist()) + return open(self.path, 'r') diff --git a/ranger/fsobject/fsobject.py b/ranger/fsobject/fsobject.py index afef48e4..4ca5a6c8 100644 --- a/ranger/fsobject/fsobject.py +++ b/ranger/fsobject/fsobject.py @@ -19,25 +19,23 @@ CONTAINER_EXTENSIONS = ('7z', 'ace', 'ar', 'arc', 'bz', 'bz2', 'cab', 'cpio', from os import access, listdir, lstat, readlink, stat from time import time -from os.path import abspath, basename, dirname, realpath +from os.path import abspath, basename, dirname, realpath, splitext, extsep from . import BAD_INFO from ranger.shared import MimeTypeAware, FileManagerAware from ranger.ext.shell_escape import shell_escape from ranger.ext.spawn import spawn +from ranger.ext.lazy_property import lazy_property from ranger.ext.human_readable import human_readable class FileSystemObject(MimeTypeAware, FileManagerAware): - (_filetype, - _shell_escaped_basename, - basename, + (basename, basename_lower, dirname, extension, infostring, - last_used, path, permissions, - stat) = (None,) * 11 + stat) = (None,) * 8 (content_loaded, force_load, @@ -50,7 +48,7 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): is_socket, accessible, - exists, + exists, # "exists" currently means "link_target_exists" loaded, marked, runnable, @@ -76,8 +74,8 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): self.path = path self.basename = basename(path) self.basename_lower = self.basename.lower() + self.extension = splitext(self.basename)[1].lstrip(extsep) or None self.dirname = dirname(path) - self.realpath = self.path self.preload = preload try: @@ -86,48 +84,33 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): except ValueError: self.extension = None - self.use() - def __repr__(self): return "<{0} {1}>".format(self.__class__.__name__, self.path) - @property + @lazy_property def shell_escaped_basename(self): - if self._shell_escaped_basename is None: - self._shell_escaped_basename = shell_escape(self.basename) - return self._shell_escaped_basename + return shell_escape(self.basename) - @property + @lazy_property def filetype(self): - if self._filetype is None: - try: - got = spawn(["file", '-Lb', '--mime-type', self.path]) - except OSError: - self._filetype = '' - else: - self._filetype = got - return self._filetype - - def get_description(self): - return "Loading " + str(self) + try: + return spawn(["file", '-Lb', '--mime-type', self.path]) + except OSError: + return "" def __str__(self): """returns a string containing the absolute path""" return str(self.path) def use(self): - """mark the filesystem-object as used at the current time""" - self.last_used = time() - - def is_older_than(self, seconds): - """returns whether this object wasn't use()d in the last n seconds""" - if seconds < 0: - return True - return self.last_used + seconds < time() + """Used in garbage-collecting. Override in Directory""" def set_mimetype(self): """assign attributes such as self.video according to the mimetype""" - self._mimetype = self.mimetypes.guess_type(self.basename, False)[0] + basename = self.basename + if self.extension == 'part': + basename = basename[0:-5] + self._mimetype = self.mimetypes.guess_type(basename, False)[0] if self._mimetype is None: self._mimetype = '' @@ -168,6 +151,15 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): """Called by directory.mark_item() and similar functions""" self.marked = bool(boolean) + @lazy_property + def realpath(self): + if self.is_link: + try: + return realpath(self.path) + except: + return None # it is impossible to get the link destination + return self.path + def load(self): """ reads useful information about the filesystem-object from the @@ -179,46 +171,43 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): # Get the stat object, either from preload or from [l]stat new_stat = None path = self.path + is_link = False if self.preload: new_stat = self.preload[1] - is_link = (new_stat.st_mode & 0o120000) == 0o120000 + is_link = new_stat.st_mode & 0o170000 == 0o120000 if is_link: new_stat = self.preload[0] self.preload = None + self.exists = True if new_stat else False else: try: new_stat = lstat(path) - is_link = (new_stat.st_mode & 0o120000) == 0o120000 + is_link = new_stat.st_mode & 0o170000 == 0o120000 if is_link: new_stat = stat(path) + self.exists = True except: - pass + self.exists = False # Set some attributes - if new_stat: - mode = new_stat.st_mode - self.accessible = True - self.is_device = (mode & 0o060000) == 0o060000 - self.is_fifo = (mode & 0o010000) == 0o010000 - self.is_link = is_link - self.is_socket = (mode & 0o140000) == 0o140000 - if access(path, 0): - self.exists = True - if self.is_directory: - self.runnable = (mode & 0o0100) == 0o0100 - else: - self.exists = False - self.runnable = False - if is_link: - try: - self.realpath = realpath(path) - except OSError: - pass # it is impossible to get the link destination - else: - self.accessible = False - # Determine infostring - if self.is_file: + self.accessible = True if new_stat else False + mode = new_stat.st_mode if new_stat else 0 + + format = mode & 0o170000 + if format == 0o020000 or format == 0o060000: # stat.S_IFCHR/BLK + self.is_device = True + self.size = 0 + self.infostring = 'dev' + elif format == 0o010000: # stat.S_IFIFO + self.is_fifo = True + self.size = 0 + self.infostring = 'fifo' + elif format == 0o140000: # stat.S_IFSOCK + self.is_socket = True + self.size = 0 + self.infostring = 'sock' + elif self.is_file: if new_stat: self.size = new_stat.st_size self.infostring = ' ' + human_readable(self.size) @@ -236,17 +225,9 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): self.infostring = ' %d' % self.size self.accessible = True self.runnable = True - elif self.is_device: - self.size = 0 - self.infostring = 'dev' - elif self.is_fifo: - self.size = 0 - self.infostring = 'fifo' - elif self.is_socket: - self.size = 0 - self.infostring = 'sock' - if self.is_link: + if is_link: self.infostring = '->' + self.infostring + self.is_link = True self.stat = new_stat @@ -274,12 +255,6 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): self.permissions = ''.join(perms) return self.permissions - def go(self): - """enter the directory if the filemanager is running""" - if self.fm: - return self.fm.enter_dir(self.path) - return False - def load_if_outdated(self): """ Calls load() if the currently cached information is outdated @@ -292,11 +267,7 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): real_mtime = lstat(self.path).st_mtime except OSError: real_mtime = None - if self.stat: - cached_mtime = self.stat.st_mtime - else: - cached_mtime = 0 - if real_mtime != cached_mtime: + if not self.stat or self.stat.st_mtime != real_mtime: self.load() return True return False diff --git a/ranger/gui/bar.py b/ranger/gui/bar.py index f5e34eb1..03ed2f78 100644 --- a/ranger/gui/bar.py +++ b/ranger/gui/bar.py @@ -13,6 +13,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +from ranger.ext.utfwidth import uwid + class Bar(object): left = None right = None @@ -132,7 +134,7 @@ class ColoredString(object): self.string = self.string[:n] def __len__(self): - return len(self.string) + return uwid(self.string) def __str__(self): return self.string 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/curses_shortcuts.py b/ranger/gui/curses_shortcuts.py index 82d8c787..3df45700 100644 --- a/ranger/gui/curses_shortcuts.py +++ b/ranger/gui/curses_shortcuts.py @@ -18,6 +18,22 @@ import _curses from ranger.ext.iter_tools import flatten from ranger.shared import SettingsAware +def ascii_only(string): + # Some python versions have problems with invalid unicode strings. + # I think this exception is rare enough that this naive hack is enough. + # It simply removes all non-ascii chars from a string. + def validate_char(char): + try: + if ord(char) > 127: + return '?' + except: + return '?' + return char + if isinstance(string, str): + return ''.join(validate_char(c) for c in string) + return string + + class CursesShortcuts(SettingsAware): """ This class defines shortcuts to faciliate operations with curses. @@ -33,12 +49,33 @@ class CursesShortcuts(SettingsAware): self.win.addstr(*args) except (_curses.error, TypeError): pass + except UnicodeEncodeError: + try: + self.win.addstr(*(ascii_only(obj) for obj in args)) + except (_curses.error, TypeError): + pass def addnstr(self, *args): try: self.win.addnstr(*args) except (_curses.error, TypeError): pass + except UnicodeEncodeError: + try: + self.win.addnstr(*(ascii_only(obj) for obj in args)) + except (_curses.error, TypeError): + pass + + def addch(self, *args): + try: + self.win.addch(*args) + except (_curses.error, TypeError): + pass + except UnicodeEncodeError: + try: + self.win.addch(*(ascii_only(obj) for obj in args)) + except (_curses.error, TypeError): + pass def color(self, *keys): """Change the colors from now on.""" 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/ui.py b/ranger/gui/ui.py index e9c20395..b0c1a352 100644 --- a/ranger/gui/ui.py +++ b/ranger/gui/ui.py @@ -19,6 +19,7 @@ import curses import _curses from .displayable import DisplayableContainer +from ranger.gui.curses_shortcuts import ascii_only from ranger.container.keymap import CommandArgs from .mouse_event import MouseEvent @@ -239,7 +240,10 @@ class UI(DisplayableContainer): split = cwd.rsplit(os.sep, self.settings.shorten_title) if os.sep in split[0]: cwd = os.sep.join(split[1:]) - sys.stdout.write("\033]2;ranger:" + cwd + "\007") + try: + sys.stdout.write("\033]2;ranger:" + cwd + "\007") + except UnicodeEncodeError: + sys.stdout.write("\033]2;ranger:" + ascii_only(cwd) + "\007") self.win.refresh() def finalize(self): diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index fbacbccd..d617e64e 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -14,7 +14,6 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. """The BrowserColumn widget displays the contents of a directory or file.""" -import re import stat from time import time @@ -22,36 +21,6 @@ from . import Widget from .pager import Pager from ranger.fsobject import BAD_INFO -# Don't even try to preview files which mach this regular expression: -PREVIEW_BLACKLIST = re.compile(r""" - # look at the extension: - \.( - # one character extensions: - [oa] - # media formats: - | avi | [mj]pe?g | mp\d | og[gmv] | wm[av] | mkv | flv - | png | bmp | vob | wav | mpc | flac | divx? | xcf | pdf - # binary files: - | torrent | class | so | img | py[co] | dmg - # containers: - | iso | rar | zip | 7z | tar | gz | bz2 | tgz - ) - # ignore filetype-independent suffixes: - (\.part|\.bak|~)? - # ignore fully numerical file extensions: - (\.\d+)*? - $ -""", re.VERBOSE | re.IGNORECASE) - -PREVIEW_WHITELIST = re.compile(r""" - \.( - txt | py | c - ) - # ignore filetype-independent suffixes: - (\.part|\.bak|~)? - $ -""", re.VERBOSE | re.IGNORECASE) - class BrowserColumn(Pager): main_column = False display_infostring = False @@ -127,7 +96,7 @@ class BrowserColumn(Pager): return False if self.target.is_file: - if not self._preview_this_file(self.target): + if not self.target.has_preview(): return False if self.target.is_directory: @@ -173,43 +142,28 @@ class BrowserColumn(Pager): self.need_redraw = False self.last_redraw_time = time() - def _preview_this_file(self, target): - if not (target \ - and self.settings.preview_files \ - and target.is_file \ - and target.accessible \ - and target.stat \ - and not target.is_device \ - and not target.stat.st_mode & stat.S_IFIFO): - return False - - if PREVIEW_WHITELIST.search(target.basename): - return True - if PREVIEW_BLACKLIST.search(target.basename): - return False - if target.is_binary(): - return False - return True - def _draw_file(self): """Draw a preview of the file, if the settings allow it""" self.win.move(0, 0) if not self.target.accessible: - self.win.addnstr("not accessible", self.wid) + self.addnstr("not accessible", self.wid) Pager.close(self) return - if not self._preview_this_file(self.target): + if self.target is None or not self.target.has_preview(): Pager.close(self) return try: - f = open(self.target.path, 'r') + f = self.target.get_preview_source() except: Pager.close(self) else: - self.set_source(f) - Pager.draw(self) + if f is None: + Pager.close(self) + else: + self.set_source(f) + Pager.draw(self) def _draw_directory(self): """Draw the contents of a directory""" @@ -223,7 +177,7 @@ class BrowserColumn(Pager): if not self.target.content_loaded: self.color(base_color) - self.win.addnstr("...", self.wid) + self.addnstr("...", self.wid) self.color_reset() return @@ -232,13 +186,13 @@ class BrowserColumn(Pager): if not self.target.accessible: self.color(base_color, 'error') - self.win.addnstr("not accessible", self.wid) + self.addnstr("not accessible", self.wid) self.color_reset() return if self.target.empty(): self.color(base_color, 'empty') - self.win.addnstr("empty", self.wid) + self.addnstr("empty", self.wid) self.color_reset() return @@ -295,27 +249,22 @@ class BrowserColumn(Pager): this_color.append(drawn.exists and 'good' or 'bad') string = drawn.basename - try: - if self.main_column: - if tagged: - self.win.addnstr(line, 0, text, self.wid - 2) - elif self.wid > 1: - self.win.addnstr(line, 1, text, self.wid - 2) - else: - self.win.addnstr(line, 0, text, self.wid) - - if self.display_infostring and drawn.infostring \ - and self.settings.display_size_in_main_column: - info = drawn.infostring - x = self.wid - 1 - len(info) - if info is BAD_INFO: - bad_info_color = (x, len(str(info))) - if x > 0: - self.win.addstr(line, x, str(info) + ' ') - except: - # the drawing of the last string will cause an error - # because ncurses tries to move out of the bounds - pass + if self.main_column: + if tagged: + self.addnstr(line, 0, text, self.wid - 2) + elif self.wid > 1: + self.addnstr(line, 1, text, self.wid - 2) + else: + self.addnstr(line, 0, text, self.wid) + + if self.display_infostring and drawn.infostring \ + and self.settings.display_size_in_main_column: + info = drawn.infostring + x = self.wid - 1 - len(info) + if info is BAD_INFO: + bad_info_color = (x, len(str(info))) + if x > 0: + self.addstr(line, x, str(info) + ' ') self.color_at(line, 0, self.wid, this_color) if bad_info_color: diff --git a/ranger/gui/widgets/browserview.py b/ranger/gui/widgets/browserview.py index a90231f2..c80e4885 100644 --- a/ranger/gui/widgets/browserview.py +++ b/ranger/gui/widgets/browserview.py @@ -144,7 +144,7 @@ class BrowserView(Widget, DisplayableContainer): if maxlen < self.wid: self.win.vline(0, maxlen, curses.ACS_VLINE, line+1) - self.win.addch(line+1, maxlen, curses.ACS_LRCORNER) + self.addch(line+1, maxlen, curses.ACS_LRCORNER) def _draw_borders(self): win = self.win @@ -188,13 +188,10 @@ class BrowserView(Widget, DisplayableContainer): # in case it's off the boundaries pass - win.addch(0, left_start, curses.ACS_ULCORNER) - win.addch(self.hei - 1, left_start, curses.ACS_LLCORNER) - win.addch(0, right_end, curses.ACS_URCORNER) - try: - win.addch(self.hei - 1, right_end, curses.ACS_LRCORNER) - except: - pass + self.addch(0, left_start, curses.ACS_ULCORNER) + self.addch(self.hei - 1, left_start, curses.ACS_LLCORNER) + self.addch(0, right_end, curses.ACS_URCORNER) + self.addch(self.hei - 1, right_end, curses.ACS_LRCORNER) def _collapse(self): # Should the last column be cut off? (Because there is no preview) diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py index fa9e438e..57264292 100644 --- a/ranger/gui/widgets/console.py +++ b/ranger/gui/widgets/console.py @@ -18,124 +18,101 @@ 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.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 + self.history = History(self.settings.max_console_history_size) + # load history from files if not ranger.arg.clean: - 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.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) + len(self.line) + 1 + overflow = -self.wid + len(self.prompt) + uwid(self.line) + 1 if overflow > 0: + #XXX: cut uft-char-wise, consider width self.addstr(self.line[overflow:]) else: self.addstr(self.line) def finalize(self): try: - self.fm.ui.win.move(self.y, - self.x + min(self.wid-1, self.pos + len(self.prompt))) + xpos = uwid(self.line[0:self.pos]) + len(self.prompt) + self.fm.ui.win.move(self.y, self.x + min(self.wid-1, xpos)) 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 @@ -208,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() @@ -216,16 +196,20 @@ 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) if direction.horizontal(): - self.pos = direction.move( + # Ensure that the pointer is moved utf-char-wise + uc = uchars(self.line) + upos = len(uchars(self.line[:self.pos])) + newupos = direction.move( direction=direction.right(), minimum=0, - maximum=len(self.line) + 1, - current=self.pos) + maximum=len(uc) + 1, + current=upos) + self.pos = len(''.join(uc[:newupos])) def delete_rest(self, direction): self.tab_deque = None @@ -247,76 +231,30 @@ 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 - if mod == -1 and len(self.line) == 0: - self.close() - pos = self.pos + mod - - self.line = self.line[0:pos] + self.line[pos+1:] - self.move(right=mod) + if mod == -1 and self.pos == 0: + if not self.line: + self.close() + return + # Delete utf-char-wise + uc = uchars(self.line) + upos = len(uchars(self.line[:self.pos])) + mod + left_part = ''.join(uc[:upos]) + self.pos = len(left_part) + 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 @@ -340,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]) @@ -355,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/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py index 3019930b..2a92f313 100644 --- a/ranger/gui/widgets/statusbar.py +++ b/ranger/gui/widgets/statusbar.py @@ -224,19 +224,23 @@ class StatusBar(Widget): right.add(human_readable(sum(f.size \ for f in target.marked_items \ if f.is_file), seperator='')) - right.add(" / " + str(len(target.marked_items))) + right.add("/" + str(len(target.marked_items))) else: - right.add(human_readable(target.disk_usage, seperator='')) - right.add(", ", "space") + right.add(human_readable(target.disk_usage, seperator='') + + " sum, ") right.add(human_readable(self.env.get_free_space( \ - target.mount_path), seperator='')) + target.mount_path), seperator='') + " free") right.add(" ", "space") if target.marked_items: # Indicate that there are marked files. Useful if you scroll # away and don't see them anymore. right.add('Mrk', base, 'marked') - elif max_pos > 0: + elif len(target.files): + right.add(str(target.pointer + 1) + '/' + + str(len(target.files)) + ' ', base) + if max_pos == 0: + right.add('All', base, 'all') if pos == 0: right.add('Top', base, 'top') elif pos >= max_pos: @@ -245,7 +249,7 @@ class StatusBar(Widget): right.add('{0:0>.0f}%'.format(100.0 * pos / max_pos), base, 'percentage') else: - right.add('All', base, 'all') + right.add('0/0 All', base, 'all') def _print_result(self, result): self.win.move(0, 0) diff --git a/ranger/gui/widgets/taskview.py b/ranger/gui/widgets/taskview.py index 475b903c..e988b08c 100644 --- a/ranger/gui/widgets/taskview.py +++ b/ranger/gui/widgets/taskview.py @@ -86,7 +86,8 @@ class TaskView(Widget, Accumulator): if i is None: i = self.pointer - self.fm.loader.remove(index=i) + if self.fm.loader.queue: + self.fm.loader.remove(index=i) def task_move(self, to, i=None): if i is None: diff --git a/ranger/gui/widgets/titlebar.py b/ranger/gui/widgets/titlebar.py index 17da7748..35e2e3d9 100644 --- a/ranger/gui/widgets/titlebar.py +++ b/ranger/gui/widgets/titlebar.py @@ -52,7 +52,7 @@ class TitleBar(Widget): self._print_result(self.result) if self.wid > 2: self.color('in_titlebar', 'throbber') - self.win.addnstr(self.y, self.wid - 2 - self.tab_width, + self.addnstr(self.y, self.wid - 2 - self.tab_width, self.throbber, 1) def click(self, event): 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/index.py b/ranger/help/index.py index 316e975f..1245da64 100644 --- a/ranger/help/index.py +++ b/ranger/help/index.py @@ -18,8 +18,8 @@ k Move around: Use the cursor keys, or "h" to go left, h l "j" to go down, "k" to go up, "l" to go right. j - Close Ranger: Use ":q<Enter>" or "Q". - Specific help: Type the help key "?" prepended with a number: + Close Ranger: Type "Q" + Specific help: Type "?", prepended with a number: |0?| This index |1?| Basic movement and browsing diff --git a/ranger/help/invocation.py b/ranger/help/invocation.py index 3de574cc..26cffd4a 100644 --- a/ranger/help/invocation.py +++ b/ranger/help/invocation.py @@ -43,9 +43,10 @@ command line. This is useful when your configuration is broken, when you want to avoid clutter, etc. ---fail-if-run +--fail-unless-cd Return the exit code 1 if ranger is used to run a file, for example - with `ranger --fail-if-run filename`. This can be useful for scripts. + with `ranger --fail-unless-cd filename`. This can be useful for scripts. + (This option used to be called --fail-if-run) -r <dir>, --confdir=<dir> Define a different configuration directory. The default is @@ -69,7 +70,7 @@ command line. Examples: ranger episode1.avi ranger --debug /usr/bin - ranger --confdir=~/.config/ranger --fail-if-run + ranger --confdir=~/.config/ranger --fail-unless-cd ============================================================================== @@ -95,7 +96,7 @@ docstrings. Use this option if you don't need the documentation. Examples: PYTHONOPTIMIZE=1 ranger episode1.avi PYTHONOPTIMIZE=2 ranger --debug /usr/bin - python -OO `which ranger` --confdir=~/.config/ranger --fail-if-run + python -OO `which ranger` --confdir=~/.config/ranger --fail-unless-cd Note: The author expected "-OO" to reduce the memory usage, but that doesn't seem to happen. diff --git a/ranger/help/movement.py b/ranger/help/movement.py index 3287e9bb..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 @@ -96,7 +99,10 @@ of the file you're pointing at. <Space> mark a file v toggle all marks - V remove all marks + V, uv remove all marks + ^V mark files in a specific direction + e.g. ^Vgg marks all files from the current to the top + u^V unmark files in a specific direction By "tagging" files, you can highlight them and mark them to be special in whatever context you want. Tags are persistent across sessions. diff --git a/ranger/help/starting.py b/ranger/help/starting.py index 069d6a04..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 @@ -95,12 +102,13 @@ Flags give you a way to modify the behaviour of the spawned process. s Silent mode. Output will be discarded. d Detach the process. (Run in background) p Redirect output to the pager + w Wait for an enter-press when the process is done -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 f5d8614f..7604af12 100644 --- a/ranger/shared/settings.py +++ b/ranger/shared/settings.py @@ -20,35 +20,35 @@ 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, + '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, + 'save_console_history': bool, + 'scroll_offset': int, + '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, - 'update_title': bool, - 'shorten_title': int, # Note: False is an instance of int + 'sort_reverse': bool, + 'sort': str, 'tilde_in_titlebar': bool, - 'max_history_size': (int, type(None)), - 'max_console_history_size': (int, type(None)), - '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'), + 'update_title': bool, 'xterm_alt_key': bool, } @@ -66,7 +66,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]) |