diff options
38 files changed, 2388 insertions, 1244 deletions
diff --git a/TODO b/TODO index 715ebc1b..cab7eb04 100644 --- a/TODO +++ b/TODO @@ -51,6 +51,8 @@ General (X) #71 10/03/21 previews: black/whitelist + read file (X) #79 10/04/08 tab number zero ( ) #80 10/04/08 when closing tabs, avoid gaps? + ( ) #81 10/04/15 system crash when previewing /proc/kcore with root permissions + Bugs diff --git a/ranger.py b/ranger.py index 636bd384..06b86531 100755 --- a/ranger.py +++ b/ranger.py @@ -34,13 +34,11 @@ return 1 # embed a shellscript. __doc__ = """Ranger - file browser for the unix terminal""" - # Importing the main method may fail if the ranger directory # is neither in the same directory as this file, nor in one of # pythons global import paths. try: from ranger.__main__ import main - except ImportError: import sys if '-d' not in sys.argv and '--debug' not in sys.argv: @@ -49,7 +47,5 @@ except ImportError: print("launch ranger.py in the top directory.") else: raise - else: main() - 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 diff --git a/test/tc_commandlist.py b/test/tc_commandlist.py deleted file mode 100644 index 9af2cf05..00000000 --- a/test/tc_commandlist.py +++ /dev/null @@ -1,100 +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/>. - -if __name__ == '__main__': from __init__ import init; init() - -from unittest import TestCase, main -from ranger.container.commandlist import CommandList as CL - -class Test(TestCase): - def assertKeyError(self, obj, key): - self.assertRaises(KeyError, obj.__getitem__, key) - - def test_commandist(self): - cl = CL() - fnc = lambda arg: 1 - fnc2 = lambda arg: 2 - dmy = cl.dummy_object - - cl.bind(fnc, 'aaaa') - cl.rebuild_paths() - - self.assertEqual(dmy, cl['a']) - self.assertEqual(dmy, cl['aa']) - self.assertEqual(dmy, cl['aaa']) - self.assertEqual(fnc, cl['aaaa'].execute) - self.assertKeyError(cl, 'aabb') - self.assertKeyError(cl, 'aaaaa') - - cl.bind(fnc, 'aabb') - cl.rebuild_paths() - - self.assertEqual(dmy, cl['a']) - self.assertEqual(dmy, cl['aa']) - self.assertEqual(dmy, cl['aab']) - self.assertEqual(fnc, cl['aabb'].execute) - self.assertEqual(dmy, cl['aaa']) - self.assertEqual(fnc, cl['aaaa'].execute) - - cl.unbind('aabb') - cl.rebuild_paths() - - self.assertEqual(dmy, cl['a']) - self.assertEqual(dmy, cl['aa']) - self.assertKeyError(cl, 'aabb') - self.assertKeyError(cl, 'aab') - self.assertEqual(dmy, cl['aaa']) - self.assertEqual(fnc, cl['aaaa'].execute) - - # Hints work different now. Since a rework of this system - # is planned anyway, there is no need to fix the test. - # hint_text = 'some tip blablablba' - # cl.hint(hint_text, 'aa') - # cl.rebuild_paths() - - self.assertEqual(dmy, cl['a']) - # self.assertEqual(hint_text, cl['aa'].text) - self.assertEqual(dmy, cl['aaa']) - self.assertEqual(fnc, cl['aaaa'].execute) - - # ------------------------ test aliases - cl.alias('aaaa', 'cc') - cl.rebuild_paths() - - self.assertEqual(dmy, cl['c']) - self.assertEqual(cl['cc'].execute, cl['aaaa'].execute) - - cl.bind(fnc2, 'aaaa') - cl.rebuild_paths() - - self.assertEqual(cl['cc'].execute, cl['aaaa'].execute) - - cl.unbind('cc') - cl.rebuild_paths() - - self.assertEqual(fnc2, cl['aaaa'].execute) - self.assertKeyError(cl, 'cc') - - # ----------------------- test clearing - cl.clear() - self.assertKeyError(cl, 'a') - self.assertKeyError(cl, 'aa') - self.assertKeyError(cl, 'aaa') - self.assertKeyError(cl, 'aaaa') - self.assertKeyError(cl, 'aab') - self.assertKeyError(cl, 'aabb') - - -if __name__ == '__main__': main() diff --git a/test/tc_direction.py b/test/tc_direction.py new file mode 100644 index 00000000..f45b4b36 --- /dev/null +++ b/test/tc_direction.py @@ -0,0 +1,88 @@ +# 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/>. + +if __name__ == '__main__': from __init__ import init; init() + +import unittest +from ranger.ext.direction import Direction +from ranger.ext.openstruct import OpenStruct + +class TestDirections(unittest.TestCase): + def test_symmetry(self): + d1 = Direction(right=4, down=7, relative=True) + d2 = Direction(left=-4, up=-7, absolute=False) + + def subtest(d): + self.assertEqual(4, d.right()) + self.assertEqual(7, d.down()) + self.assertEqual(-4, d.left()) + self.assertEqual(-7, d.up()) + self.assertEqual(True, d.relative()) + self.assertEqual(False, d.absolute()) + + self.assertTrue(d.horizontal()) + self.assertTrue(d.vertical()) + + subtest(d1) + subtest(d2) + + def test_conflicts(self): + d3 = Direction(right=5, left=2, up=3, down=6, + absolute=True, relative=True) + self.assertEqual(d3.right(), -d3.left()) + self.assertEqual(d3.left(), -d3.right()) + self.assertEqual(d3.up(), -d3.down()) + self.assertEqual(d3.down(), -d3.up()) + self.assertEqual(d3.absolute(), not d3.relative()) + self.assertEqual(d3.relative(), not d3.absolute()) + + def test_copy(self): + d = Direction(right=5) + c = d.copy() + self.assertEqual(c.right(), d.right()) + d['right'] += 3 + self.assertNotEqual(c.right(), d.right()) + c['right'] += 3 + self.assertEqual(c.right(), d.right()) + + self.assertFalse(d.vertical()) + self.assertTrue(d.horizontal()) + +# Doesn't work in python2? +# def test_duck_typing(self): +# dct = dict(right=7, down=-3) +# self.assertEqual(-7, Direction.left(dct)) +# self.assertEqual(3, Direction.up(dct)) + + def test_move(self): + d = Direction(pages=True) + self.assertEqual(3, d.move(direction=3)) + self.assertEqual(5, d.move(direction=3, current=2)) + self.assertEqual(15, d.move(direction=3, pagesize=5)) + self.assertEqual(9, d.move(direction=3, pagesize=5, maximum=10)) + self.assertEqual(18, d.move(direction=9, override=2)) + d2 = Direction(absolute=True) + self.assertEqual(5, d2.move(direction=9, override=5)) + + def test_select(self): + d = Direction(down=3) + lst = list(range(100)) + self.assertEqual((6, [3,4,5,6]), d.select(current=3, pagesize=10, override=None, lst=lst)) + d = Direction(down=3, pages=True) + self.assertEqual((9, [3,4,5,6,7,8,9]), d.select(current=3, pagesize=2, override=None, lst=lst)) + +if __name__ == '__main__': + unittest.main() + diff --git a/test/tc_displayable.py b/test/tc_displayable.py index 1bbffa73..558a20ff 100644 --- a/test/tc_displayable.py +++ b/test/tc_displayable.py @@ -113,26 +113,29 @@ class TestDisplayableWithCurses(unittest.TestCase): self.assertRaises(ValueError, disp.resize, -1, 0, hei, wid) self.assertRaises(ValueError, disp.resize, 0, -1, hei, wid) - box = [int(randint(0, hei) * 0.2), 0, - int(randint(0, wid) * 0.2), 0] - box[1] = randint(box[0], hei) - box[1] = randint(box[0], hei) - - def in_box(y, x): - return (x >= box[1] and x < box[1] + box[3]) and \ - (y >= box[0] and y < box[0] + box[2]) - - disp.resize(*box) - for y, x in zip(range(10), range(10)): - is_in_box = in_box(y, x) - - point1 = (y, x) - self.assertEqual(is_in_box, point1 in disp) - - point2 = Fake() - point2.x = x - point2.y = y - self.assertEqual(is_in_box, point2 in disp) + for i in range(1000): + box = [int(randint(0, hei) * 0.2), int(randint(0, wid) * 0.2)] + box.append(randint(0, hei - box[0])) + box.append(randint(0, wid - box[1])) + + def in_box(y, x): + return (y >= box[1] and y < box[1] + box[3]) and \ + (x >= box[0] and x < box[0] + box[2]) + + disp.resize(*box) + self.assertEqual(box, [disp.y, disp.x, disp.hei, disp.wid], + "Resizing failed for some reason on loop " + str(i)) + + for y, x in zip(range(10), range(10)): + is_in_box = in_box(y, x) + + point1 = (y, x) + self.assertEqual(is_in_box, point1 in disp) + + point2 = Fake() + point2.x = x + point2.y = y + self.assertEqual(is_in_box, point2 in disp) def test_click(self): self.disp.click = raise_ok diff --git a/test/tc_keyapi.py b/test/tc_keyapi.py index 4dfa1221..2f522173 100644 --- a/test/tc_keyapi.py +++ b/test/tc_keyapi.py @@ -29,6 +29,7 @@ class Test(TestCase): def __init__(self): self.fm = dummyfm() self.n = None + self.direction = None arg = commandarg() diff --git a/test/tc_newkeys.py b/test/tc_newkeys.py new file mode 100644 index 00000000..8efb707d --- /dev/null +++ b/test/tc_newkeys.py @@ -0,0 +1,606 @@ +# coding=utf-8 +# 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/>. + +if __name__ == '__main__': from __init__ import init; init() +from unittest import TestCase, main + +from ranger.ext.tree import Tree +from ranger.container.keymap import * +from ranger.container.keybuffer import KeyBuffer +from ranger.ext.keybinding_parser import parse_keybinding + +import sys + +def simulate_press(self, string): + for char in parse_keybinding(string): + self.add(char) + if self.done: + return self.command + if self.failure: + break + return self.command + +class PressTestCase(TestCase): + """Some useful methods for the actual test""" + def _mkpress(self, keybuffer, _=0): + def press(keys): + keybuffer.clear() + match = simulate_press(keybuffer, keys) + self.assertFalse(keybuffer.failure, + "parsing keys '"+keys+"' did fail!") + self.assertTrue(keybuffer.done, + "parsing keys '"+keys+"' did not complete!") + arg = CommandArgs(None, None, keybuffer) + self.assert_(match.function, "No function found! " + \ + str(match.__dict__)) + return match.function(arg) + return press + + def assertPressFails(self, kb, keys): + kb.clear() + simulate_press(kb, keys) + self.assertTrue(kb.failure, "Keypress did not fail as expected") + kb.clear() + + def assertPressIncomplete(self, kb, keys): + kb.clear() + simulate_press(kb, keys) + self.assertFalse(kb.failure, "Keypress failed, expected incomplete") + self.assertFalse(kb.done, "Keypress done which was unexpected") + kb.clear() + +class Test(PressTestCase): + """The test cases""" + def test_passive_action(self): + km = KeyMap() + directions = KeyMap() + kb = KeyBuffer(km, directions) + def n(value): + """return n or value""" + def fnc(arg=None): + if arg is None or arg.n is None: + return value + return arg.n + return fnc + + km.map('ppp', n(5)) + km.map('pp<bg>', n(8)) + km.map('pp<dir>', n(2)) + directions.map('j', dir=Direction(down=1)) + + press = self._mkpress(kb, km) + self.assertEqual(5, press('ppp')) + self.assertEqual(3, press('3ppp')) + + self.assertEqual(2, press('ppj')) + + kb.clear() + match = simulate_press(kb, 'pp') + args = CommandArgs(0, 0, kb) + self.assert_(match) + self.assert_(match.function) + self.assertEqual(8, match.function(args)) + + def test_translate_keys(self): + def test(string, *args): + if not args: + args = (string, ) + self.assertEqual(ordtuple(*args), tuple(parse_keybinding(string))) + + def ordtuple(*args): + lst = [] + for arg in args: + if isinstance(arg, str): + lst.extend(ord(c) for c in arg) + else: + lst.append(arg) + return tuple(lst) + + # 1 argument means: assume nothing is translated. + test('k') + test('kj') + test('k<dir>', 'k', DIRKEY) + test('k<ANY>z<any>', 'k', ANYKEY, 'z', ANYKEY) + test('k<anY>z<dir>', 'k', ANYKEY, 'z', DIRKEY) + test('<cr>', "\n") + test('<tab><tab><cr>', "\t\t\n") + test('<') + test('>') + test('<C-a>', 1) + test('<C-b>', 2) + for i in range(1, 26): + test('<C-' + chr(i+ord('a')-1) + '>', i) + test('<A-x>', 27, ord('x')) + test('<a-o>', 27, ord('o')) + test('k<a') + test('k<anz>') + test('k<a<nz>') + test('k<a<nz>') + test('k<a<>nz>') + test('>nz>') + + def test_alias(self): + def add_dirs(arg): + return sum(dir.down() for dir in arg.directions) + def return5(_): + return 5 + + directions = KeyMap() + directions.map('j', dir=Direction(down=1)) + directions.map('k', dir=Direction(down=-1)) + directions.map('<CR>', alias='j') + directions.map('@', alias='<CR>') + + base = KeyMap() + base.map('a<dir>', add_dirs) + base.map('b<dir>', add_dirs) + base.map('x<dir>x<dir>', add_dirs) + base.map('f', return5) + base.map('yy', alias='y') + base.map('!', alias='!') + + other = KeyMap() + other.map('b<dir>b<dir>', alias='x<dir>x<dir>') + other.map('c<dir>', add_dirs) + other.map('g', alias='f') + + km = base.merge(other, copy=True) + kb = KeyBuffer(km, directions) + + press = self._mkpress(kb, km) + + self.assertEqual(1, press('aj')) + self.assertEqual(2, press('bjbj')) + self.assertEqual(1, press('cj')) + self.assertEqual(1, press('c<CR>')) + + self.assertEqual(5, press('f')) + self.assertEqual(5, press('g')) + self.assertEqual(press('c<CR>'), press('c@')) + self.assertEqual(press('c<CR>'), press('c@')) + self.assertEqual(press('c<CR>'), press('c@')) + + for n in range(1, 10): + self.assertPressIncomplete(kb, 'y' * n) + + for n in range(1, 5): + self.assertPressFails(kb, '!' * n) + + def test_tree(self): + t = Tree() + t.set('abcd', "Yes") + self.assertEqual("Yes", t.traverse('abcd')) + self.assertRaises(KeyError, t.traverse, 'abcde') + self.assertRaises(KeyError, t.traverse, 'xyz') + self.assert_(isinstance(t.traverse('abc'), Tree)) + + t2 = Tree() + self.assertRaises(KeyError, t2.set, 'axy', "Lol", force=False) + t2.set('axx', 'ololol') + t2.set('axyy', "Lol") + self.assertEqual("Yes", t.traverse('abcd')) + self.assertRaises(KeyError, t2.traverse, 'abcd') + self.assertEqual("Lol", t2.traverse('axyy')) + self.assertEqual("ololol", t2.traverse('axx')) + + t2.unset('axyy') + self.assertEqual("ololol", t2.traverse('axx')) + self.assertRaises(KeyError, t2.traverse, 'axyy') + self.assertRaises(KeyError, t2.traverse, 'axy') + + t2.unset('a') + self.assertRaises(KeyError, t2.traverse, 'abcd') + self.assertRaises(KeyError, t2.traverse, 'a') + self.assert_(t2.empty()) + + def test_merge_trees(self): + def makeTreeA(): + t = Tree() + t.set('aaaX', 1) + t.set('aaaY', 2) + t.set('aaaZ', 3) + t.set('bbbA', 11) + t.set('bbbB', 12) + t.set('bbbC', 13) + t.set('bbbD', 14) + t.set('bP', 21) + t.set('bQ', 22) + return t + + def makeTreeB(): + u = Tree() + u.set('aaaX', 0) + u.set('bbbC', 'Yes') + u.set('bbbD', None) + u.set('bbbE', 15) + u.set('bbbF', 16) + u.set('bQ', 22) + u.set('bR', 23) + u.set('ffff', 1337) + return u + + # test 1 + t = Tree('a') + u = Tree('b') + merged = t.merge(u, copy=True) + self.assertEqual('b', merged._tree) + + # test 2 + t = Tree('a') + u = makeTreeA() + merged = t.merge(u, copy=True) + self.assertEqual(u._tree, merged._tree) + + # test 3 + t = makeTreeA() + u = makeTreeB() + v = t.merge(u, copy=True) + + self.assertEqual(0, v['aaaX']) + self.assertEqual(2, v['aaaY']) + self.assertEqual(3, v['aaaZ']) + self.assertEqual(11, v['bbbA']) + self.assertEqual('Yes', v['bbbC']) + self.assertEqual(None, v['bbbD']) + self.assertEqual(15, v['bbbE']) + self.assertEqual(16, v['bbbF']) + self.assertRaises(KeyError, t.__getitem__, 'bbbG') + self.assertEqual(21, v['bP']) + self.assertEqual(22, v['bQ']) + self.assertEqual(23, v['bR']) + self.assertEqual(1337, v['ffff']) + + # merge shouldn't be destructive + self.assertEqual(makeTreeA()._tree, t._tree) + self.assertEqual(makeTreeB()._tree, u._tree) + + v['fff'].replace('Lolz') + self.assertEqual('Lolz', v['fff']) + + v['aaa'].replace('Very bad') + v.plow('qqqqqqq').replace('eww.') + + self.assertEqual(makeTreeA()._tree, t._tree) + self.assertEqual(makeTreeB()._tree, u._tree) + + def test_add(self): + c = KeyMap() + c.map('aa', 'b', lambda *_: 'lolz') + self.assert_(c['aa'].function(), 'lolz') + @c.map('a', 'c') + def test(): + return 5 + self.assert_(c['b'].function(), 'lolz') + self.assert_(c['c'].function(), 5) + self.assert_(c['a'].function(), 5) + + def test_quantifier(self): + km = KeyMap() + directions = KeyMap() + kb = KeyBuffer(km, directions) + def n(value): + """return n or value""" + def fnc(arg=None): + if arg is None or arg.n is None: + return value + return arg.n + return fnc + km.map('p', n(5)) + press = self._mkpress(kb, km) + self.assertEqual(5, press('p')) + self.assertEqual(3, press('3p')) + self.assertEqual(6223, press('6223p')) + + def test_direction(self): + km = KeyMap() + directions = KeyMap() + kb = KeyBuffer(km, directions) + directions.map('j', dir=Direction(down=1)) + directions.map('k', dir=Direction(down=-1)) + def nd(arg): + """ n * direction """ + n = arg.n is None and 1 or arg.n + dir = arg.direction is None and Direction(down=1) \ + or arg.direction + return n * dir.down() + km.map('d<dir>', nd) + km.map('dd', func=nd) + + press = self._mkpress(kb, km) + + self.assertPressIncomplete(kb, 'd') + self.assertEqual( 1, press('dj')) + self.assertEqual( 3, press('3ddj')) + self.assertEqual( 15, press('3d5j')) + self.assertEqual(-15, press('3d5k')) + # supporting this kind of key combination would be too confusing: + # self.assertEqual( 15, press('3d5d')) + self.assertEqual( 3, press('3dd')) + self.assertEqual( 33, press('33dd')) + self.assertEqual( 1, press('dd')) + + km.map('x<dir>', nd) + km.map('xxxx', func=nd) + + self.assertEqual(1, press('xxxxj')) + self.assertEqual(1, press('xxxxjsomeinvalitchars')) + + # these combinations should break: + self.assertPressFails(kb, 'xxxj') + self.assertPressFails(kb, 'xxj') + self.assertPressFails(kb, 'xxkldfjalksdjklsfsldkj') + self.assertPressFails(kb, 'xyj') + self.assertPressIncomplete(kb, 'x') # direction missing + + def test_any_key(self): + km = KeyMap() + directions = KeyMap() + kb = KeyBuffer(km, directions) + directions.map('j', dir=Direction(down=1)) + directions.map('k', dir=Direction(down=-1)) + + directions.map('g<any>', dir=Direction(down=-1)) + + def cat(arg): + n = arg.n is None and 1 or arg.n + return ''.join(chr(c) for c in arg.matches) * n + + km.map('return<any>', cat) + km.map('cat4<any><any><any><any>', cat) + km.map('foo<dir><any>', cat) + + press = self._mkpress(kb, km) + + self.assertEqual('x', press('returnx')) + self.assertEqual('abcd', press('cat4abcd')) + self.assertEqual('abcdabcd', press('2cat4abcd')) + self.assertEqual('55555', press('5return5')) + + self.assertEqual('x', press('foojx')) + self.assertPressFails(kb, 'fooggx') # ANYKEY forbidden in DIRECTION + + km.map('<any>', lambda _: Ellipsis) + self.assertEqual('x', press('returnx')) + self.assertEqual('abcd', press('cat4abcd')) + self.assertEqual(Ellipsis, press('2cat4abcd')) + self.assertEqual(Ellipsis, press('5return5')) + self.assertEqual(Ellipsis, press('g')) + self.assertEqual(Ellipsis, press('ß')) + self.assertEqual(Ellipsis, press('ア')) + self.assertEqual(Ellipsis, press('9')) + + def test_multiple_directions(self): + km = KeyMap() + directions = KeyMap() + kb = KeyBuffer(km, directions) + directions.map('j', dir=Direction(down=1)) + directions.map('k', dir=Direction(down=-1)) + + def add_dirs(arg): + return sum(dir.down() for dir in arg.directions) + + km.map('x<dir>y<dir>', add_dirs) + km.map('four<dir><dir><dir><dir>', add_dirs) + + press = self._mkpress(kb, km) + + self.assertEqual(2, press('xjyj')) + self.assertEqual(0, press('fourjkkj')) + self.assertEqual(2, press('four2j4k2j2j')) + self.assertEqual(10, press('four1j2j3j4j')) + self.assertEqual(10, press('four1j2j3j4jafslkdfjkldj')) + + def test_corruptions(self): + km = KeyMap() + directions = KeyMap() + kb = KeyBuffer(km, directions) + press = self._mkpress(kb, km) + directions.map('j', dir=Direction(down=1)) + directions.map('k', dir=Direction(down=-1)) + km.map('xxx', lambda _: 1) + + self.assertEqual(1, press('xxx')) + + # corrupt the tree + tup = tuple(parse_keybinding('xxx')) + x = ord('x') + km._tree[x][x][x] = "Boo" + + self.assertPressFails(kb, 'xxy') + self.assertPressFails(kb, 'xzy') + self.assertPressIncomplete(kb, 'xx') + self.assertPressIncomplete(kb, 'x') + if not sys.flags.optimize: + self.assertRaises(AssertionError, simulate_press, kb, 'xxx') + kb.clear() + + def test_directions_as_functions(self): + km = KeyMap() + directions = KeyMap() + kb = KeyBuffer(km, directions) + press = self._mkpress(kb, km) + + def move(arg): + return arg.direction.down() + + directions.map('j', dir=Direction(down=1)) + directions.map('s', alias='j') + directions.map('k', dir=Direction(down=-1)) + km.map('<dir>', func=move) + + self.assertEqual(1, press('j')) + self.assertEqual(1, press('j')) + self.assertEqual(1, press('j')) + self.assertEqual(1, press('j')) + self.assertEqual(1, press('j')) + self.assertEqual(1, press('s')) + self.assertEqual(1, press('s')) + self.assertEqual(1, press('s')) + self.assertEqual(1, press('s')) + self.assertEqual(1, press('s')) + self.assertEqual(-1, press('k')) + self.assertEqual(-1, press('k')) + self.assertEqual(-1, press('k')) + + km.map('k', func=lambda _: 'love') + + self.assertEqual(1, press('j')) + self.assertEqual('love', press('k')) + + self.assertEqual(1, press('40j')) + self.assertEqual(40, kb.quant) + + km.map('<dir><dir><any><any>', func=move) + + self.assertEqual(1, press('40jkhl')) + self.assertEqual(40, kb.quant) + + def test_tree_deep_copy(self): + t = Tree() + s = t.plow('abcd') + s.replace('X') + u = t.copy() + self.assertEqual(t._tree, u._tree) + s = t.traverse('abc') + s.replace('Y') + self.assertNotEqual(t._tree, u._tree) + + def test_keymanager(self): + def func(arg): + return 5 + def getdown(arg): + return arg.direction.down() + + buffer = KeyBuffer(None, None) + press = self._mkpress(buffer) + keymanager = KeyManager(buffer, ['foo', 'bar']) + + map = keymanager.get_context('foo') + map('a', func) + map('b', func) + map = keymanager.get_context('bar') + map('c', func) + map('<dir>', getdown) + + keymanager.dir('foo', 'j', down=1) + keymanager.dir('bar', 'j', down=1) + + keymanager.use_context('foo') + self.assertEqual(5, press('a')) + self.assertEqual(5, press('b')) + self.assertPressFails(buffer, 'c') + + keymanager.use_context('bar') + self.assertPressFails(buffer, 'a') + self.assertPressFails(buffer, 'b') + self.assertEqual(5, press('c')) + self.assertEqual(1, press('j')) + keymanager.use_context('foo') + keymanager.use_context('foo') + keymanager.use_context('foo') + keymanager.use_context('bar') + keymanager.use_context('foo') + keymanager.use_context('bar') + keymanager.use_context('bar') + self.assertEqual(1, press('j')) + + def test_alias_to_direction(self): + def func(arg): + return arg.direction.down() + + km = KeyMapWithDirections() + kb = KeyBuffer(km, km.directions) + press = self._mkpress(kb) + + km.map('<dir>', func) + km.map('d<dir>', func) + km.dir('j', down=42) + km.dir('k', alias='j') + self.assertEqual(42, press('j')) + + km.dir('o', alias='j') + km.dir('ick', alias='j') + self.assertEqual(42, press('o')) + self.assertEqual(42, press('dj')) + self.assertEqual(42, press('dk')) + self.assertEqual(42, press('do')) + self.assertEqual(42, press('dick')) + self.assertPressFails(kb, 'dioo') + + def test_both_directory_and_any_key(self): + def func(arg): + return arg.direction.down() + def func2(arg): + return "yay" + + km = KeyMap() + directions = KeyMap() + kb = KeyBuffer(km, directions) + press = self._mkpress(kb) + + km.map('abc<dir>', func) + directions.map('j', dir=Direction(down=42)) + self.assertEqual(42, press('abcj')) + + km.unmap('abc<dir>') + + km.map('abc<any>', func2) + self.assertEqual("yay", press('abcd')) + + km.map('abc<dir>', func) + + km.map('abc<any>', func2) + self.assertEqual("yay", press('abcd')) + + def test_map_collision(self): + def add_dirs(arg): + return sum(dir.down() for dir in arg.directions) + def return5(_): + return 5 + + + directions = KeyMap() + directions.map('gg', dir=Direction(down=1)) + + + km = KeyMap() + km.map('gh', return5) + km.map('agh', return5) + km.map('a<dir>', add_dirs) + + kb = KeyBuffer(km, directions) + press = self._mkpress(kb, km) + + self.assertEqual(5, press('gh')) + self.assertEqual(5, press('agh')) +# self.assertPressFails(kb, 'agh') + # TODO: Make the next line work! For now, skip it. + # self.assertEqual(1, press('agg')) + + def test_keymap_with_dir(self): + def func(arg): + return arg.direction.down() + + km = KeyMapWithDirections() + kb = KeyBuffer(km, km.directions) + + press = self._mkpress(kb) + + km.map('abc<dir>', func) + km.dir('j', down=42) + self.assertEqual(42, press('abcj')) + +if __name__ == '__main__': main() diff --git a/test/tc_ui.py b/test/tc_ui.py index affec907..3c659459 100644 --- a/test/tc_ui.py +++ b/test/tc_ui.py @@ -28,7 +28,7 @@ class Test(unittest.TestCase): def setUp(self): self.fm = Fake() - self.ui = ui.UI(env=Fake(), fm=self.fm, commandlist=Fake()) + self.ui = ui.UI(env=Fake(), fm=self.fm) def fakesetup(): self.ui.widget = Fake() |