diff options
-rw-r--r-- | ranger/container/__init__.py | 3 | ||||
-rw-r--r-- | ranger/container/keybuffer.py | 176 | ||||
-rw-r--r-- | ranger/container/keymap.py | 289 | ||||
-rw-r--r-- | ranger/defaults/keys.py | 1 | ||||
-rw-r--r-- | ranger/ext/keybinding_parser.py | 96 | ||||
-rw-r--r-- | test/tc_newkeys.py | 25 |
6 files changed, 308 insertions, 282 deletions
diff --git a/ranger/container/__init__.py b/ranger/container/__init__.py index c1bb8194..3351cc63 100644 --- a/ranger/container/__init__.py +++ b/ranger/container/__init__.py @@ -17,5 +17,6 @@ used to manage stored data """ from ranger.container.history import History -from .keymap import KeyMap, KeyBuffer, KeyManager +from .keymap import KeyMap, KeyManager +from .keybuffer import KeyBuffer from .bookmarks import Bookmarks diff --git a/ranger/container/keybuffer.py b/ranger/container/keybuffer.py new file mode 100644 index 00000000..50914f84 --- /dev/null +++ b/ranger/container/keybuffer.py @@ -0,0 +1,176 @@ +# 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 collections import deque +from string import digits +from ranger.ext.keybinding_parser import parse_keybinding, \ + DIRKEY, ANYKEY, PASSIVE_ACTION +from ranger.container.keymap import Binding, KeyMap + +MAX_ALIAS_RECURSION = 20 + +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): + 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._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 chr(key) in digits 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: + chr(key) in digits 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): + 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 + + self.key_queue = deque() + + self.eval_quantifier = True + self.eval_command = True + + def __str__(self): + """returns a concatenation of all characters""" + return "".join("{0:c}".format(c) for c in self.all_keys) diff --git a/ranger/container/keymap.py b/ranger/container/keymap.py index f09d9cfb..167ba160 100644 --- a/ranger/container/keymap.py +++ b/ranger/container/keymap.py @@ -13,45 +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/>. -import curses.ascii -from collections import deque -from string import ascii_lowercase -from inspect import isfunction, getargspec from ranger.ext.tree import Tree from ranger.ext.direction import Direction +from ranger.ext.keybinding_parser import parse_keybinding, DIRKEY, ANYKEY -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): + def __init__(self, fm, widget, keybuf): 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 + 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): @@ -64,7 +46,7 @@ class KeyMap(Tree): if keywords: return self._add_binding(*args, **keywords) firstarg = args[-1] - if isfunction(firstarg): + if hasattr(firstarg, '__call__'): keywords[FUNC] = firstarg return self._add_binding(*args[:-1], **keywords) def decorator_function(func): @@ -79,14 +61,14 @@ class KeyMap(Tree): assert keys bind = Binding(keys, actions) for key in keys: - self.set(translate_keys(key), bind) + self.set(parse_keybinding(key), bind) def unmap(self, *keys): for key in keys: - self.unset(translate_keys(key)) + self.unset(parse_keybinding(key)) def __getitem__(self, key): - return self.traverse(translate_keys(key)) + return self.traverse(parse_keybinding(key)) class KeyMapWithDirections(KeyMap): @@ -153,13 +135,6 @@ class Binding(object): 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: @@ -169,238 +144,4 @@ class Binding(object): 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): - 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._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(translate_keys(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 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: - self.failure = True - return None - except KeyError: - try: - is_ascii_digit(key) 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: - self.key_queue.extend(translate_keys(self.tree_pointer.alias)) - self.tree_pointer = self.keymap._tree - self.max_alias_recursion -= 1 - else: - self.command = self.tree_pointer - self.done = True - - def clear(self): - 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 - - self.key_queue = deque() - - 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, - '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)) - -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: - 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) + self.alias = tuple(parse_keybinding(alias)) diff --git a/ranger/defaults/keys.py b/ranger/defaults/keys.py index 9e8e19e2..61138007 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 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/test/tc_newkeys.py b/test/tc_newkeys.py index c06c2894..8aa043ae 100644 --- a/test/tc_newkeys.py +++ b/test/tc_newkeys.py @@ -19,15 +19,26 @@ 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 = keybuffer.simulate_press(keys) + match = simulate_press(keybuffer, keys) self.assertFalse(keybuffer.failure, "parsing keys '"+keys+"' did fail!") self.assertTrue(keybuffer.done, @@ -40,13 +51,13 @@ class PressTestCase(TestCase): def assertPressFails(self, kb, keys): kb.clear() - kb.simulate_press(keys) + simulate_press(kb, keys) self.assertTrue(kb.failure, "Keypress did not fail as expected") kb.clear() def assertPressIncomplete(self, kb, keys): kb.clear() - kb.simulate_press(keys) + simulate_press(kb, keys) self.assertFalse(kb.failure, "Keypress failed, expected incomplete") self.assertFalse(kb.done, "Keypress done which was unexpected") kb.clear() @@ -77,7 +88,7 @@ class Test(PressTestCase): self.assertEqual(2, press('ppj')) kb.clear() - match = kb.simulate_press('pp') + match = simulate_press(kb, 'pp') args = CommandArgs(0, 0, kb) self.assert_(match) self.assert_(match.function) @@ -87,7 +98,7 @@ class Test(PressTestCase): def test(string, *args): if not args: args = (string, ) - self.assertEqual(ordtuple(*args), tuple(translate_keys(string))) + self.assertEqual(ordtuple(*args), tuple(parse_keybinding(string))) def ordtuple(*args): lst = [] @@ -404,7 +415,7 @@ class Test(PressTestCase): self.assertEqual(1, press('xxx')) # corrupt the tree - tup = tuple(translate_keys('xxx')) + tup = tuple(parse_keybinding('xxx')) x = ord('x') km._tree[x][x][x] = "Boo" @@ -413,7 +424,7 @@ class Test(PressTestCase): self.assertPressIncomplete(kb, 'xx') self.assertPressIncomplete(kb, 'x') if not sys.flags.optimize: - self.assertRaises(AssertionError, kb.simulate_press, 'xxx') + self.assertRaises(AssertionError, simulate_press, kb, 'xxx') kb.clear() def test_directions_as_functions(self): |