diff options
-rw-r--r-- | ABOUT_THIS_BRANCH | 14 | ||||
-rw-r--r-- | ranger/api/keys.py | 1 | ||||
-rw-r--r-- | ranger/container/__init__.py | 3 | ||||
-rw-r--r-- | ranger/container/commandlist.py | 225 | ||||
-rw-r--r-- | ranger/container/keybuffer.py | 71 | ||||
-rw-r--r-- | ranger/container/keymap.py | 346 | ||||
-rw-r--r-- | ranger/core/actions.py | 50 | ||||
-rw-r--r-- | ranger/core/environment.py | 4 | ||||
-rw-r--r-- | ranger/defaults/keys.py | 220 | ||||
-rw-r--r-- | ranger/ext/direction.py | 121 | ||||
-rw-r--r-- | ranger/ext/tree.py | 135 | ||||
-rw-r--r-- | ranger/gui/ui.py | 42 | ||||
-rw-r--r-- | ranger/gui/widgets/browserview.py | 8 | ||||
-rw-r--r-- | ranger/gui/widgets/console.py | 5 | ||||
-rw-r--r-- | ranger/gui/widgets/pager.py | 11 | ||||
-rw-r--r-- | ranger/gui/widgets/taskview.py | 4 | ||||
-rw-r--r-- | test/tc_commandlist.py | 100 | ||||
-rw-r--r-- | test/tc_newkeys.py | 491 | ||||
-rw-r--r-- | test/tc_ui.py | 2 |
19 files changed, 1411 insertions, 442 deletions
diff --git a/ABOUT_THIS_BRANCH b/ABOUT_THIS_BRANCH new file mode 100644 index 00000000..ae09d54d --- /dev/null +++ b/ABOUT_THIS_BRANCH @@ -0,0 +1,14 @@ +I put my personal branch online, maybe you find some of the +parts useful. To see whats different, type: + +git diff master..hut + +This branch is being regularily rebased on the master branch, +which rewrites history, so maybe its better to pick single commits +from this branch into your own branch rather than working directly +on this one: + +git log master..hut +# search for a commit you like, write down SHA1 identifier +git checkout <your branch> +git cherry-pick <SHA1 of the commit> diff --git a/ranger/api/keys.py b/ranger/api/keys.py index 87186378..86911569 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 class Wrapper(object): def __init__(self, firstattr): diff --git a/ranger/container/__init__.py b/ranger/container/__init__.py index 51122291..4c8f08ba 100644 --- a/ranger/container/__init__.py +++ b/ranger/container/__init__.py @@ -17,6 +17,5 @@ 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, 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 deleted file mode 100644 index 2992aea2..00000000 --- a/ranger/container/keybuffer.py +++ /dev/null @@ -1,71 +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 to_string(i): - try: - return chr(i) - except ValueError: - return '?' - -from collections import deque -from curses.ascii import ascii - -ZERO = ord('0') -NINE = ord('9') - -class KeyBuffer(object): - def __init__(self): - self.number = None - self.queue = deque() - self.queue_with_numbers = deque() - - 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: - return - else: - self.queue.append(key) - - def tuple_with_numbers(self): - """Get a tuple of ascii codes.""" - return tuple(self.queue_with_numbers) - - 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) - - def __str__(self): - """returns a concatenation of all characters""" - return "".join( map( to_string, self.queue_with_numbers ) ) diff --git a/ranger/container/keymap.py b/ranger/container/keymap.py new file mode 100644 index 00000000..e8cf6119 --- /dev/null +++ b/ranger/container/keymap.py @@ -0,0 +1,346 @@ +# 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 +from string import ascii_lowercase +from inspect import isfunction, getargspec +from ranger.ext.tree import Tree +from ranger.ext.direction import Direction + +MAX_ALIAS_RECURSION = 20 +PASSIVE_ACTION = 9003 +DIRKEY = 9001 +ANYKEY = 9002 +FUNC = 'func' +DIRECTION = 'direction' +DIRARG = 'dir' +ALIASARG = 'alias' + +def to_string(i): + """convert a ord'd integer to a string""" + try: + return chr(i) + except ValueError: + return '?' + +def is_ascii_digit(n): + return n >= 48 and n <= 57 + +class CommandArgs(object): + """The arguments which are passed to a keybinding function""" + def __init__(self, fm, widget, keybuffer): + self.fm = fm + self.wdg = widget + self.keybuffer = keybuffer + self.n = keybuffer.quant + self.direction = keybuffer.directions and keybuffer.directions[0] or None + self.directions = keybuffer.directions + self.keys = str(keybuffer) + self.matches = keybuffer.matches + self.match = keybuffer.matches and keybuffer.matches[0] or None + self.binding = keybuffer.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 isfunction(firstarg): + 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(translate_keys(key), bind) + + def __getitem__(self, key): + return self.traverse(translate_keys(key)) + +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 + self.has_direction = False + else: + argnames = getargspec(self.function)[0] + try: + self.has_direction = actions['with_direction'] + except KeyError: + self.has_direction = DIRECTION in argnames + 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(translate_keys(alias)) + +class KeyBuffer(object): + """The evaluator and storage for pressed keys""" + def __init__(self, keymap, direction_keys): + self.assign(keymap, direction_keys) + + def assign(self, keymap, direction_keys): + self.keymap = keymap + self.direction_keys = direction_keys + + def add(self, key): + if self.failure: + return None + assert isinstance(key, int) + assert key >= 0 + self.all_keys.append(key) + + # 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): + # swap quant and direction_quant in bindings like '<dir>' + if self.quant is not None and self.command is None \ + and self.direction_quant is None: + self.direction_quant = self.quant + self.quant = None + + try: + assert isinstance(self.dir_tree_pointer, dict) + self.dir_tree_pointer = self.dir_tree_pointer[key] + except KeyError: + self.failure = True + else: + self._direction_try_to_finish() + + def _direction_try_to_finish(self, rec=MAX_ALIAS_RECURSION): + if rec <= 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: + try: + self.dir_tree_pointer = self.direction_keys[match.alias] + self._direction_try_to_finish(rec - 1) + except KeyError: + self.failure = True + return None + else: + if self.direction_quant is not None: + direction = match.actions['dir'] * self.direction_quant + direction.has_explicit_direction = True + else: + direction = match.actions['dir'].copy() + 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 is_ascii_digit(key) 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: + print(self.tree_pointer) + self.failure = True + return None + except KeyError: + try: + 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, rec=MAX_ALIAS_RECURSION): + if rec <= 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: + try: + self.tree_pointer = self.keymap[self.tree_pointer.alias] + self._try_to_finish(rec - 1) + except KeyError: + self.failure = True + return None + else: + self.command = self.tree_pointer + self.done = True + + def clear(self): + 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 + + self.eval_quantifier = True + self.eval_command = True + + def __str__(self): + """returns a concatenation of all characters""" + return "".join(to_string(c) for c in self.all_keys) + + def simulate_press(self, string): + for char in translate_keys(string): + self.add(char) + if self.done: + return self.command + if self.failure: + break + return self.command + +special_keys = { + 'dir': DIRKEY, + 'any': ANYKEY, + 'bg': PASSIVE_ACTION, + 'cr': ord("\n"), + 'enter': ord("\n"), + 'space': ord(" "), + 'down': curses.KEY_DOWN, + 'up': curses.KEY_UP, + 'left': curses.KEY_LEFT, + 'right': curses.KEY_RIGHT, + 'mouse': curses.KEY_MOUSE, + 'resize': curses.KEY_RESIZE, + 'pagedown': curses.KEY_NPAGE, + 'pageup': curses.KEY_PPAGE, + 'home': curses.KEY_HOME, + 'end': curses.KEY_END, + 'tab': ord('\t'), +} +for char in ascii_lowercase: + special_keys['c-' + char] = ord(char) - 96 + +def translate_keys(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: + yield special_keys[string] + except KeyError: + yield ord('<') + for c in bracket_content: + yield ord(c) + yield ord('>') + 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) diff --git a/ranger/core/actions.py b/ranger/core/actions.py index c4d82e58..e55d65b1 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -148,8 +148,10 @@ class Actions(EnvironmentAware, SettingsAware): """Delete the bookmark with the name <key>""" self.bookmarks.delete(key) - def move_left(self, narg=1): + def move_left(self, narg=None): """Enter the parent directory""" + if narg is None: + narg = 1 try: directory = os.path.join(*(['..'] * narg)) except: @@ -274,6 +276,44 @@ class Actions(EnvironmentAware, SettingsAware): self.env.cwd.move(relative=relative, absolute=absolute, narg=narg) + def move(self, dir, narg=None): + if narg is not None: + dir = dir * narg + + self.notify(str(dir)) + + if dir.right is not None: + if dir.right >= 0: + if dir.has_explicit_direction: + self.move_right(narg=dir.right) + else: + self.move_right(narg=dir.original_right - 1) + elif dir.right < 0: + self.move_left(narg=dir.left) + else: + if dir.percent: + if dir.absolute: + self.move_pointer_by_percentage( \ + absolute=dir.down, narg=narg) + else: + self.move_pointer_by_percentage( \ + relative=dir.down, narg=narg) + elif dir.pages: + self.move_pointer_by_pages(dir.down) + elif dir.absolute: + if dir.has_explicit_direction: + self.move_pointer(absolute=dir.down) + else: + self.move_pointer(absolute=dir.original_down) + else: + self.move_pointer(relative=dir.down) + + def draw_bookmarks(self): + self.ui.browser.draw_bookmarks = True + + def hide_bookmarks(self): + self.ui.browser.draw_bookmarks = False + 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])) @@ -288,9 +328,12 @@ class Actions(EnvironmentAware, SettingsAware): if narg is not None: absolute = narg + if absolute is not None: + absolute = int(absolute * factor) + self.env.cwd.move( relative=int(relative * factor), - absolute=int(absolute * factor)) + absolute=absolute) def scroll(self, relative): """Scroll down by <relative> lines""" @@ -369,6 +412,9 @@ class Actions(EnvironmentAware, SettingsAware): if hasattr(self.ui, 'notify'): self.ui.notify(text, duration=duration, bad=bad) + def hint(self, text): + self.notify(text) + def mark(self, all=False, toggle=False, val=None, movedown=None, narg=1): """ A wrapper for the directory.mark_xyz functions. diff --git a/ranger/core/environment.py b/ranger/core/environment.py index a30b8dcd..4301d237 100644 --- a/ranger/core/environment.py +++ b/ranger/core/environment.py @@ -45,7 +45,7 @@ class Environment(SettingsAware): self.path = abspath(expanduser(path)) self.pathway = () self.directories = {} - self.keybuffer = KeyBuffer() + self.keybuffer = KeyBuffer(None, None) self.copy = set() self.history = History(self.settings.max_history_size) @@ -66,7 +66,7 @@ class Environment(SettingsAware): 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/defaults/keys.py b/ranger/defaults/keys.py index 6042fd8b..f48c7012 100644 --- a/ranger/defaults/keys.py +++ b/ranger/defaults/keys.py @@ -187,6 +187,21 @@ def initialize_commands(map): map('?', KEY_F1, fm.display_help()) map('w', lambda arg: arg.fm.ui.open_taskview()) + # ---------------------------------------------------------- custom + # This is useful to track watched episode of a series. + @bind(']') + def tag_next_and_run(arg): + fm = arg.fm + fm.tag_remove() + fm.tag_remove(movedown=False) + fm.tag_toggle() + fm.move_pointer(relative=-2) + fm.move_right() + fm.move_pointer(relative=1) + + # "enter" = shortcut for "1l" + bind(KEY_ENTER, ctrl('j'), fm.move_right(mode=1)) + # ------------------------------------------------ system functions _system_functions(map) map('ZZ', fm.exit()) @@ -307,3 +322,208 @@ def _basic_movement(map): map(KEY_UP, wdg.move(relative=-1)) map(KEY_HOME, wdg.move(absolute=0)) map(KEY_END, wdg.move(absolute=-1)) + + + +# ------ newkey: + + +def base_directions(): + # Direction Keys + map = KeyMap() + map('<down>', dir=Direction(down=1)) + map('<up>', dir=Direction(down=-1)) + map('<left>', dir=Direction(right=-1)) + map('<right>', dir=Direction(right=1)) + map('<home>', dir=Direction(down=0, absolute=True)) + map('<end>', dir=Direction(down=-1, absolute=True)) + map('<pagedown>', dir=Direction(down=1, pages=True)) + map('<pageup>', dir=Direction(down=-1, pages=True)) + map('%<any>', dir=Direction(down=1, percent=True, absolute=True)) + map('<space>', dir=Direction(down=1, pages=True)) + map('<CR>', dir=Direction(down=1)) + + return map + +def vim(): + # Direction Keys + map = KeyMap() + map.merge(base_directions()) + map('j', alias='<down>') + map('k', alias='<up>') + map('h', alias='<left>') + map('l', alias='<right>') + map('gg', alias='<home>') + map('G', alias='<end>') + map('J', dir=Direction(down=20)) + map('K', dir=Direction(down=-20)) + + return map + +def system_keys(): + map = KeyMap() + map('Q', fm.exit()) + map('<mouse>', fm.handle_mouse()) + map('<C-L>', fm.redraw_window()) + map('<resize>', fm.resize()) + + return map + +def browser_keys(): + map = KeyMap() + map.merge(system_keys()) + + @map('<dir>') + def move(arg): + arg.fm.move(dir=arg.direction, narg=arg.n) + map(fm.exit(), 'Q') + + map('<cr>', fm.move(dir=Direction(right=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('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('b<bg>', fm.hint("bind_//h//idden //p//review_files" \ + "//d//irectories_first //c//ollapse_preview flush//i//nput")) + map('bh', fm.toggle_boolean_option('show_hidden')) + map('bp', fm.toggle_boolean_option('preview_files')) + map('bi', fm.toggle_boolean_option('flushinput')) + map('bd', fm.toggle_boolean_option('directories_first')) + map('bc', 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.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)')) + + + # --------------------------------------------- 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('gt', fm.cd('/tmp')) + map('gs', fm.cd('/srv')) + map('gR', fm.cd(RANGERDIR)) + + # ------------------------------------------------------- searching + map('/', fm.open_console(cmode.SEARCH)) + + map('n', fm.search()) + map('N', fm.search(forward=False)) + + map(TAB, 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()) + + + map(':', ';', fm.open_console(cmode.COMMAND)) + + # ---------------------------------------------------- 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 + map('ZZ', fm.exit()) + map(ctrl('R'), fm.reset()) + map('R', fm.reload_cwd()) + map(ctrl('C'), fm.exit()) + + map(':', ';', fm.open_console(cmode.COMMAND)) + map('>', fm.open_console(cmode.COMMAND_QUICK)) + map('!', fm.open_console(cmode.OPEN)) + map('r', fm.open_console(cmode.OPEN_QUICK)) + + return map + +def console_keys(): + map = KeyMap() + map.merge(system_keys()) + + @map('<any>') + def type_key(arg): + arg.wdg.type_key(arg.match) + + map('<up>', wdg.history_move(-1)) + map('<down>', wdg.history_move(1)) + map('<tab>', wdg.tab()) + +#from pprint import pprint +#pprint(browser_keys()._tree[106].__dict__) +#raise SystemExit() + +ui_keys = browser_keys() +taskview_keys = ui_keys +pager_keys = ui_keys +embedded_pager_keys = ui_keys +console_keys = console_keys() +directions = vim() diff --git a/ranger/ext/direction.py b/ranger/ext/direction.py new file mode 100644 index 00000000..30eb87ce --- /dev/null +++ b/ranger/ext/direction.py @@ -0,0 +1,121 @@ +# 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 NoDefault(object): + pass + +class Direction(object): + """An object with a down and right method""" + def __init__(self, right=None, down=None, absolute=False, + percent=False, pages=False, **keywords): + self.has_explicit_direction = False + + if 'up' in keywords: + self.down = -keywords['up'] + else: + self.down = down + + if 'left' in keywords: + self.right = -keywords['left'] + else: + self.right = right + + if 'relative' in keywords: + self.absolute = not relative + else: + self.absolute = absolute + + if 'default' in keywords: + self.default = keywords['default'] + else: + self.default = NoDefault + + self.original_down = self.down + self.original_right = self.right + + self.percent = percent + self.pages = pages + + @property + def up(self): + if self.down is None: + return None + return -self.down + + @property + def left(self): + if self.right is None: + return None + return -self.right + + @property + def relative(self): + return not self.absolute + + def down_or_default(self, default): + if self.has_been_modified: + return self.down + return default + + def steps_down(self, page_length=10): + if self.pages: + return self.down * page_length + else: + return self.down + + def steps_right(self, page_length=10): + if self.pages: + return self.right * page_length + else: + return self.right + + def copy(self): + new = type(self)() + new.__dict__.update(self.__dict__) + return new + + def __mul__(self, other): + copy = self.copy() + if self.absolute: + if self.down is not None: + copy.down = other + if self.right is not None: + copy.right = other + else: + if self.down is not None: + copy.down *= other + if self.right is not None: + copy.right *= other + copy.original_down = self.original_down + copy.original_right = self.original_right + return copy + __rmul__ = __mul__ + + def __str__(self): + s = ['<Direction'] + if self.down is not None: + s.append(" down=" + str(self.down)) + if self.right is not None: + s.append(" right=" + str(self.right)) + if self.absolute: + s.append(" absolute") + else: + s.append(" relative") + if self.pages: + s.append(" pages") + if self.percent: + s.append(" percent") + s.append('>') + return ''.join(s) diff --git a/ranger/ext/tree.py b/ranger/ext/tree.py new file mode 100644 index 00000000..6d841c2a --- /dev/null +++ b/ranger/ext/tree.py @@ -0,0 +1,135 @@ +# 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) + 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/gui/ui.py b/ranger/gui/ui.py index af72ee68..2d86736c 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", @@ -31,7 +31,7 @@ class UI(DisplayableContainer): is_set_up = False mousemask = curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION load_mode = False - def __init__(self, commandlist=None, env=None, fm=None): + def __init__(self, keymap=None, env=None, fm=None): self._draw_title = os.environ["TERM"] in TERMINALS_WITH_TITLE os.environ['ESCDELAY'] = '25' # don't know a cleaner way @@ -40,13 +40,13 @@ 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) + if keymap is None: + self.keymap = self.settings.keys.browser_keys() else: - self.commandlist = commandlist + self.keymap = keymap self.win = curses.initscr() + self.env.keybuffer.assign(self.keymap, self.settings.keys.directions) + self.env.keybuffer.clear() DisplayableContainer.__init__(self, None) @@ -138,28 +138,28 @@ class UI(DisplayableContainer): if DisplayableContainer.press(self, key): return - try: - tup = self.env.keybuffer.tuple_without_numbers() + 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)) except Exception as error: self.fm.notify(error) - self.env.key_clear() + if kbuf.done: + kbuf.clear() + else: + kbuf.clear() def get_next_key(self): """Waits for key input and returns the pressed key""" diff --git a/ranger/gui/widgets/browserview.py b/ranger/gui/widgets/browserview.py index ebce9900..8d6dc611 100644 --- a/ranger/gui/widgets/browserview.py +++ b/ranger/gui/widgets/browserview.py @@ -24,6 +24,7 @@ class BrowserView(Widget, DisplayableContainer): ratios = None preview = True preview_available = True + draw_bookmarks = False stretch_ratios = None need_clear = False @@ -64,10 +65,9 @@ class BrowserView(Widget, DisplayableContainer): self.add_child(self.pager) 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.old_cf != self.env.cf: self.need_clear = True if self.settings.draw_borders: diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py index 20a75c4e..4dea98c7 100644 --- a/ranger/gui/widgets/console.py +++ b/ranger/gui/widgets/console.py @@ -54,10 +54,9 @@ class Console(Widget): allow_close = False def __init__(self, win): - from ranger.container import CommandList, History + from ranger.container import History Widget.__init__(self, win) - self.commandlist = CommandList() - self.settings.keys.initialize_console_commands(self.commandlist) + self.keymap = self.settings.keys.console_keys self.clear() self.histories = [None] * 4 self.histories[DEFAULT_HISTORY] = History() diff --git a/ranger/gui/widgets/pager.py b/ranger/gui/widgets/pager.py index e376a2a6..c5ed8af1 100644 --- a/ranger/gui/widgets/pager.py +++ b/ranger/gui/widgets/pager.py @@ -18,7 +18,6 @@ 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 import log @@ -42,14 +41,10 @@ class Pager(Widget): self.markup = None self.lines = [] - self.commandlist = CommandList() - if embedded: - keyfnc = self.settings.keys.initialize_embedded_pager_commands + self.keymap = self.settings.keys.embedded_pager_keys else: - keyfnc = self.settings.keys.initialize_pager_commands - - keyfnc(self.commandlist) + self.keymap = self.settings.keys.pager_keys def open(self): self.scroll_begin = 0 @@ -166,7 +161,7 @@ class Pager(Widget): try: tup = self.env.keybuffer.tuple_without_numbers() if tup: - cmd = self.commandlist[tup] + cmd = self.keymap[tup] else: return diff --git a/ranger/gui/widgets/taskview.py b/ranger/gui/widgets/taskview.py index 6e86465c..f7937e11 100644 --- a/ranger/gui/widgets/taskview.py +++ b/ranger/gui/widgets/taskview.py @@ -22,7 +22,6 @@ from collections import deque from . import Widget from ranger.ext.accumulator import Accumulator -from ranger.container import CommandList class TaskView(Widget, Accumulator): old_lst = None @@ -31,8 +30,7 @@ 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) + self.keymap = self.settings.keys.taskview_keys def draw(self): base_clr = deque() 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_newkeys.py b/test/tc_newkeys.py new file mode 100644 index 00000000..0c810af5 --- /dev/null +++ b/test/tc_newkeys.py @@ -0,0 +1,491 @@ +# 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 * + +import sys + +class PressTestCase(TestCase): + """Some useful methods for the actual test""" + def _mkpress(self, keybuffer, keymap): + def press(keys): + keybuffer.clear() + match = keybuffer.simulate_press(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() + kb.simulate_press(keys) + self.assertTrue(kb.failure, "Keypress did not fail as expected") + kb.clear() + + def assertPressIncomplete(self, kb, keys): + kb.clear() + kb.simulate_press(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 = kb.simulate_press('pp') + args = CommandArgs(0, 0, kb) + self.assert_(match) + self.assert_(match.function) + self.assertEqual(8, match.function(args)) + + 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') + self.assertEqual(1, press('agg')) + + + def test_translate_keys(self): + def test(string, *args): + if not args: + args = (string, ) + self.assertEqual(ordtuple(*args), tuple(translate_keys(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) + + 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('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, 50): + 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(translate_keys('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, kb.simulate_press, '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(40, press('40j')) + + km.map('<dir><dir><any><any>', func=move) + + self.assertEqual(40, press('40jkhl')) + + 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) + + +if __name__ == '__main__': main() diff --git a/test/tc_ui.py b/test/tc_ui.py index affec907..98ddff93 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, keymap=Fake()) def fakesetup(): self.ui.widget = Fake() |