diff options
author | hut <hut@lavabit.com> | 2010-04-18 18:49:59 +0200 |
---|---|---|
committer | hut <hut@lavabit.com> | 2010-04-18 18:49:59 +0200 |
commit | 2cba822ce4c9f163769ae5d0196a193d4ca892a8 (patch) | |
tree | cfffd4187b5be12e83b19c6c1e7f69b43a057d34 /ranger | |
parent | 39d74d84932d26168e5e42dd5446e11582d05b12 (diff) | |
parent | 738113725dbe2d7d5d54279c6ef38927113ec2f8 (diff) | |
download | ranger-2cba822ce4c9f163769ae5d0196a193d4ca892a8.tar.gz |
Merge branch 'devel'
Diffstat (limited to 'ranger')
30 files changed, 1667 insertions, 1119 deletions
diff --git a/ranger/__main__.py b/ranger/__main__.py index 6b5a21b3..b9847bf5 100644 --- a/ranger/__main__.py +++ b/ranger/__main__.py @@ -18,7 +18,7 @@ import os import sys - +import ranger def parse_arguments(): """Parse the program arguments""" @@ -46,18 +46,68 @@ def parse_arguments(): arg = OpenStruct(options.__dict__, targets=positional) arg.confdir = os.path.expanduser(arg.confdir) - if not arg.clean: + return arg + + +def load_settings(fm, clean): + import ranger.api.commands + if not clean: try: - os.makedirs(arg.confdir) + os.makedirs(ranger.arg.confdir) except OSError as err: if err.errno != 17: # 17 means it already exists print("This configuration directory could not be created:") - print(arg.confdir) - print("To run ranger without the need for configuration files") - print("use the --clean option.") + print(ranger.arg.confdir) + print("To run ranger without the need for configuration") + print("files, use the --clean option.") raise SystemExit() - sys.path[0:0] = [arg.confdir] - return arg + + sys.path[0:0] = [ranger.arg.confdir] + + # Load commands + comcont = ranger.api.commands.CommandContainer() + ranger.api.commands.alias = comcont.alias + try: + import commands + comcont.load_commands_from_module(commands) + except ImportError: + pass + from ranger.defaults import commands + comcont.load_commands_from_module(commands) + commands = comcont + + # Load apps + try: + import apps + except ImportError: + from ranger.defaults import apps + + # Load keys + from ranger import shared, api + from ranger.api import keys + keymanager = shared.EnvironmentAware.env.keymanager + keys.keymanager = keymanager + from ranger.defaults import keys + try: + import keys + except ImportError: + pass + # COMPAT WARNING + if hasattr(keys, 'initialize_commands'): + print("Warning: the syntax for ~/.ranger/keys.py has changed.") + print("Your custom keys are not loaded."\ + " Please update your configuration.") + del sys.path[0] + else: + comcont = ranger.api.commands.CommandContainer() + ranger.api.commands.alias = comcont.alias + from ranger.defaults import commands, keys, apps + comcont.load_commands_from_module(commands) + commands = comcont + fm.commands = commands + fm.keys = keys + fm.apps = apps.CustomApplications() + def main(): """initialize objects and run the filemanager""" @@ -71,7 +121,6 @@ def main(): from signal import signal, SIGINT from locale import getdefaultlocale, setlocale, LC_ALL - import ranger from ranger.ext import curses_interrupt_handler from ranger.core.fm import FM from ranger.core.environment import Environment @@ -97,7 +146,6 @@ def main(): SettingsAware._setup() - # Initialize objects if arg.targets: target = arg.targets[0] if not os.access(target, os.F_OK): @@ -105,28 +153,34 @@ def main(): sys.exit(1) elif os.path.isfile(target): thefile = File(target) - FM().execute_file(thefile, mode=arg.mode, flags=arg.flags) + fm = FM() + load_settings(fm, ranger.arg.clean) + fm.execute_file(thefile, mode=arg.mode, flags=arg.flags) sys.exit(0) else: path = target else: path = '.' - EnvironmentAware._assign(Environment(path)) - try: - my_ui = UI() - my_fm = FM(ui=my_ui) - FileManagerAware._assign(my_fm) + # Initialize objects + EnvironmentAware._assign(Environment(path)) + fm = FM() + load_settings(fm, ranger.arg.clean) + FileManagerAware._assign(fm) + fm.ui = UI() # Run the file manager - my_fm.initialize() - my_ui.initialize() - my_fm.loop() + fm.initialize() + fm.ui.initialize() + fm.loop() finally: # Finish, clean up - if 'my_ui' in vars(): - my_ui.destroy() + try: + fm.ui.destroy() + except (AttributeError, NameError): + pass + if __name__ == '__main__': top_dir = os.path.dirname(sys.path[0]) diff --git a/ranger/api/commands.py b/ranger/api/commands.py new file mode 100644 index 00000000..db6c1a3c --- /dev/null +++ b/ranger/api/commands.py @@ -0,0 +1,182 @@ +# Copyright (C) 2009, 2010 Roman Zimbelmann <romanz@lavabit.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +from collections import deque +from ranger.shared import FileManagerAware +from ranger.gui.widgets import console_mode as cmode +from ranger.ext.command_parser import LazyParser as parse + + +class CommandContainer(object): + def __init__(self): + self.aliases = {} + self.commands = {} + + def __getitem__(self, key): + return self.commands[key] + + def alias(self, new, old): + self.aliases[new] = old + + def load_commands_from_module(self, module): + for varname, var in vars(module).items(): + try: + if issubclass(var, Command) and var != Command: + self.commands[var.name or varname] = var + except TypeError: + pass + for new, old in self.aliases.items(): + try: + self.commands[new] = self.commands[old] + except: + pass + + def get_command(self, name, abbrev=True): + if abbrev: + lst = [cls for cmd, cls in self.commands.items() \ + if cmd.startswith(name) \ + and cls.allow_abbrev \ + or cmd == name] + if len(lst) == 0: + raise KeyError + if len(lst) == 1 or self.commands[name] in lst: + return lst[0] + raise ValueError("Ambiguous command") + else: + try: + return self.commands[name] + except KeyError: + return None + + def command_generator(self, start): + return (cmd + ' ' for cmd in self.commands if cmd.startswith(start)) + + +class Command(FileManagerAware): + """Abstract command class""" + name = None + allow_abbrev = True + def __init__(self, line, mode): + self.line = line + self.mode = mode + + def execute(self): + """Override this""" + + def tab(self): + """Override this""" + + def quick_open(self): + """Override this""" + + def _tab_only_directories(self): + from os.path import dirname, basename, expanduser, join, isdir + + line = parse(self.line) + cwd = self.fm.env.cwd.path + + try: + rel_dest = line.rest(1) + except IndexError: + rel_dest = '' + + # expand the tilde into the user directory + if rel_dest.startswith('~'): + rel_dest = expanduser(rel_dest) + + # define some shortcuts + abs_dest = join(cwd, rel_dest) + abs_dirname = dirname(abs_dest) + rel_basename = basename(rel_dest) + rel_dirname = dirname(rel_dest) + + try: + # are we at the end of a directory? + if rel_dest.endswith('/') or rel_dest == '': + _, dirnames, _ = os.walk(abs_dest).next() + + # are we in the middle of the filename? + else: + _, dirnames, _ = os.walk(abs_dirname).next() + dirnames = [dn for dn in dirnames \ + if dn.startswith(rel_basename)] + except (OSError, StopIteration): + # os.walk found nothing + pass + else: + dirnames.sort() + + # no results, return None + if len(dirnames) == 0: + return + + # one result. since it must be a directory, append a slash. + if len(dirnames) == 1: + return line.start(1) + join(rel_dirname, dirnames[0]) + '/' + + # more than one result. append no slash, so the user can + # manually type in the slash to advance into that directory + return (line.start(1) + join(rel_dirname, dirname) for dirname in dirnames) + + def _tab_directory_content(self): + from os.path import dirname, basename, expanduser, join, isdir + + line = parse(self.line) + cwd = self.fm.env.cwd.path + + try: + rel_dest = line.rest(1) + except IndexError: + rel_dest = '' + + # expand the tilde into the user directory + if rel_dest.startswith('~'): + rel_dest = expanduser(rel_dest) + + # define some shortcuts + abs_dest = join(cwd, rel_dest) + abs_dirname = dirname(abs_dest) + rel_basename = basename(rel_dest) + rel_dirname = dirname(rel_dest) + + try: + # are we at the end of a directory? + if rel_dest.endswith('/') or rel_dest == '': + _, dirnames, filenames = os.walk(abs_dest).next() + names = dirnames + filenames + + # are we in the middle of the filename? + else: + _, dirnames, filenames = os.walk(abs_dirname).next() + names = [name for name in (dirnames + filenames) \ + if name.startswith(rel_basename)] + except (OSError, StopIteration): + # os.walk found nothing + pass + else: + names.sort() + + # no results, return None + if len(names) == 0: + return + + # one result. since it must be a directory, append a slash. + if len(names) == 1: + return line.start(1) + join(rel_dirname, names[0]) + '/' + + # more than one result. append no slash, so the user can + # manually type in the slash to advance into that directory + return (line.start(1) + join(rel_dirname, name) for name in names) diff --git a/ranger/api/keys.py b/ranger/api/keys.py index 5b833c34..92a0269c 100644 --- a/ranger/api/keys.py +++ b/ranger/api/keys.py @@ -21,6 +21,7 @@ from inspect import getargspec, ismethod from ranger import RANGERDIR 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 class Wrapper(object): def __init__(self, firstattr): @@ -33,10 +34,11 @@ class Wrapper(object): def function(command_argument): args, kws = real_args, real_keywords number = command_argument.n + direction = command_argument.direction obj = getattr(command_argument, self.__firstattr__) fnc = getattr(obj, attr) - if number is not None: - args, kws = replace_narg(number, fnc, args, kws) + if number is not None or direction is not None: + args, kws = replace_narg(number, direction, fnc, args, kws) return fnc(*args, **kws) return function return wrapper @@ -54,14 +56,15 @@ class Wrapper(object): # If the method has an argument named "narg", pressing a number before # the key will pass that number as the narg argument. If you want the # same behaviour in a custom lambda function, you can write: -# bind('gg', fm.move_pointer(absolute=0)) +# bind('gg', fm.move(to=0)) # as: -# bind('gg', lambda arg: narg(arg.n, arg.fm.move_pointer, absolute=0)) +# bind('gg', lambda arg: narg(arg.n, arg.fm.move, to=0)) fm = Wrapper('fm') wdg = Wrapper('wdg') +DIRARG_KEYWORD = 'dirarg' NARG_KEYWORD = 'narg' def narg(number_, function_, *args_, **keywords_): @@ -77,7 +80,7 @@ def narg(number_, function_, *args_, **keywords_): args, keywords = replace_narg(number_, function_, args_, keywords_) return function_(*args, **keywords) -def replace_narg(number, function, args, keywords): +def replace_narg(number, direction, function, args, keywords): """ This function returns (args, keywords) with one little change: if <function> has a named argument called "narg", args and keywords @@ -92,10 +95,10 @@ def replace_narg(number, function, args, keywords): => (1, 666), {} """ argspec = getargspec(function).args - if NARG_KEYWORD in argspec: + args = list(args) + if number is not None and NARG_KEYWORD in argspec: try: # is narg in args? - args = list(args) index = argspec.index(NARG_KEYWORD) if ismethod(function): index -= 1 # because of 'self' @@ -104,4 +107,14 @@ def replace_narg(number, function, args, keywords): # is narg in keywords? keywords = dict(keywords) keywords[NARG_KEYWORD] = number + if direction is not None and DIRARG_KEYWORD in argspec: + try: + index = argspec.index(DIRARG_KEYWORD) + if ismethod(function): + index -= 1 # because of 'self' + args[index] = direction + except (ValueError, IndexError): + # is narg in keywords? + keywords = dict(keywords) + keywords[DIRARG_KEYWORD] = direction return args, keywords diff --git a/ranger/api/options.py b/ranger/api/options.py index 4748823d..61026a4a 100644 --- a/ranger/api/options.py +++ b/ranger/api/options.py @@ -17,15 +17,3 @@ import re from re import compile as regexp from ranger import colorschemes as allschemes from ranger.gui import color - -class AttrToString(object): - """ - Purely for compatibility to 1.0.3. - """ - def __getattr__(self, attr): - print("NOTE: your configuration is out of date.") - print("instead of this: colorscheme = colorschemes." + attr) - print("please use a string: colorscheme = \"" + attr + "\"") - return attr - -colorschemes = AttrToString() diff --git a/ranger/container/__init__.py b/ranger/container/__init__.py index 51122291..3351cc63 100644 --- a/ranger/container/__init__.py +++ b/ranger/container/__init__.py @@ -17,6 +17,6 @@ used to manage stored data """ from ranger.container.history import History -from ranger.container.keybuffer import KeyBuffer -from .commandlist import CommandList +from .keymap import KeyMap, KeyManager +from .keybuffer import KeyBuffer from .bookmarks import Bookmarks diff --git a/ranger/container/commandlist.py b/ranger/container/commandlist.py deleted file mode 100644 index eb289c46..00000000 --- a/ranger/container/commandlist.py +++ /dev/null @@ -1,225 +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/>. - -from ranger.ext.openstruct import OpenStruct - -class CommandArgument(object): - def __init__(self, fm, displayable, keybuffer): - self.fm = fm - self.wdg = displayable - self.keybuffer = keybuffer - self.n = keybuffer.number - self.keys = str(keybuffer) - -def cmdarg(displayable): - return CommandArgument(displayable.fm, \ - displayable, displayable.env.keybuffer) - -class CommandList(object): - """ - CommandLists are dictionary-like objects which give you a command - for a given key combination. CommandLists must be filled before use. - """ - - dummy_object = None - dummies_in_paths = False - paths = {} - commandlist = [] - - def __init__(self): - self.commandlist = [] - self.paths = {} - - def __getitem__(self, key): - """Returns the command with the given key combination""" - if isinstance(key, str): - key = self._str_to_tuple(key) - return self.paths[key] - - def rebuild_paths(self): - """ - Fill the path dictionary with dummie objects. - We need to know when to clear the keybuffer (when a wrong key is pressed) - and when to wait for the rest of the key combination. For "gg" we - will assign "g" to a dummy which tells the program to do the latter - and wait. - """ - if self.dummies_in_paths: - self.remove_dummies() - - for cmd in self.commandlist: - for key in cmd.keys: - for path in self._keypath(key): - if path not in self.paths: - self.paths[path] = self.dummy_object - - self.dummies_in_paths = True - - def _keypath(self, tup): - """split a tuple like (a,b,c,d) into [(a,), (a,b), (a,b,c)]""" - length = len(tup) - - if length == 0: - return () - if length == 1: - return (tup, ) - - current = [] - all = [] - - for i in range(len(tup) - 1): - current.append(tup[i]) - all.append(tuple(current)) - - return all - - def remove_dummies(self): - """ - Remove dummie objects in case you have to rebuild a path dictionary - which already contains dummie objects. - """ - for k in tuple(self.paths.keys()): - if self.paths[k] == self.dummy_object: del self.paths[k] - self.dummies_in_paths = False - - def __call__(self, *args, **keywords): - if keywords: - self.show(*args, **keywords) - else: - lastarg = args[-1] - if hasattr(lastarg, '__call__'): - # do the binding - self.bind(lastarg, *args[:-1]) - else: - # act as a decorator. eg: - # @bind('a') - # def do_stuff(arg): - # arg.fm.ui.do_stuff() - # - # is equivalent to: - # bind('a', lambda arg: arg.fm.ui.do_stuff()) - return lambda fnc: self.bind(fnc, *args) - - def _str_to_tuple(self, obj): - """splits a string into a tuple of integers""" - if isinstance(obj, tuple): - return obj - elif isinstance(obj, str): - return tuple(map(ord, obj)) - elif isinstance(obj, int): - return (obj, ) - else: - raise TypeError('need a str, int or tuple for str_to_tuple') - - def bind(self, fnc, *keys): - """create a Command object and assign it to the given key combinations.""" - if len(keys) == 0: return - - keys = tuple(map(self._str_to_tuple, keys)) - - cmd = Command(fnc, keys) - - self.commandlist.append(cmd) - for key in cmd.keys: - self.paths[key] = cmd - - def show(self, *keys, **keywords): - """create a Show object and assign it to the given key combinations.""" - if len(keys) == 0: return - - keys = tuple(map(self._str_to_tuple, keys)) - - obj = Show(keywords, keys) - - self.commandlist.append(obj) - for key in obj.keys: - self.paths[key] = obj - - def alias(self, existing, *new): - """bind the <new> keys to the command of the <existing> key""" - existing = self._str_to_tuple(existing) - new = tuple(map(self._str_to_tuple, new)) - - obj = AliasedCommand(_make_getter(self.paths, existing), new) - - self.commandlist.append(obj) - for key in new: - self.paths[key] = obj - - def unbind(self, *keys): - i = len(self.commandlist) - keys = set(map(self._str_to_tuple, keys)) - - while i > 0: - i -= 1 - cmd = self.commandlist[i] - cmd.keys -= keys - if not cmd.keys: - del self.commandlist[i] - - for k in keys: - del self.paths[k] - - def clear(self): - """remove all bindings""" - self.paths.clear() - del self.commandlist[:] - - -class Command(object): - """Command objects store information about a command""" - - keys = [] - - def __init__(self, fnc, keys): - self.keys = set(keys) - self.execute = fnc - - def execute(self, *args): - """Execute the command""" - - def execute_wrap(self, displayable): - self.execute(cmdarg(displayable)) - - -class AliasedCommand(Command): - def __init__(self, getter, keys): - self.getter = getter - self.keys = set(keys) - - def get_execute(self): - return self.getter() - - execute = property(get_execute) - - -class Show(object): - """Show objects do things without clearing the keybuffer""" - - keys = [] - text = '' - - def __init__(self, dictionary, keys): - self.keys = set(keys) - self.show_obj = OpenStruct(dictionary) - - -def _make_getter(paths, key): - def getter(): - try: - return paths[key].execute - except: - return lambda: None - return getter diff --git a/ranger/container/keybuffer.py b/ranger/container/keybuffer.py index 2992aea2..23b82a16 100644 --- a/ranger/container/keybuffer.py +++ b/ranger/container/keybuffer.py @@ -13,59 +13,168 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -def to_string(i): - try: - return chr(i) - except ValueError: - return '?' - +import curses.ascii from collections import deque -from curses.ascii import ascii +from string import digits +from ranger.ext.keybinding_parser import parse_keybinding, \ + DIRKEY, ANYKEY, PASSIVE_ACTION +from ranger.container.keymap import Binding, KeyMap # mainly for assertions -ZERO = ord('0') -NINE = ord('9') +MAX_ALIAS_RECURSION = 20 +digitlist = set(ord(n) for n in digits) class KeyBuffer(object): - def __init__(self): - self.number = None - self.queue = deque() - self.queue_with_numbers = deque() + """The evaluator and storage for pressed keys""" + def __init__(self, keymap, direction_keys): + self.assign(keymap, direction_keys) - def clear(self): - """Clear the keybuffer and restore the initial state""" - self.number = None - self.queue.clear() - self.queue_with_numbers.clear() - - def append(self, key): - """ - Append a key to the keybuffer, or initial numbers to - the number attribute. - """ - self.queue_with_numbers.append(key) - - if not self.queue and key >= ZERO and key <= NINE: - if self.number is None: - self.number = 0 - try: - self.number = self.number * 10 + int(chr(key)) - except ValueError: + def assign(self, keymap, direction_keys): + """Change the keymap and direction keys of the keybuffer""" + self.keymap = keymap + self.direction_keys = direction_keys + + def add(self, key): + """Add a key and evaluate it""" + assert isinstance(key, int) + assert key >= 0 + self.all_keys.append(key) + self.key_queue.append(key) + while self.key_queue: + key = self.key_queue.popleft() + + # evaluate quantifiers + if self.eval_quantifier and self._do_eval_quantifier(key): return + + # evaluate the command + if self.eval_command and self._do_eval_command(key): + return + + # evaluate (the first number of) the direction-quantifier + if self.eval_quantifier and self._do_eval_quantifier(key): + return + + # evaluate direction keys {j,k,gg,pagedown,...} + if not self.eval_command: + self._do_eval_direction(key) + + def _do_eval_direction(self, key): + try: + assert isinstance(self.dir_tree_pointer, dict) + self.dir_tree_pointer = self.dir_tree_pointer[key] + except KeyError: + self.failure = True else: - self.queue.append(key) + self._direction_try_to_finish() + + def _direction_try_to_finish(self): + if self.max_alias_recursion <= 0: + self.failure = True + return None + match = self.dir_tree_pointer + assert isinstance(match, (Binding, dict, KeyMap)) + if isinstance(match, KeyMap): + self.dir_tree_pointer = self.dir_tree_pointer._tree + match = self.dir_tree_pointer + if isinstance(self.dir_tree_pointer, Binding): + if match.alias: + self.key_queue.extend(parse_keybinding(match.alias)) + self.dir_tree_pointer = self.direction_keys._tree + self.max_alias_recursion -= 1 + else: + direction = match.actions['dir'].copy() + if self.direction_quant is not None: + direction.multiply(self.direction_quant) + self.directions.append(direction) + self.direction_quant = None + self.eval_command = True + self._try_to_finish() + + def _do_eval_quantifier(self, key): + if self.eval_command: + tree = self.tree_pointer + else: + tree = self.dir_tree_pointer + if key in digitlist and ANYKEY not in tree: + attr = self.eval_command and 'quant' or 'direction_quant' + if getattr(self, attr) is None: + setattr(self, attr, 0) + setattr(self, attr, getattr(self, attr) * 10 + key - 48) + else: + self.eval_quantifier = False + return None + return True + + def _do_eval_command(self, key): + assert isinstance(self.tree_pointer, dict), self.tree_pointer + try: + self.tree_pointer = self.tree_pointer[key] + except TypeError: + self.failure = True + return None + except KeyError: + try: + key in digitlist or self.direction_keys._tree[key] + self.tree_pointer = self.tree_pointer[DIRKEY] + except KeyError: + try: + self.tree_pointer = self.tree_pointer[ANYKEY] + except KeyError: + self.failure = True + return None + else: + self.matches.append(key) + assert isinstance(self.tree_pointer, (Binding, dict)) + self._try_to_finish() + else: + assert isinstance(self.tree_pointer, (Binding, dict)) + self.eval_command = False + self.eval_quantifier = True + self.dir_tree_pointer = self.direction_keys._tree + else: + if isinstance(self.tree_pointer, dict): + try: + self.command = self.tree_pointer[PASSIVE_ACTION] + except (KeyError, TypeError): + self.command = None + self._try_to_finish() + + def _try_to_finish(self): + if self.max_alias_recursion <= 0: + self.failure = True + return None + assert isinstance(self.tree_pointer, (Binding, dict, KeyMap)) + if isinstance(self.tree_pointer, KeyMap): + self.tree_pointer = self.tree_pointer._tree + if isinstance(self.tree_pointer, Binding): + if self.tree_pointer.alias: + keys = parse_keybinding(self.tree_pointer.alias) + self.key_queue.extend(keys) + self.tree_pointer = self.keymap._tree + self.max_alias_recursion -= 1 + else: + self.command = self.tree_pointer + self.done = True + + def clear(self): + """Reset the keybuffer. Do this once before the first usage.""" + self.max_alias_recursion = MAX_ALIAS_RECURSION + self.failure = False + self.done = False + self.quant = None + self.matches = [] + self.command = None + self.direction_quant = None + self.directions = [] + self.all_keys = [] + self.tree_pointer = self.keymap._tree + self.dir_tree_pointer = self.direction_keys._tree - def tuple_with_numbers(self): - """Get a tuple of ascii codes.""" - return tuple(self.queue_with_numbers) + self.key_queue = deque() - def tuple_without_numbers(self): - """ - Get a tuple of ascii codes. - If the keybuffer starts with numbers, those will - be left out. To access them, use keybuffer.number - """ - return tuple(self.queue) + self.eval_quantifier = True + self.eval_command = True def __str__(self): """returns a concatenation of all characters""" - return "".join( map( to_string, self.queue_with_numbers ) ) + return "".join("{0:c}".format(c) for c in self.all_keys) diff --git a/ranger/container/keymap.py b/ranger/container/keymap.py new file mode 100644 index 00000000..d52a5215 --- /dev/null +++ b/ranger/container/keymap.py @@ -0,0 +1,164 @@ +# Copyright (C) 2009, 2010 Roman Zimbelmann <romanz@lavabit.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from ranger.ext.tree import Tree +from ranger.ext.direction import Direction +from ranger.ext.keybinding_parser import parse_keybinding, DIRKEY, ANYKEY + +FUNC = 'func' +DIRARG = 'dir' +ALIASARG = 'alias' + +class CommandArgs(object): + """ + A CommandArgs object is passed to the keybinding function. + + This object simply aggregates information about the pressed keys + and the current environment. + + Attributes: + fm: the FM instance + wdg: the currently focused widget (or fm, if none is focused) + keybuffer: the keybuffer object + n: the prefixed number, eg 5 in the command "5yy" + directions: a list of directions which are entered for "<dir>" + direction: the first direction object from that list + keys: a string representation of the keybuffer + matches: all keys which are entered for "<any>" + match: the first match + binding: the used Binding object + """ + def __init__(self, fm, widget, keybuf): + self.fm = fm + self.wdg = widget + self.keybuffer = keybuf + self.n = keybuf.quant + self.direction = keybuf.directions and keybuf.directions[0] or None + self.directions = keybuf.directions + self.keys = str(keybuf) + self.matches = keybuf.matches + self.match = keybuf.matches and keybuf.matches[0] or None + self.binding = keybuf.command + + @staticmethod + def from_widget(widget): + return CommandArgs(widget.fm, \ + widget, widget.env.keybuffer) + + +class KeyMap(Tree): + """Contains a tree with all the keybindings""" + def map(self, *args, **keywords): + if keywords: + return self._add_binding(*args, **keywords) + firstarg = args[-1] + if hasattr(firstarg, '__call__'): + keywords[FUNC] = firstarg + return self._add_binding(*args[:-1], **keywords) + def decorator_function(func): + keywords = {FUNC:func} + self.map(*args, **keywords) + return func + return decorator_function + + __call__ = map + + def _add_binding(self, *keys, **actions): + assert keys + bind = Binding(keys, actions) + for key in keys: + self.set(parse_keybinding(key), bind) + + def unmap(self, *keys): + for key in keys: + self.unset(parse_keybinding(key)) + + def __getitem__(self, key): + return self.traverse(parse_keybinding(key)) + + +class KeyMapWithDirections(KeyMap): + def __init__(self, *args, **keywords): + Tree.__init__(self, *args, **keywords) + self.directions = KeyMap() + + def merge(self, other): + assert hasattr(other, 'directions'), 'Merging with wrong type?' + Tree.merge(self, other) + Tree.merge(self.directions, other.directions) + + def dir(self, *args, **keywords): + if ALIASARG in keywords: + self.directions.map(*args, **keywords) + else: + self.directions.map(*args, dir=Direction(**keywords)) + + +class KeyManager(object): + def __init__(self, keybuffer, contexts): + self._keybuffer = keybuffer + self._list_of_contexts = contexts + self.clear() + + def clear(self): + self.contexts = dict() + for context in self._list_of_contexts: + self.contexts[context] = KeyMapWithDirections() + + def map(self, context, *args, **keywords): + self.get_context(context).map(*args, **keywords) + + def dir(self, context, *args, **keywords): + self.get_context(context).dir(*args, **keywords) + + def unmap(self, context, *args, **keywords): + self.get_context(context).unmap(*args, **keywords) + + def merge_all(self, keymapwithdirection): + for context, keymap in self.contexts.items(): + keymap.merge(keymapwithdirection) + + def get_context(self, context): + assert isinstance(context, str) + assert context in self.contexts, "no such context: " + context + return self.contexts[context] + + def use_context(self, context): + context = self.get_context(context) + if self._keybuffer.keymap is not context: + self._keybuffer.assign(context, context.directions) + self._keybuffer.clear() + + +class Binding(object): + """The keybinding object""" + def __init__(self, keys, actions): + assert hasattr(keys, '__iter__') + assert isinstance(actions, dict) + self.actions = actions + try: + self.function = self.actions[FUNC] + except KeyError: + self.function = None + try: + self.direction = self.actions[DIRARG] + except KeyError: + self.direction = None + try: + alias = self.actions[ALIASARG] + except KeyError: + self.alias = None + else: + self.alias = tuple(parse_keybinding(alias)) diff --git a/ranger/core/actions.py b/ranger/core/actions.py index 029493b0..5b18fae4 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -20,6 +20,7 @@ from os import symlink, getcwd from inspect import cleandoc import ranger +from ranger.ext.direction import Direction from ranger import fsobject from ranger.shared import FileManagerAware, EnvironmentAware, SettingsAware from ranger.gui.widgets import console_mode as cmode @@ -27,20 +28,11 @@ from ranger.fsobject import File from ranger.ext import shutil_generatorized as shutil_g from ranger.fsobject.loader import LoadableObject -class Actions(EnvironmentAware, SettingsAware): +class Actions(FileManagerAware, EnvironmentAware, SettingsAware): search_method = 'ctime' search_forward = False # -------------------------- - # -- Backwards Compatibility - # -------------------------- - - def dummy(self, *args, **keywords): - """For backwards compatibility only.""" - - handle_mouse = resize = dummy - - # -------------------------- # -- Basic Commands # -------------------------- @@ -76,10 +68,10 @@ class Actions(EnvironmentAware, SettingsAware): """Redraw the window""" self.ui.redraw_window() - def open_console(self, mode=':', string=''): + def open_console(self, mode=':', string='', prompt=None): """Open the console if the current UI supports that""" if hasattr(self.ui, 'open_console'): - self.ui.open_console(mode, string) + self.ui.open_console(mode, string, prompt=prompt) def execute_file(self, files, **kw): """Execute a file. @@ -99,48 +91,55 @@ class Actions(EnvironmentAware, SettingsAware): # -- Moving Around # -------------------------- - def move_left(self, narg=1): - """Enter the parent directory""" - try: - directory = os.path.join(*(['..'] * narg)) - except: - return - self.env.enter_dir(directory) - - def move_right(self, mode=0, narg=None): - """Enter the current directory or execute the current file""" - cf = self.env.cf - sel = self.env.get_selection() - - if isinstance(narg, int): - mode = narg - if not self.env.enter_dir(cf): - if sel: - if self.execute_file(sel, mode=mode) is False: - self.open_console(cmode.OPEN_QUICK) + def move(self, narg=None, **kw): + """ + A universal movement method. - def move_pointer(self, relative = 0, absolute = None, narg=None): - """Move the pointer down by <relative> or to <absolute>""" - self.env.cwd.move(relative=relative, - absolute=absolute, narg=narg) + Accepts these parameters: + (int) down, (int) up, (int) left, (int) right, (int) to, + (bool) absolute, (bool) relative, (bool) pages, + (bool) percentage - def move_pointer_by_pages(self, relative): - """Move the pointer down by <relative> pages""" - self.env.cwd.move(relative=int(relative * self.env.termsize[0])) + to=X is translated to down=X, absolute=True - def move_pointer_by_percentage(self, relative=0, absolute=None, narg=None): - """Move the pointer down by <relative>% or to <absolute>%""" - try: - factor = len(self.env.cwd) / 100.0 - except: + Example: + self.move(down=4, pages=True) # moves down by 4 pages. + self.move(to=2, pages=True) # moves to page 2. + self.move(to=1, percentage=True) # moves to 80% + """ + cwd = self.env.cwd + if not cwd or not cwd.accessible or not cwd.content_loaded: return - if narg is not None: - absolute = narg + direction = Direction(kw) + if 'left' in direction or direction.left() > 0: + steps = direction.left() + if narg is not None: + steps *= narg + try: + directory = os.path.join(*(['..'] * steps)) + except: + return + self.env.enter_dir(directory) + + elif 'right' in direction: + mode = 0 + if narg is not None: + mode = narg + cf = self.env.cf + 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.env.cwd.move( - relative=int(relative * factor), - absolute=int(absolute * factor)) + elif direction.vertical(): + newpos = direction.move( + direction=direction.down(), + override=narg, + maximum=len(cwd), + current=cwd.pointer, + pagesize=self.ui.browser.hei) + cwd.move(to=newpos) def history_go(self, relative): """Move back and forth in the history""" @@ -172,16 +171,16 @@ class Actions(EnvironmentAware, SettingsAware): self.enter_dir(cf.path) elif cwd.pointer >= len(cwd) - 1: while True: - self.enter_dir('..') + self.move(left=1) cwd = self.env.cwd if cwd.pointer < len(cwd) - 1: break if cwd.path == '/': break - self.move_pointer(1) + self.move(down=1) self.traverse() else: - self.move_pointer(1) + self.move(down=1) self.traverse() # -------------------------- @@ -201,6 +200,9 @@ class Actions(EnvironmentAware, SettingsAware): return self.execute_file(file, app = 'editor') + def hint(self, text): + self.ui.hint(text) + def toggle_boolean_option(self, string): """Toggle a boolean option named <string>""" if isinstance(self.env.settings[string], bool): @@ -262,7 +264,7 @@ class Actions(EnvironmentAware, SettingsAware): cwd.mark_item(item, val) if movedown: - self.move_pointer(relative=narg) + self.move(down=narg) if hasattr(self.ui, 'redraw_main_column'): self.ui.redraw_main_column() @@ -338,7 +340,7 @@ class Actions(EnvironmentAware, SettingsAware): if movedown is None: movedown = len(sel) == 1 if movedown: - self.move_pointer(relative=1) + self.move(down=1) if hasattr(self.ui, 'redraw_main_column'): self.ui.redraw_main_column() @@ -355,7 +357,7 @@ class Actions(EnvironmentAware, SettingsAware): if movedown is None: movedown = len(sel) == 1 if movedown: - self.move_pointer(relative=1) + self.move(down=1) if hasattr(self.ui, 'redraw_main_column'): self.ui.redraw_main_column() @@ -384,6 +386,12 @@ class Actions(EnvironmentAware, SettingsAware): """Delete the bookmark with the name <key>""" self.bookmarks.delete(key) + def draw_bookmarks(self): + self.ui.browser.draw_bookmarks = True + + def hide_bookmarks(self): + self.ui.browser.draw_bookmarks = False + # -------------------------- # -- Pager # -------------------------- @@ -508,16 +516,29 @@ class Actions(EnvironmentAware, SettingsAware): self.env.cut = False self.ui.browser.main_column.request_redraw() - def copy(self): + def copy(self, narg=None, dirarg=None): """Copy the selected items""" - - selected = self.env.get_selection() - self.env.copy = set(f for f in selected if f in self.env.cwd.files) + cwd = self.env.cwd + if not narg and not dirarg: + selected = (f for f in self.env.get_selection() if f in cwd.files) + else: + if not dirarg and narg: + direction = Direction(down=1) + offset = 0 + else: + direction = Direction(dirarg) + offset = 1 + 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) self.env.cut = False self.ui.browser.main_column.request_redraw() - def cut(self): - self.copy() + def cut(self, narg=None, dirarg=None): + self.copy(narg=narg, dirarg=dirarg) self.env.cut = True self.ui.browser.main_column.request_redraw() diff --git a/ranger/core/environment.py b/ranger/core/environment.py index 49d92502..6b2d692c 100644 --- a/ranger/core/environment.py +++ b/ranger/core/environment.py @@ -20,10 +20,13 @@ import socket from os.path import abspath, normpath, join, expanduser, isdir from ranger.fsobject.directory import Directory, NoDirectoryGiven -from ranger.container import KeyBuffer, History +from ranger.container import KeyBuffer, KeyManager, History from ranger.ext.signal_dispatcher import SignalDispatcher from ranger.shared import SettingsAware +ALLOWED_CONTEXTS = ('browser', 'pager', 'embedded_pager', 'taskview', + 'console') + class Environment(SettingsAware, SignalDispatcher): """A collection of data which is relevant for more than one class. @@ -40,6 +43,7 @@ class Environment(SettingsAware, SignalDispatcher): pathway = None path = None keybuffer = None + keymanager = None def __init__(self, path): SignalDispatcher.__init__(self) @@ -47,7 +51,8 @@ class Environment(SettingsAware, SignalDispatcher): self._cf = None self.pathway = () self.directories = {} - self.keybuffer = KeyBuffer() + self.keybuffer = KeyBuffer(None, None) + self.keymanager = KeyManager(self.keybuffer, ALLOWED_CONTEXTS) self.copy = set() self.history = History(self.settings.max_history_size) @@ -81,7 +86,7 @@ class Environment(SettingsAware, SignalDispatcher): if key == curses.KEY_RESIZE: self.keybuffer.clear() - self.keybuffer.append(key) + self.keybuffer.add(key) def key_clear(self): """Clear the keybuffer""" diff --git a/ranger/core/fm.py b/ranger/core/fm.py index 224ef06f..1124f355 100644 --- a/ranger/core/fm.py +++ b/ranger/core/fm.py @@ -19,7 +19,6 @@ The File Manager, putting the pieces together from time import time from collections import deque -from curses import KEY_MOUSE, KEY_RESIZE import os import sys @@ -51,17 +50,12 @@ class FM(Actions, SignalDispatcher): self.tabs = {} self.current_tab = 1 self.loader = Loader() - self.apps = self.settings.apps.CustomApplications() - - def mylogfunc(text): - self.notify(text, bad=True) - self.run = Runner(ui=self.ui, apps=self.apps, - logfunc=mylogfunc) self.log.append('Ranger {0} started! Process ID is {1}.' \ .format(__version__, os.getpid())) self.log.append('Running on Python ' + sys.version.replace('\n','')) + # COMPAT @property def executables(self): """For compatibility. Calls get_executables()""" @@ -94,12 +88,22 @@ class FM(Actions, SignalDispatcher): self.ui = DefaultUI() self.ui.initialize() + def mylogfunc(text): + self.notify(text, bad=True) + self.run = Runner(ui=self.ui, apps=self.apps, + logfunc=mylogfunc) + self.env.signal_bind('cd', self._update_current_tab) def block_input(self, sec=0): self.input_blocked = sec != 0 self.input_blocked_until = time() + sec + def input_is_blocked(self): + if self.input_blocked and time() > self.input_blocked_until: + self.input_blocked = False + return self.input_blocked + def loop(self): """ The main loop consists of: @@ -136,19 +140,7 @@ class FM(Actions, SignalDispatcher): ui.set_load_mode(loader.has_work()) - key = ui.get_next_key() - - if key > 0: - if key == KEY_MOUSE: - ui.handle_mouse() - elif key == KEY_RESIZE: - ui.update_size() - else: - if self.input_blocked and \ - time() > self.input_blocked_until: - self.input_blocked = False - if not self.input_blocked: - ui.handle_key(key) + ui.handle_input() gc_tick += 1 if gc_tick > TICKS_BEFORE_COLLECTING_GARBAGE: diff --git a/ranger/defaults/commands.py b/ranger/defaults/commands.py index 2bb860bd..241e66df 100644 --- a/ranger/defaults/commands.py +++ b/ranger/defaults/commands.py @@ -13,130 +13,53 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import os -from collections import deque -from ranger.shared import FileManagerAware -from ranger.gui.widgets import console_mode as cmode -from ranger.ext.command_parser import LazyParser as parse - -class Command(FileManagerAware): - """Abstract command class""" - name = None - allow_abbrev = True - def __init__(self, line, mode): - self.line = line - self.mode = mode - - def execute(self): - """Override this""" - - def tab(self): - """Override this""" - - def quick_open(self): - """Override this""" - - def _tab_only_directories(self): - from os.path import dirname, basename, expanduser, join, isdir - - line = parse(self.line) - cwd = self.fm.env.cwd.path - - try: - rel_dest = line.rest(1) - except IndexError: - rel_dest = '' - - # expand the tilde into the user directory - if rel_dest.startswith('~'): - rel_dest = expanduser(rel_dest) - - # define some shortcuts - abs_dest = join(cwd, rel_dest) - abs_dirname = dirname(abs_dest) - rel_basename = basename(rel_dest) - rel_dirname = dirname(rel_dest) - - try: - # are we at the end of a directory? - if rel_dest.endswith('/') or rel_dest == '': - _, dirnames, _ = os.walk(abs_dest).next() - - # are we in the middle of the filename? - else: - _, dirnames, _ = os.walk(abs_dirname).next() - dirnames = [dn for dn in dirnames \ - if dn.startswith(rel_basename)] - except (OSError, StopIteration): - # os.walk found nothing - pass - else: - dirnames.sort() - - # no results, return None - if len(dirnames) == 0: - return - - # one result. since it must be a directory, append a slash. - if len(dirnames) == 1: - return line.start(1) + join(rel_dirname, dirnames[0]) + '/' - - # more than one result. append no slash, so the user can - # manually type in the slash to advance into that directory - return (line.start(1) + join(rel_dirname, dirname) for dirname in dirnames) - - def _tab_directory_content(self): - from os.path import dirname, basename, expanduser, join, isdir - - line = parse(self.line) - cwd = self.fm.env.cwd.path - - try: - rel_dest = line.rest(1) - except IndexError: - rel_dest = '' - - # expand the tilde into the user directory - if rel_dest.startswith('~'): - rel_dest = expanduser(rel_dest) - - # define some shortcuts - abs_dest = join(cwd, rel_dest) - abs_dirname = dirname(abs_dest) - rel_basename = basename(rel_dest) - rel_dirname = dirname(rel_dest) - - try: - # are we at the end of a directory? - if rel_dest.endswith('/') or rel_dest == '': - _, dirnames, filenames = os.walk(abs_dest).next() - names = dirnames + filenames - - # are we in the middle of the filename? - else: - _, dirnames, filenames = os.walk(abs_dirname).next() - names = [name for name in (dirnames + filenames) \ - if name.startswith(rel_basename)] - except (OSError, StopIteration): - # os.walk found nothing - pass - else: - names.sort() - - # no results, return None - if len(names) == 0: - return - - # one result. since it must be a directory, append a slash. - if len(names) == 1: - return line.start(1) + join(rel_dirname, names[0]) + '/' - - # more than one result. append no slash, so the user can - # manually type in the slash to advance into that directory - return (line.start(1) + join(rel_dirname, name) for name in names) - - -# -------------------------------- definitions +''' +This is the default file for command definitions. + +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. + +The return values for tab() can be either: + None: There is no tab completion + A string: Change the console to this string + A list/tuple/generator: cycle through every item in it +The return value for quick() can be: + False: Nothing happens + True: Execute the command afterwards +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: + from ranger.api.commands import * + +and write some command definitions, for example: + + class tabnew(Command): + def execute(self): + self.fm.tab_new() + + class tabgo(Command): + """ + :tabgo <n> + + Go to the nth tab. + """ + def execute(self): + num = self.line.split()[1] + self.fm.tab_open(int(num)) + +For a list of all actions, check /ranger/core/actions.py. +''' + +from ranger.api.commands import * + +alias('e', 'edit') +alias('q', 'quit') +alias('q!', 'quit!') +alias('qall', 'quit!') class cd(Command): """ @@ -164,7 +87,7 @@ class cd(Command): def tab(self): return self._tab_only_directories() - def quick_open(self): + def quick(self): from os.path import isdir, join, normpath line = parse(self.line) cwd = self.fm.env.cwd.path @@ -202,10 +125,10 @@ class find(Command): self.fm.search_method = 'search' if self.count == 1: - self.fm.move_right() + self.fm.move(right=1) self.fm.block_input(0.5) - def quick_open(self): + def quick(self): self._search() if self.count == 1: return True @@ -227,7 +150,7 @@ class find(Command): if arg in filename: self.count += 1 if self.count == 1: - cwd.move(absolute=(cwd.pointer + i) % len(cwd.files)) + cwd.move(to=(cwd.pointer + i) % len(cwd.files)) self.fm.env.cf = cwd.pointed_obj if self.count > 1: return False @@ -511,46 +434,3 @@ class grep(Command): action.extend(['-e', line.rest(1), '-r']) action.extend(f.path for f in self.fm.env.get_selection()) self.fm.execute_command(action, flags='p') - - -# -------------------------------- rest - -by_name = {} -for varname, var in vars().copy().items(): - try: - if issubclass(var, Command) and var != Command: - by_name[var.name or varname] = var - except TypeError: - pass -del varname -del var - -def alias(**kw): - """Create an alias for commands, eg: alias(quit=exit)""" - for key, value in kw.items(): - by_name[key] = value - -def get_command(name, abbrev=True): - if abbrev: - lst = [cls for cmd, cls in by_name.items() \ - if cmd.startswith(name) \ - and cls.allow_abbrev \ - or cmd == name] - if len(lst) == 0: - raise KeyError - if len(lst) == 1 or by_name[name] in lst: - return lst[0] - raise ValueError("Ambiguous command") - else: - try: - return by_name[name] - except KeyError: - return None - -def command_generator(start): - return (cmd + ' ' for cmd in by_name if cmd.startswith(start)) - -alias(e=edit, q=quit) # for unambiguity -alias(**{'q!':quit_now}) -alias(qall=quit_now) - diff --git a/ranger/defaults/keys.py b/ranger/defaults/keys.py index 0c805cb6..3f279d2b 100644 --- a/ranger/defaults/keys.py +++ b/ranger/defaults/keys.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2009, 2010 Roman Zimbelmann <romanz@lavabit.com> # # This program is free software: you can redistribute it and/or modify @@ -17,314 +18,353 @@ This is the default key configuration file of ranger. Syntax for binding keys: map(*keys, fnc) -keys are one or more key-combinations which are either: -* a string -* an integer which represents an ascii code -* a tuple of integers +Examples for keys: "x", "gg", "<C-J><A-4>", "<tab>", "<down><up><right>" -fnc is a function which is called with the CommandArgument object. +fnc is a function which is called with the CommandArgs object. -The CommandArgument object has these attributes: +The CommandArgs object has these attributes: arg.fm: the file manager instance arg.wdg: the current widget arg.n: the number typed before the key combination (if allowed) +arg.direction: the direction object (if applicable) arg.keys: the string representation of the used key combination arg.keybuffer: the keybuffer instance -Check ranger.keyapi for more information -""" - -# NOTE: The "map" object used below is a callable CommandList -# object and NOT the builtin python map function! - +Direction keys are special. They must be mapped with: map.dir(*keys, **args) +where args is a dict of values such as up, down, to, absolute, relative... +Example: map.dir('gg', to=0) +Direction keys can be accessed in a mapping that contians "<dir>". +Other special keys are "<any>" which matches any single key and "<bg>" +which will run the function passively, without clearing the keybuffer. + +Additionally, there are shortcuts for accessing methods of the current +file manager and widget instance: +map('xyz', fm.method(foo=bar)) +will be translated to: +map('xyz', lamdba arg: arg.fm.method(foo=bar)) +If possible, arg.n and arg.direction are automatically inserted. + + +Example scenario +---------------- +If this keys are defined: +map("dd", "d<dir>", fm.cut(foo=bar)) +map.dir("gg", to=0) + +Type in the keys on the left and the function on the right will be executed: +dd => fm.cut(foo=bar) +5dd => fm.cut(foo=bar, narg=5) +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 +------------------------- from ranger.api.keys import * -def _vimlike_aliases(map): - alias = map.alias - - # the key 'k' will always do the same as KEY_UP, etc. - alias(KEY_UP, 'k') - alias(KEY_DOWN, 'j') - alias(KEY_LEFT, 'h') - alias(KEY_RIGHT, 'l') - - alias(KEY_NPAGE, ctrl('f')) - alias(KEY_PPAGE, ctrl('b')) - alias(KEY_HOME, 'gg') - alias(KEY_END, 'G') - -def initialize_commands(map): - """Initialize the commands for the main user interface""" - - # -------------------------------------------------------- movement - _vimlike_aliases(map) - map.alias(KEY_LEFT, KEY_BACKSPACE, DEL) - - map(KEY_DOWN, fm.move_pointer(relative=1)) - map(KEY_UP, fm.move_pointer(relative=-1)) - map(KEY_RIGHT, KEY_ENTER, ctrl('j'), fm.move_right()) - map(KEY_LEFT, KEY_BACKSPACE, DEL, fm.move_left(1)) - - map(KEY_HOME, fm.move_pointer(absolute=0)) - map(KEY_END, fm.move_pointer(absolute=-1)) - - map('%', fm.move_pointer_by_percentage(absolute=50)) - map(KEY_NPAGE, ctrl('f'), fm.move_pointer_by_pages(1)) - map(KEY_PPAGE, ctrl('b'), fm.move_pointer_by_pages(-1)) - map(ctrl('d'), 'J', fm.move_pointer_by_pages(0.5)) - map(ctrl('u'), 'K', fm.move_pointer_by_pages(-0.5)) - - def move_parent(n): - def fnc(arg): - arg.fm.move_left() - arg.fm.move_pointer(n) - if arg.fm.env.cf.is_directory: - arg.fm.move_right() - return fnc - - map(']', move_parent(1)) - map('[', move_parent(-1)) - map('}', fm.traverse()) - map('{', fm.history_go(-1)) - - # --------------------------------------------------------- history - map('H', fm.history_go(-1)) - map('L', fm.history_go(1)) - - # ----------------------------------------------- tagging / marking - map('t', fm.tag_toggle()) - map('T', fm.tag_remove()) - - map(' ', fm.mark(toggle=True)) - map('v', fm.mark(all=True, toggle=True)) - map('V', fm.mark(all=True, val=False)) - - # ------------------------------------------ file system operations - map('yy', fm.copy()) - map('dd', fm.cut()) - map('ud', fm.uncut()) - map('pp', fm.paste()) - map('po', fm.paste(overwrite=True)) - map('pl', fm.paste_symlink()) - map('p', hint='press //p// once again to confirm pasting' \ - ', or //l// to create symlinks') - - # ---------------------------------------------------- run programs - map('S', fm.execute_command(os.environ['SHELL'])) - map('E', fm.edit_file()) - map(',term', fm.execute_command('x-terminal-emulator', flags='d')) - map('du', fm.execute_command('du --max-depth=1 -h | less')) - - # -------------------------------------------------- toggle options - map('b', fm.notify('Warning: settings are now changed with z!', bad=True)) - map('z', hint="show_//h//idden //p//review_files //d//irectories_first " \ - "//c//ollapse_preview flush//i//nput ca//s//e_insensitive") - map('zh', 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')) - 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')) - - # ------------------------------------------------------------ sort - map('o', 'O', hint="//s//ize //b//ase//n//ame //m//time //t//ype //r//everse") - sort_dict = { - 's': 'size', - 'b': 'basename', - 'n': 'basename', - 'm': 'mtime', - 't': 'type', - } - - for key, val in sort_dict.items(): - for key, is_capital in ((key, False), (key.upper(), True)): - # reverse if any of the two letters is capital - map('o' + key, fm.sort(func=val, reverse=is_capital)) - map('O' + key, fm.sort(func=val, reverse=True)) - - map('or', 'Or', 'oR', 'OR', lambda arg: \ - arg.fm.sort(reverse=not arg.fm.settings.sort_reverse)) - - # ----------------------------------------------- console shortcuts - @map("A") - def append_to_filename(arg): - command = 'rename ' + arg.fm.env.cf.basename - arg.fm.open_console(cmode.COMMAND, command) - - 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('tf', fm.open_console(cmode.COMMAND, 'filter ')) - map('d', hint='d//u// (disk usage) d//d// (cut)') - map('@', fm.open_console(cmode.OPEN, '@')) - map('#', fm.open_console(cmode.OPEN, '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('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('gs', fm.cd('/srv')) - map('gR', fm.cd(RANGERDIR)) - - # ------------------------------------------------------------ tabs - map('gc', ctrl('W'), fm.tab_close()) - map('gt', TAB, fm.tab_move(1)) - map('gT', KEY_BTAB, fm.tab_move(-1)) - map('gn', ctrl('N'), fm.tab_new()) - for n in range(1, 10): - map('g' + str(n), fm.tab_open(n)) - - # ------------------------------------------------------- searching - map('/', fm.open_console(cmode.SEARCH)) - - map('n', fm.search()) - map('N', fm.search(forward=False)) - - map('ct', fm.search(order='tag')) - map('cc', fm.search(order='ctime')) - map('cm', fm.search(order='mimetype')) - map('cs', fm.search(order='size')) - map('c', hint='//c//time //m//imetype //s//ize //t//agged') - - # ------------------------------------------------------- bookmarks - for key in ALLOWED_BOOKMARK_KEYS: - map("`" + key, "'" + key, fm.enter_bookmark(key)) - map("m" + key, fm.set_bookmark(key)) - map("um" + key, fm.unset_bookmark(key)) - map("`", "'", "m", "um", draw_bookmarks=True) - - # ---------------------------------------------------- change views - map('i', fm.display_file()) - map(ctrl('p'), fm.display_log()) - map('?', KEY_F1, fm.display_help()) - map('w', lambda arg: arg.fm.ui.open_taskview()) - - # ------------------------------------------------ system functions - _system_functions(map) - map('ZZ', 'ZQ', fm.exit()) - map(ctrl('R'), fm.reset()) - map('R', fm.reload_cwd()) - @map(ctrl('C')) - def ctrl_c(arg): - try: - item = arg.fm.loader.queue[0] - except: - arg.fm.notify("Type Q or :quit<Enter> to exit Ranger") - else: - 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('!', 's', fm.open_console(cmode.OPEN)) - map('r', fm.open_console(cmode.OPEN_QUICK)) - - map.rebuild_paths() - - -def initialize_console_commands(map): - """Initialize the commands for the console widget only""" - - # -------------------------------------------------------- movement - map(KEY_UP, wdg.history_move(-1)) - map(KEY_DOWN, wdg.history_move(1)) - - map(ctrl('b'), KEY_LEFT, wdg.move(relative = -1)) - map(ctrl('f'), KEY_RIGHT, wdg.move(relative = 1)) - map(ctrl('a'), KEY_HOME, wdg.move(absolute = 0)) - map(ctrl('e'), KEY_END, wdg.move(absolute = -1)) - - # ----------------------------------------- deleting / pasting text - map(ctrl('d'), KEY_DC, wdg.delete(0)) - map(ctrl('h'), KEY_BACKSPACE, DEL, wdg.delete(-1)) - map(ctrl('w'), wdg.delete_word()) - map(ctrl('k'), wdg.delete_rest(1)) - map(ctrl('u'), wdg.delete_rest(-1)) - map(ctrl('y'), wdg.paste()) - - # ------------------------------------------------ system functions - _system_functions(map) - map.unbind('Q') # we don't want to quit with Q in the console... - - map(KEY_F1, lambda arg: arg.fm.display_command_help(arg.wdg)) - map(ctrl('c'), ESC, wdg.close()) - map(ctrl('j'), KEY_ENTER, wdg.execute()) - map(TAB, wdg.tab()) - map(KEY_BTAB, wdg.tab(-1)) - - map.rebuild_paths() - - -def initialize_taskview_commands(map): - """Initialize the commands for the TaskView widget""" - _basic_movement(map) - _vimlike_aliases(map) - _system_functions(map) - - # -------------------------------------------------- (re)move tasks - map('K', wdg.task_move(0)) - map('J', wdg.task_move(-1)) - map('dd', wdg.task_remove()) - - # ------------------------------------------------ system functions - map('?', fm.display_help()) - map('w', 'q', ESC, ctrl('d'), ctrl('c'), - lambda arg: arg.fm.ui.close_taskview()) - - map.rebuild_paths() - - -def initialize_pager_commands(map): - _base_pager_commands(map) - map('q', 'i', ESC, KEY_F1, lambda arg: arg.fm.ui.close_pager()) - map.rebuild_paths() - - -def initialize_embedded_pager_commands(map): - _base_pager_commands(map) - map('q', 'i', ESC, lambda arg: arg.fm.ui.close_embedded_pager()) - map.rebuild_paths() - -def _base_pager_commands(map): - _basic_movement(map) - _vimlike_aliases(map) - _system_functions(map) - - # -------------------------------------------------------- movement - map(KEY_LEFT, wdg.move_horizontal(relative=-4)) - map(KEY_RIGHT, wdg.move_horizontal(relative=4)) - map(KEY_NPAGE, ctrl('f'), wdg.move(relative=1, pages=True)) - map(KEY_PPAGE, ctrl('b'), wdg.move(relative=-1, pages=True)) - map(ctrl('d'), wdg.move(relative=0.5, pages=True)) - map(ctrl('u'), wdg.move(relative=-0.5, pages=True)) - map(' ', wdg.move(relative=0.8, pages=True)) - - # ---------------------------------------------------------- others - map('E', fm.edit_file()) - map('?', fm.display_help()) - - # --------------------------------------------- less-like shortcuts - map.alias(KEY_NPAGE, 'f') - map.alias(KEY_PPAGE, 'b') - map.alias(ctrl('d'), 'd') - map.alias(ctrl('u'), 'u') - +keymanager.map("browser", "d", fm.move(down=0.5, pages=True)) -def _system_functions(map): - map('Q', fm.exit()) - map(ctrl('L'), fm.redraw_window()) +# Add less-like d/u keys to the "browser" context: +map = keymanager.get_context('browser') +map("d", fm.move(down=0.5, pages=True)) +map("u", fm.move(up=0.5, pages=True)) +# Add keys to all contexts +map = KeyMapWithDirections() # create new empty keymap. +map("q", fm.exit()) +map.dir("<down>", down=3) # I'm quick, I want to move 3 at once! +keymanager.merge_all(map) # merge the new map into all existing ones. +""" + +from ranger.api.keys import * -def _basic_movement(map): - map(KEY_DOWN, wdg.move(relative=1)) - map(KEY_UP, wdg.move(relative=-1)) - map(KEY_HOME, wdg.move(absolute=0)) - map(KEY_END, wdg.move(absolute=-1)) +# =================================================================== +# == Define keys for everywhere: +# =================================================================== +map = global_keys = KeyMapWithDirections() +map('Q', fm.exit()) +map('<C-L>', fm.redraw_window()) +map('<backspace2>', alias='<backspace>') # Backspace is bugged sometimes + +#map('<dir>', wdg.move()) +@map('<dir>') # move around with direction keys +def move(arg): + arg.wdg.move(narg=arg.n, **arg.direction) + +# -------------------------------------------------- direction keys +map.dir('<down>', down=1) +map.dir('<up>', up=1) +map.dir('<left>', left=1) +map.dir('<right>', right=1) +map.dir('<home>', down=0, absolute=True) +map.dir('<end>', down=-1, absolute=True) +map.dir('<pagedown>', down=1, pages=True) +map.dir('<pageup>', down=-1, pages=True) +map.dir('%', down=1, percentage=True, absolute=True) + + +# =================================================================== +# == Define aliases +# =================================================================== +map = vim_aliases = KeyMapWithDirections() +map.dir('j', alias='<down>') +map.dir('k', alias='<up>') +map.dir('h', alias='<left>') +map.dir('l', alias='<right>') +map.dir('gg', alias='<home>') +map.dir('G', alias='<end>') +map.dir('<C-F>', alias='<pagedown>') +map.dir('<C-B>', alias='<pageup>') + +map = readline_aliases = KeyMapWithDirections() +map.dir('<C-B>', alias='<left>') +map.dir('<C-F>', alias='<right>') +map.dir('<C-A>', alias='<home>') +map.dir('<C-E>', alias='<end>') +map.dir('<C-D>', alias='<delete>') +map.dir('<C-H>', alias='<backspace>') + + +# =================================================================== +# == Define keys in "browser" context: +# =================================================================== +map = keymanager.get_context('browser') +map.merge(global_keys) +map.merge(vim_aliases) + +# -------------------------------------------------------- movement +map('gg', fm.move(to=0)) +map('<C-D>', 'J', fm.move(down=0.5, pages=True)) +map('<C-U>', 'K', fm.move(up=0.5, pages=True)) +map(']', fm.move_parent(1)) +map('[', fm.move_parent(-1)) +map('}', fm.traverse()) +map('{', fm.history_go(-1)) + +# --------------------------------------------------------- history +map('H', fm.history_go(-1)) +map('L', fm.history_go(1)) + +# ----------------------------------------------- tagging / marking +map('t', fm.tag_toggle()) +map('T', fm.tag_remove()) + +map(' ', fm.mark(toggle=True)) +map('v', fm.mark(all=True, toggle=True)) +map('V', fm.mark(all=True, val=False)) + +# ------------------------------------------ file system operations +map('yy', 'y<dir>', fm.copy()) +map('dd', 'd<dir>', fm.cut()) +map('ud', fm.uncut()) +map('pp', fm.paste()) +map('po', fm.paste(overwrite=True)) +map('pl', fm.paste_symlink()) +map('p<bg>', fm.hint('press *p* once again to confirm pasting' \ + ', or *l* to create symlinks')) + +# ---------------------------------------------------- run programs +map('S', fm.execute_command(os.environ['SHELL'])) +map('E', fm.edit_file()) +map('.term', fm.execute_command('x-terminal-emulator', flags='d')) +map('du', fm.execute_command('du --max-depth=1 -h | less')) + +# -------------------------------------------------- toggle options +map('z<bg>', fm.hint("show_*h*idden *p*review_files" \ + "*d*irectories_first *c*ollapse_preview flush*i*nput")) +map('zh', fm.toggle_boolean_option('show_hidden')) +map('zp', fm.toggle_boolean_option('preview_files')) +map('zi', fm.toggle_boolean_option('flushinput')) +map('zd', fm.toggle_boolean_option('directories_first')) +map('zc', fm.toggle_boolean_option('collapse_preview')) + +# ------------------------------------------------------------ sort +map('o<bg>', 'O<bg>', fm.hint("*s*ize *b*ase*n*ame *m*time" \ + " *t*ype *r*everse")) +sort_dict = { + 's': 'size', + 'b': 'basename', + 'n': 'basename', + 'm': 'mtime', + 't': 'type', +} + +for key, val in sort_dict.items(): + for key, is_capital in ((key, False), (key.upper(), True)): + # reverse if any of the two letters is capital + map('o' + key, fm.sort(func=val, reverse=is_capital)) + map('O' + key, fm.sort(func=val, reverse=True)) + +map('or', 'Or', 'oR', 'OR', lambda arg: \ + arg.fm.sort(reverse=not arg.fm.settings.sort_reverse)) + +# ----------------------------------------------- console shortcuts +@map("A") +def append_to_filename(arg): + command = 'rename ' + arg.fm.env.cf.basename + arg.fm.open_console(cmode.COMMAND, command) + +map('cw', fm.open_console(cmode.COMMAND, 'rename ')) +map('cd', fm.open_console(cmode.COMMAND, 'cd ')) +map('f', fm.open_console(cmode.COMMAND_QUICK, 'find ')) +map('bf', fm.open_console(cmode.COMMAND, 'filter ')) +map('d<bg>', fm.hint('d*u* (disk usage) d*d* (cut)')) +map('@', fm.open_console(cmode.OPEN, '@')) +map('#', fm.open_console(cmode.OPEN, 'p!')) + +# --------------------------------------------- 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('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('gs', fm.cd('/srv')) +map('gR', fm.cd(RANGERDIR)) + +# ------------------------------------------------------------ tabs +map('gc', '<C-W>', fm.tab_close()) +map('gt', '<TAB>', fm.tab_move(1)) +map('gT', '<S-TAB>', fm.tab_move(-1)) +map('gn', '<C-N>', fm.tab_new()) +for n in range(1, 10): + map('g' + str(n), fm.tab_open(n)) + map('<A-' + str(n) + '>', fm.tab_open(n)) + +# ------------------------------------------------------- searching +map('/', fm.open_console(cmode.SEARCH)) + +map('n', fm.search()) +map('N', fm.search(forward=False)) + +map('ct', fm.search(order='tag')) +map('cc', fm.search(order='ctime')) +map('cm', fm.search(order='mimetype')) +map('cs', fm.search(order='size')) +map('c<bg>', fm.hint('*c*time *m*imetype *s*ize')) + +# ------------------------------------------------------- bookmarks +for key in ALLOWED_BOOKMARK_KEYS: + map("`" + key, "'" + key, fm.enter_bookmark(key)) + map("m" + key, fm.set_bookmark(key)) + map("um" + key, fm.unset_bookmark(key)) +map("`<bg>", "'<bg>", "m<bg>", fm.draw_bookmarks()) + +# ---------------------------------------------------- change views +map('i', fm.display_file()) +map('<C-P>', fm.display_log()) +map('?', KEY_F1, fm.display_help()) +map('w', lambda arg: arg.fm.ui.open_taskview()) + +# ------------------------------------------------ system functions +map('ZZ', 'ZQ', fm.exit()) +map('<C-R>', fm.reset()) +map('R', fm.reload_cwd()) +@map('<C-C>') +def ctrl_c(arg): + try: + item = arg.fm.loader.queue[0] + except: + arg.fm.notify("Type Q or :quit<Enter> to exit Ranger") + else: + 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)) + + +# =================================================================== +# == Define keys for the pager +# =================================================================== +map = pager_keys = KeyMapWithDirections() +map.merge(global_keys) +map.merge(vim_aliases) + +# -------------------------------------------------------- movement +map('<left>', wdg.move(left=4)) +map('<right>', 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)) +map('<C-B>', 'b', '<pageup>', wdg.move(up=1, pages=True)) +map('<space>', wdg.move(down=0.8, pages=True)) +map('<cr>', wdg.move(down=1)) + +# ---------------------------------------------------------- others +map('E', fm.edit_file()) +map('?', fm.display_help()) + +# --------------------------------------------------- bind the keys +# There are two different kinds of pagers, each have a different +# method for exiting: + +map = keymanager.get_context('pager') +map.merge(pager_keys) +map('q', 'i', '<esc>', lambda arg: arg.fm.ui.close_pager()) + +map = keymanager.get_context('embedded_pager') +map.merge(pager_keys) +map('q', 'i', '<esc>', lambda arg: arg.fm.ui.close_embedded_pager()) + + +# =================================================================== +# == Define keys for the taskview +# =================================================================== +map = keymanager.get_context('taskview') +map.merge(global_keys) +map.merge(vim_aliases) +map('K', wdg.task_move(0)) +map('J', wdg.task_move(-1)) +map('dd', wdg.task_remove()) + +map('?', fm.display_help()) +map('w', 'q', ESC, ctrl('d'), ctrl('c'), + lambda arg: arg.fm.ui.close_taskview()) + + +# =================================================================== +# == Define keys for the console +# =================================================================== +map = keymanager.get_context('console') +map.merge(global_keys) +map.merge(readline_aliases) + +map('<up>', wdg.history_move(-1)) +map('<down>', wdg.history_move(1)) +map('<home>', wdg.move(right=0, absolute=True)) +map('<end>', wdg.move(right=-1, absolute=True)) +map('<tab>', wdg.tab()) +map('<s-tab>', wdg.tab(-1)) +map('<c-c>', '<esc>', wdg.close()) +map('<CR>', '<c-j>', wdg.execute()) +map('<F1>', lambda arg: arg.fm.display_command_help(arg.wdg)) + +map('<backspace>', wdg.delete(-1)) +map('<delete>', wdg.delete(1)) +map('<C-W>', wdg.delete_word()) +map('<C-K>', wdg.delete_rest(1)) +map('<C-U>', wdg.delete_rest(-1)) +map('<C-Y>', wdg.paste()) + +# Any key which is still undefined will simply be typed in. +@map('<any>') +def type_key(arg): + arg.wdg.type_key(arg.match) + +# Allow typing in numbers: +def type_chr(n): + return lambda arg: arg.wdg.type_key(str(n)) +for number in range(10): + map(str(number), type_chr(number)) + +# Unmap some global keys so we can type them: +map.unmap('Q') +map.directions.unmap('%') diff --git a/ranger/defaults/options.py b/ranger/defaults/options.py index cddd70eb..2de8a230 100644 --- a/ranger/defaults/options.py +++ b/ranger/defaults/options.py @@ -124,3 +124,7 @@ def colorscheme_overlay(context, fg, bg, attr): # The above function was just an example, let's set it back to None colorscheme_overlay = None + +# Enable this if key combinations with the Alt Key don't work for you. +# (Especially on xterm) +xterm_alt_key = False diff --git a/ranger/ext/accumulator.py b/ranger/ext/accumulator.py index c65370d5..2e599c85 100644 --- a/ranger/ext/accumulator.py +++ b/ranger/ext/accumulator.py @@ -13,41 +13,27 @@ # 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.direction import Direction + class Accumulator(object): def __init__(self): self.pointer = 0 self.pointed_obj = None - def move(self, relative=0, absolute=None, pages=None, narg=None): - i = self.pointer + def move(self, narg=None, **keywords): + direction = Direction(keywords) lst = self.get_list() if not lst: return self.pointer - length = len(lst) - - if isinstance(absolute, int): - if isinstance(narg, int): - absolute = narg - if absolute < 0: # wrap - i = absolute + length - else: - i = absolute - - if relative != 0: - if isinstance(pages, int): - relative *= pages * self.get_height() - if isinstance(narg, int): - relative *= narg - i = int(i + relative) - - if i >= length: - i = length - 1 - if i < 0: - i = 0 - - self.pointer = i + pointer = direction.move( + direction=direction.down(), + maximum=len(lst), + override=narg, + pagesize=self.get_height(), + current=self.pointer) + self.pointer = pointer self.correct_pointer() - return self.pointer + return pointer def move_to_obj(self, arg, attr=None): if not arg: @@ -77,10 +63,10 @@ class Accumulator(object): test = obj if test == good: - self.move(absolute=i) + self.move(to=i) return True - return self.move(absolute=self.pointer) + return self.move(to=self.pointer) def correct_pointer(self): lst = self.get_list() diff --git a/ranger/ext/direction.py b/ranger/ext/direction.py new file mode 100644 index 00000000..b9fbcac9 --- /dev/null +++ b/ranger/ext/direction.py @@ -0,0 +1,141 @@ +# 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/>. + +""" +Directions provide convenience methods for movement operations. + +Direction objects are handled just like dicts but provide +methods like up() and down() which give you the correct value +for the vertical direction, even if only the "up" or "down" key +has been defined. + +Example application: +d = Direction(down=5) +print(d.up()) # prints -5 +print(bool(d.horizontal())) # False, since no horizontal direction is defined +""" + +class Direction(dict): + __doc__ = __doc__ # for nicer pydoc + + def __init__(self, dictionary=None, **keywords): + if dictionary is not None: + dict.__init__(self, dictionary) + else: + dict.__init__(self, keywords) + if 'to' in self: + self['down'] = self['to'] + self['absolute'] = True + + def copy(self): + return Direction(**self) + + def _get_bool(self, first, second, fallback=None): + try: return self[first] + except: + try: return not self[second] + except: return fallback + + def _get_direction(self, first, second, fallback=0): + try: return self[first] + except: + try: return -self[second] + except: return fallback + + def up(self): + return -Direction.down(self) + + def down(self): + return Direction._get_direction(self, 'down', 'up') + + def right(self): + return Direction._get_direction(self, 'right', 'left') + + def absolute(self): + return Direction._get_bool(self, 'absolute', 'relative') + + def left(self): + return -Direction.right(self) + + def relative(self): + return not Direction.absolute(self) + + def vertical_direction(self): + down = Direction.down(self) + return (down > 0) - (down < 0) + + def horizontal_direction(self): + right = Direction.right(self) + return (right > 0) - (right < 0) + + def vertical(self): + return set(self) & set(['up', 'down']) + + def horizontal(self): + return set(self) & set(['left', 'right']) + + def pages(self): + return 'pages' in self and self['pages'] + + def percentage(self): + return 'percentage' in self and self['percentage'] + + def multiply(self, n): + for key in ('up', 'right', 'down', 'left'): + try: + self[key] *= n + except: + pass + + def set(self, n): + for key in ('up', 'right', 'down', 'left'): + if key in self: + self[key] = n + + def move(self, direction, override=None, minimum=0, maximum=9999, + current=0, pagesize=1, offset=0): + """ + Calculates the new position in a given boundary. + + Example: + d = Direction(pages=True) + d.move(direction=3) # = 3 + d.move(direction=3, current=2) # = 5 + d.move(direction=3, pagesize=5) # = 15 + d.move(direction=3, pagesize=5, maximum=10) # = 10 + d.move(direction=9, override=2) # = 18 + """ + pos = direction + if override is not None: + if self.absolute(): + pos = override + else: + pos *= override + if self.pages(): + pos *= pagesize + elif self.percentage(): + pos *= maximum / 100.0 + if self.absolute(): + if pos < minimum: + pos += maximum + else: + pos += current + return int(max(min(pos, maximum + offset - 1), minimum)) + + def select(self, lst, override, current, pagesize, 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] + return dest + offset - 1, selection diff --git a/ranger/ext/keybinding_parser.py b/ranger/ext/keybinding_parser.py new file mode 100644 index 00000000..c33ac12f --- /dev/null +++ b/ranger/ext/keybinding_parser.py @@ -0,0 +1,96 @@ +# Copyright (C) 2009, 2010 Roman Zimbelmann <romanz@lavabit.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import curses.ascii +from string import ascii_lowercase + +def parse_keybinding(obj): + """ + Translate a keybinding to a sequence of integers + + Example: + lol<CR> => (108, 111, 108, 10) + """ + assert isinstance(obj, (tuple, int, str)) + if isinstance(obj, tuple): + for char in obj: + yield char + elif isinstance(obj, int): + yield obj + elif isinstance(obj, str): + in_brackets = False + bracket_content = None + for char in obj: + if in_brackets: + if char == '>': + in_brackets = False + string = ''.join(bracket_content).lower() + try: + keys = special_keys[string] + for key in keys: + yield key + except KeyError: + yield ord('<') + for c in bracket_content: + yield ord(c) + yield ord('>') + except TypeError: + yield keys # it was no tuple, just an int + else: + bracket_content.append(char) + else: + if char == '<': + in_brackets = True + bracket_content = [] + else: + yield ord(char) + if in_brackets: + yield ord('<') + for c in bracket_content: + yield ord(c) + +# Arbitrary numbers which are not used with curses.KEY_XYZ +DIRKEY = 9001 +ANYKEY = 9002 +PASSIVE_ACTION = 9003 + +special_keys = { + 'dir': DIRKEY, + 'any': ANYKEY, + 'bg': PASSIVE_ACTION, + 'backspace': curses.KEY_BACKSPACE, + 'backspace2': curses.ascii.DEL, + 'delete': curses.KEY_DC, + 'cr': ord("\n"), + 'enter': ord("\n"), + 'space': ord(" "), + 'esc': curses.ascii.ESC, + 'down': curses.KEY_DOWN, + 'up': curses.KEY_UP, + 'left': curses.KEY_LEFT, + 'right': curses.KEY_RIGHT, + 'pagedown': curses.KEY_NPAGE, + 'pageup': curses.KEY_PPAGE, + 'home': curses.KEY_HOME, + 'end': curses.KEY_END, + 'tab': ord('\t'), + 's-tab': curses.KEY_BTAB, +} + +for char in ascii_lowercase: + special_keys['c-' + char] = ord(char) - 96 + +for char in (ascii_lowercase + '0123456789'): + special_keys['a-' + char] = (27, ord(char)) diff --git a/ranger/ext/move.py b/ranger/ext/move.py deleted file mode 100644 index 948adae6..00000000 --- a/ranger/ext/move.py +++ /dev/null @@ -1,23 +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/>. - -def move_between(current, minimum, maximum, relative=0, absolute=None): - i = current - if isinstance(absolute, int): - i = absolute - if isinstance(relative, int): - i += relative - i = max(minimum, min(maximum - 1, i)) - return i diff --git a/ranger/ext/tree.py b/ranger/ext/tree.py new file mode 100644 index 00000000..a954136b --- /dev/null +++ b/ranger/ext/tree.py @@ -0,0 +1,136 @@ +# 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/>. + +class Tree(object): + def __init__(self, dictionary=None, parent=None, key=None): + if dictionary is None: + self._tree = dict() + else: + self._tree = dictionary + self.key = key + self.parent = parent + + def copy(self): + """Create a deep copy""" + def deep_copy_dict(dct): + dct = dct.copy() + for key, val in dct.items(): + if isinstance(val, dict): + dct[key] = deep_copy_dict(val) + return dct + newtree = Tree() + if isinstance(self._tree, dict): + newtree._tree = deep_copy_dict(self._tree) + else: + newtree._tree = self._tree + return newtree + + def merge(self, other, copy=False): + """Merge another Tree into a copy of self""" + def deep_merge(branch, otherbranch): + assert isinstance(otherbranch, dict) + if not isinstance(branch, dict): + branch = dict() + elif copy: + branch = branch.copy() + for key, val in otherbranch.items(): + if isinstance(val, dict): + if key not in branch: + branch[key] = None + branch[key] = deep_merge(branch[key], val) + else: + branch[key] = val + return branch + + if isinstance(self._tree, dict) and isinstance(other._tree, dict): + content = deep_merge(self._tree, other._tree) + elif copy and hasattr(other._tree, 'copy'): + content = other._tree.copy() + else: + content = other._tree + return type(self)(content) + + def set(self, keys, value, force=True): + """Sets the element at the end of the path to <value>.""" + if not isinstance(keys, (list, tuple)): + keys = tuple(keys) + if len(keys) == 0: + self.replace(value) + else: + fnc = force and self.plow or self.traverse + subtree = fnc(keys) + subtree.replace(value) + + def unset(self, iterable): + chars = list(iterable) + first = True + + while chars: + if first or isinstance(subtree, Tree) and subtree.empty(): + top = chars.pop() + subtree = self.traverse(chars) + assert top in subtree._tree, "no such key: " + chr(top) + del subtree._tree[top] + else: + break + first = False + + def empty(self): + return len(self._tree) == 0 + + def replace(self, value): + if self.parent: + self.parent[self.key] = value + self._tree = value + + def plow(self, iterable): + """Move along a path, creating nonexistant subtrees""" + tree = self._tree + last_tree = None + char = None + for char in iterable: + try: + newtree = tree[char] + if not isinstance(newtree, dict): + raise KeyError() + except KeyError: + newtree = dict() + tree[char] = newtree + last_tree = tree + tree = newtree + if isinstance(tree, dict): + return type(self)(tree, parent=last_tree, key=char) + else: + return tree + + def traverse(self, iterable): + """Move along a path, raising exceptions when failed""" + tree = self._tree + last_tree = tree + char = None + for char in iterable: + last_tree = tree + try: + tree = tree[char] + except TypeError: + raise KeyError("trying to enter leaf") + except KeyError: + raise KeyError(repr(char) + " not in tree " + str(tree)) + if isinstance(tree, dict): + return type(self)(tree, parent=last_tree, key=char) + else: + return tree + + __getitem__ = traverse diff --git a/ranger/fsobject/directory.py b/ranger/fsobject/directory.py index 1319ad21..c434ee5c 100644 --- a/ranger/fsobject/directory.py +++ b/ranger/fsobject/directory.py @@ -59,6 +59,7 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): order_outdated = False content_outdated = False + content_loaded = False sort_dict = { 'basename': sort_by_basename, @@ -214,7 +215,7 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): if self.pointed_obj is not None: self.sync_index() else: - self.move(absolute=0) + self.move(to=0) else: self.filenames = None self.files = None diff --git a/ranger/gui/defaultui.py b/ranger/gui/defaultui.py index 08e0b204..a0a5da4e 100644 --- a/ranger/gui/defaultui.py +++ b/ranger/gui/defaultui.py @@ -93,8 +93,8 @@ class DefaultUI(UI): def close_embedded_pager(self): self.browser.close_pager() - def open_console(self, mode, string=''): - if self.console.open(mode, string): + def open_console(self, mode, string='', prompt=None): + if self.console.open(mode, string, prompt=prompt): self.status.msg = None self.console.on_close = self.close_console self.console.visible = True diff --git a/ranger/gui/displayable.py b/ranger/gui/displayable.py index a7a0945d..9ca72b13 100644 --- a/ranger/gui/displayable.py +++ b/ranger/gui/displayable.py @@ -72,7 +72,6 @@ class Displayable(EnvironmentAware, FileManagerAware, CursesShortcuts): self.hei = 0 self.paryx = (0, 0) self.parent = None - self.fresh = True self._old_visible = self.visible @@ -155,7 +154,7 @@ class Displayable(EnvironmentAware, FileManagerAware, CursesShortcuts): def resize(self, y, x, hei=None, wid=None): """Resize the widget""" - do_move = self.fresh + do_move = True try: maxy, maxx = self.env.termsize except TypeError: @@ -213,7 +212,6 @@ class Displayable(EnvironmentAware, FileManagerAware, CursesShortcuts): except: pass - self.fresh = False self.paryx = self.win.getparyx() self.y, self.x = self.paryx if self.parent: diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py index cdaf6cde..3ac45ebe 100644 --- a/ranger/gui/ui.py +++ b/ranger/gui/ui.py @@ -20,8 +20,8 @@ import curses import _curses from .displayable import DisplayableContainer +from ranger.container.keymap import CommandArgs from .mouse_event import MouseEvent -from ranger.container import CommandList TERMINALS_WITH_TITLE = ("xterm", "xterm-256color", "rxvt", "rxvt-256color", "rxvt-unicode", "aterm", "Eterm", @@ -47,7 +47,7 @@ def _setup_mouse(signal): class UI(DisplayableContainer): is_set_up = False load_mode = False - def __init__(self, commandlist=None, env=None, fm=None): + def __init__(self, env=None, fm=None): self._draw_title = os.environ["TERM"] in TERMINALS_WITH_TITLE os.environ['ESCDELAY'] = '25' # don't know a cleaner way @@ -56,13 +56,9 @@ class UI(DisplayableContainer): if fm is not None: self.fm = fm - - if commandlist is None: - self.commandlist = CommandList() - self.settings.keys.initialize_commands(self.commandlist) - else: - self.commandlist = commandlist self.win = curses.initscr() + self.env.keymanager.use_context('browser') + self.env.keybuffer.clear() DisplayableContainer.__init__(self, None) @@ -136,41 +132,74 @@ class UI(DisplayableContainer): if hasattr(self, 'hint'): self.hint() - self.env.key_append(key) + if key < 0: + self.env.keybuffer.clear() + return if DisplayableContainer.press(self, key): return - try: - tup = self.env.keybuffer.tuple_without_numbers() + self.env.keymanager.use_context('browser') + self.env.key_append(key) + kbuf = self.env.keybuffer + cmd = kbuf.command - if tup: - cmd = self.commandlist[tup] - else: - return - except KeyError: - self.env.key_clear() + self.fm.hide_bookmarks() + + if kbuf.failure: + kbuf.clear() + return + elif not cmd: return self.env.cmd = cmd - if hasattr(cmd, 'show_obj') and hasattr(cmd.show_obj, 'hint'): - if hasattr(self, 'hint'): - self.hint(cmd.show_obj.hint) - elif hasattr(cmd, 'execute'): + if cmd.function: try: - cmd.execute_wrap(self) + cmd.function(CommandArgs.from_widget(self.fm)) except Exception as error: self.fm.notify(error) - self.env.key_clear() + if kbuf.done: + kbuf.clear() + else: + kbuf.clear() + + def handle_keys(self, *keys): + for key in keys: + self.handle_key(key) - def get_next_key(self): - """Waits for key input and returns the pressed key""" + def handle_input(self): key = self.win.getch() - if key is not -1: + if key is 27 or key >= 128 and key < 256: + # Handle special keys like ALT+X or unicode here: + keys = [key] + previous_load_mode = self.load_mode + self.set_load_mode(True) + for n in range(4): + getkey = self.win.getch() + if getkey is not -1: + keys.append(getkey) + if len(keys) == 1: + keys.append(-1) + if self.settings.xterm_alt_key: + if len(keys) == 2 and keys[1] in range(127, 256): + keys = [27, keys[1] - 128] + self.handle_keys(*keys) + self.set_load_mode(previous_load_mode) + if self.settings.flushinput: + curses.flushinp() + else: + # Handle simple key presses, CTRL+X, etc here: if self.settings.flushinput: curses.flushinp() - return key + if key > 0: + if key == curses.KEY_MOUSE: + self.handle_mouse() + elif key == curses.KEY_RESIZE: + self.update_size() + else: + if not self.fm.input_is_blocked(): + self.handle_key(key) def setup(self): """ diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index 73f1e61e..d7dbac4f 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -104,7 +104,7 @@ class BrowserColumn(Pager): self.fm.enter_dir(self.target.path) if index < len(self.target): - self.fm.move_pointer(absolute = index) + self.fm.move(to=index) elif event.pressed(3): try: clicked_file = self.target.files[index] @@ -114,7 +114,7 @@ class BrowserColumn(Pager): else: if self.level > 0: - self.fm.move_right() + self.fm.move(right=0) return True @@ -369,11 +369,11 @@ class BrowserColumn(Pager): self.scroll_begin = self._get_scroll_begin() self.target.scroll_begin = self.scroll_begin - def scroll(self, relative): - """scroll by n lines""" + def scroll(self, n): + """scroll down by n lines""" self.need_redraw = True - self.target.move(relative=relative) - self.target.scroll_begin += 3 * relative + self.target.move(down=n) + self.target.scroll_begin += 3 * n def __str__(self): return self.__class__.__name__ + ' at level ' + str(self.level) diff --git a/ranger/gui/widgets/browserview.py b/ranger/gui/widgets/browserview.py index f586596e..33418a2f 100644 --- a/ranger/gui/widgets/browserview.py +++ b/ranger/gui/widgets/browserview.py @@ -25,6 +25,7 @@ class BrowserView(Widget, DisplayableContainer): ratios = None preview = True preview_available = True + draw_bookmarks = False stretch_ratios = None need_clear = False @@ -89,10 +90,9 @@ class BrowserView(Widget, DisplayableContainer): self.need_clear = True def draw(self): - try: - if self.env.cmd.show_obj.draw_bookmarks: - self._draw_bookmarks() - except AttributeError: + if self.draw_bookmarks: + self._draw_bookmarks() + else: if self.need_clear: self.win.erase() self.need_redraw = True diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py index 5f45c26f..c18e7f50 100644 --- a/ranger/gui/widgets/console.py +++ b/ranger/gui/widgets/console.py @@ -24,13 +24,14 @@ import re from collections import deque from . import Widget -from ranger.defaults import commands 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.container import CommandList, History +from ranger.ext.direction import Direction +from ranger.container import History from ranger.container.history import HistoryEmptyException import ranger @@ -48,7 +49,6 @@ class _CustomTemplate(string.Template): class Console(Widget): mode = None visible = False - commandlist = None last_cursor_mode = None prompt = ':' copy = '' @@ -62,8 +62,6 @@ class Console(Widget): def __init__(self, win): Widget.__init__(self, win) - self.commandlist = CommandList() - self.settings.keys.initialize_console_commands(self.commandlist) self.clear() self.histories = [] # load histories from files @@ -113,9 +111,14 @@ class Console(Widget): except: pass - def open(self, mode, string=''): + def open(self, mode, string='', prompt=None): if not is_valid_mode(mode): return False + 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) @@ -158,37 +161,37 @@ class Console(Widget): self.line = '' def press(self, key): + self.env.keymanager.use_context('console') + self.env.key_append(key) + kbuf = self.env.keybuffer + cmd = kbuf.command - keytuple = self.env.keybuffer.tuple_with_numbers() - try: - cmd = self.commandlist[keytuple] - except KeyError: - # An unclean hack to allow unicode input. - # This whole part should be replaced. - try: - chrkey = chr(keytuple[0]) - except: - pass - else: - self.type_key(chrkey) - finally: - self.env.key_clear() - return - - if cmd == self.commandlist.dummy_object: + if kbuf.failure: + kbuf.clear() + return + elif not cmd: return - try: - cmd.execute_wrap(self) - except Exception as error: - self.fm.notify(error) - self.env.key_clear() + self.env.cmd = cmd + + if cmd.function: + try: + cmd.function(CommandArgs.from_widget(self)) + except Exception as error: + self.fm.notify(error) + if kbuf.done: + kbuf.clear() + else: + kbuf.clear() def type_key(self, key): self.tab_deque = None if isinstance(key, int): - key = chr(key) + try: + key = chr(key) + except ValueError: + return if self.pos == len(self.line): self.line += key @@ -216,14 +219,16 @@ class Console(Widget): self.history.fast_forward() self.history.modify(self.line) - def move(self, relative = 0, absolute = None): - if absolute is not None: - if absolute < 0: - self.pos = len(self.line) + 1 + absolute - else: - self.pos = absolute - - self.pos = min(max(0, self.pos + relative), len(self.line)) + def move(self, **keywords): + from ranger import log + log(keywords) + direction = Direction(keywords) + if direction.horizontal(): + self.pos = direction.move( + direction=direction.right(), + minimum=0, + maximum=len(self.line) + 1, + current=self.pos) def delete_rest(self, direction): self.tab_deque = None @@ -262,7 +267,7 @@ class Console(Widget): pos = self.pos + mod self.line = self.line[0:pos] + self.line[pos+1:] - self.move(relative = mod) + self.move(right=mod) self.on_line_change() def execute(self): @@ -341,7 +346,7 @@ class CommandConsole(ConsoleWithTab): return command_class(self.line, self.mode) def _get_cmd_class(self): - return commands.get_command(self.line.split()[0]) + return self.fm.commands.get_command(self.line.split()[0]) def _get_tab(self): if ' ' in self.line: @@ -351,7 +356,7 @@ class CommandConsole(ConsoleWithTab): else: return None - return commands.command_generator(self.line) + return self.fm.commands.command_generator(self.line) class QuickCommandConsole(CommandConsole): @@ -375,7 +380,7 @@ class QuickCommandConsole(CommandConsole): pass else: cmd = cls(self.line, self.mode) - if cmd and cmd.quick_open(): + if cmd and cmd.quick(): self.execute(cmd) @@ -419,8 +424,6 @@ class OpenConsole(ConsoleWithTab): def init(self): self.history = self.histories[OPEN_HISTORY] - OpenConsole.prompt = "{0}@{1} $ ".format(self.env.username, - self.env.hostname) def execute(self): command, flags = self._parse() diff --git a/ranger/gui/widgets/pager.py b/ranger/gui/widgets/pager.py index 2fc8ecda..00f3ec78 100644 --- a/ranger/gui/widgets/pager.py +++ b/ranger/gui/widgets/pager.py @@ -18,8 +18,8 @@ The pager displays text and allows you to scroll inside it. """ import re from . import Widget -from ranger.container.commandlist import CommandList -from ranger.ext.move import move_between +from ranger.ext.direction import Direction +from ranger.container.keymap import CommandArgs BAR_REGEXP = re.compile(r'\|\d+\?\|') QUOTES_REGEXP = re.compile(r'"[^"]+?"') @@ -41,15 +41,6 @@ class Pager(Widget): self.markup = None self.lines = [] - self.commandlist = CommandList() - - if embedded: - keyfnc = self.settings.keys.initialize_embedded_pager_commands - else: - keyfnc = self.settings.keys.initialize_pager_commands - - keyfnc(self.commandlist) - def open(self): self.scroll_begin = 0 self.markup = None @@ -116,68 +107,50 @@ class Pager(Widget): if TITLE_REGEXP.match(line): self.color_at(i, 0, -1, 'title', *baseclr) - - def move(self, relative=0, absolute=None, pages=None, narg=None): - i = self.scroll_begin - if isinstance(absolute, int): - if isinstance(narg, int): - absolute = narg - if absolute < 0: - i = absolute + len(self.lines) - else: - i = absolute - - if relative != 0: - if isinstance(pages, int): - relative *= pages * self.hei - if isinstance(narg, int): - relative *= narg - i = int(i + relative) - - length = len(self.lines) - self.hei - if i >= length: - self._get_line(i+self.hei) - - length = len(self.lines) - self.hei - if i >= length: - i = length - - if i < 0: - i = 0 - - self.scroll_begin = i - - def move_horizontal(self, relative=0, absolute=None, narg=None): - if narg is not None: - if absolute is None: - relative = relative < 0 and -narg or narg - else: - absolute = narg - - self.startx = move_between( - current=self.startx, - minimum=0, - maximum=999, - relative=relative, - absolute=absolute) + def move(self, narg=None, **kw): + direction = Direction(kw) + if direction.horizontal(): + self.startx = direction.move( + direction=direction.right(), + override=narg, + maximum=self._get_max_width(), + current=self.startx, + pagesize=self.wid, + offset=-self.wid) + if direction.vertical(): + if self.source_is_stream: + self._get_line(self.scroll_begin + self.hei * 2) + self.scroll_begin = direction.move( + direction=direction.down(), + override=narg, + maximum=len(self.lines), + current=self.scroll_begin, + pagesize=self.hei, + offset=-self.hei) def press(self, key): - try: - tup = self.env.keybuffer.tuple_without_numbers() - if tup: - cmd = self.commandlist[tup] - else: - return + self.env.keymanager.use_context(self.embedded and 'embedded_pager' or 'pager') + self.env.key_append(key) + kbuf = self.env.keybuffer + cmd = kbuf.command + + if kbuf.failure: + kbuf.clear() + return + elif not cmd: + return + + self.env.cmd = cmd - except KeyError: - self.env.key_clear() + if cmd.function: + try: + cmd.function(CommandArgs.from_widget(self)) + except Exception as error: + self.fm.notify(error) + if kbuf.done: + kbuf.clear() else: - if hasattr(cmd, 'execute'): - try: - cmd.execute_wrap(self) - except Exception as error: - self.fm.notify(error) - self.env.key_clear() + kbuf.clear() def set_source(self, source, strip=False): if self.source and self.source_is_stream: @@ -207,10 +180,11 @@ class Pager(Widget): n = event.ctrl() and 1 or 3 direction = event.mouse_wheel_direction() if direction: - self.move(relative=direction) + self.move(down=direction * n) return True def _get_line(self, n, attempt_to_read=True): + assert isinstance(n, int), n try: return self.lines[n] except (KeyError, IndexError): @@ -237,3 +211,6 @@ class Pager(Widget): except IndexError: raise StopIteration i += 1 + + def _get_max_width(self): + return max(len(line) for line in self.lines) diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py index b414153a..752bfd6b 100644 --- a/ranger/gui/widgets/statusbar.py +++ b/ranger/gui/widgets/statusbar.py @@ -123,7 +123,7 @@ class StatusBar(Widget): highlight = True space_left = self.wid starting_point = self.x - for string in self.hint.split('//'): + for string in self.hint.split('*'): highlight = not highlight if highlight: self.color('in_statusbar', 'text', 'highlight') diff --git a/ranger/gui/widgets/taskview.py b/ranger/gui/widgets/taskview.py index 6e86465c..475b903c 100644 --- a/ranger/gui/widgets/taskview.py +++ b/ranger/gui/widgets/taskview.py @@ -22,7 +22,7 @@ from collections import deque from . import Widget from ranger.ext.accumulator import Accumulator -from ranger.container import CommandList +from ranger.container.keymap import CommandArgs class TaskView(Widget, Accumulator): old_lst = None @@ -31,8 +31,6 @@ class TaskView(Widget, Accumulator): Widget.__init__(self, win) Accumulator.__init__(self) self.scroll_begin = 0 - self.commandlist = CommandList() - self.settings.keys.initialize_taskview_commands(self.commandlist) def draw(self): base_clr = deque() @@ -90,29 +88,35 @@ class TaskView(Widget, Accumulator): self.fm.loader.remove(index=i) - def task_move(self, absolute, i=None): + def task_move(self, to, i=None): if i is None: i = self.pointer - self.fm.loader.move(_from=i, to=absolute) + self.fm.loader.move(_from=i, to=to) def press(self, key): - try: - tup = self.env.keybuffer.tuple_without_numbers() - if tup: - cmd = self.commandlist[tup] - else: - return - - except KeyError: - self.env.key_clear() + self.env.keymanager.use_context('taskview') + self.env.key_append(key) + kbuf = self.env.keybuffer + cmd = kbuf.command + + if kbuf.failure: + kbuf.clear() + return + elif not cmd: + return + + self.env.cmd = cmd + + if cmd.function: + try: + cmd.function(CommandArgs.from_widget(self)) + except Exception as error: + self.fm.notify(error) + if kbuf.done: + kbuf.clear() else: - if hasattr(cmd, 'execute'): - try: - cmd.execute_wrap(self) - except Exception as error: - self.fm.notify(error) - self.env.key_clear() + kbuf.clear() def get_list(self): return self.fm.loader.queue diff --git a/ranger/shared/settings.py b/ranger/shared/settings.py index 44b0e55e..16167fe4 100644 --- a/ranger/shared/settings.py +++ b/ranger/shared/settings.py @@ -46,12 +46,7 @@ ALLOWED_SETTINGS = { 'colorscheme': str, 'colorscheme_overlay': (type(None), type(lambda:0)), 'hidden_filter': lambda x: isinstance(x, str) or hasattr(x, 'match'), -} - - -COMPAT_MAP = { - 'sort_reverse': 'reverse', - 'sort_directories_first': 'directories_first', + 'xterm_alt_key': bool, } @@ -152,32 +147,10 @@ class SettingsAware(object): else: settings._setting_sources.append(my_options) - # For backward compatibility: - for new, old in COMPAT_MAP.items(): - try: - setattr(my_options, new, getattr(my_options, old)) - print("Warning: the option `{0}'"\ - " was renamed to `{1}'\nPlease update"\ - " your configuration file soon." \ - .format(old, new)) - except AttributeError: - pass - from ranger.defaults import options as default_options settings._setting_sources.append(default_options) assert all(hasattr(default_options, setting) \ for setting in ALLOWED_SETTINGS), \ "Ensure that all options are defined in the defaults!" - try: - import apps - except ImportError: - from ranger.defaults import apps - settings._raw_set('apps', apps) - try: - import keys - except ImportError: - from ranger.defaults import keys - settings._raw_set('keys', keys) - SettingsAware.settings = settings |