about summary refs log blame commit diff stats
path: root/104keyboard.subx
blob: f8213b9c05f5a6cb8e9e98005db5ffa3ea1e4947 (plain) (tree)



































































































































































































                                                                                              
# Primitives for keyboard control.
# Require Linux and a modern terminal.

== code

enable-keyboard-immediate-mode:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # . save registers
    50/push-eax
    51/push-ecx
    52/push-edx
    53/push-ebx
    56/push-esi
    57/push-edi
    #
    (_maybe-open-terminal)
    # var terminal-info/esi: (addr termios)
    # termios is a type from the Linux kernel. We don't care how large it is.
    81 5/subop/subtract %esp 0x100/imm32
    89/<- %esi 4/r32/esp
    # ioctl(*Terminal-file-descriptor, TCGETS, terminal-info)
    89/<- %edx 6/r32/esi
    b9/copy-to-ecx 0x5401/imm32/TCGETS
    8b/-> *Terminal-file-descriptor 3/r32/ebx
    e8/call syscall_ioctl/disp32
    # terminal-info->c_iflags &= Keyboard-immediate-mode-iflags
#?     (write-buffered Stderr "iflags before: ")
#?     (print-int32-buffered Stderr *esi)
#?     (write-buffered Stderr Newline)
#?     (flush Stderr)
    8b/-> *esi 0/r32/eax  # Termios-c_iflag
    23/and *Keyboard-immediate-mode-iflags 0/r32/eax
    89/<- *esi 0/r32/eax  # Termios-c_iflag
#?     (write-buffered Stderr "iflags after: ")
#?     (print-int32-buffered Stderr *esi)
#?     (write-buffered Stderr Newline)
#?     (flush Stderr)
    # terminal-info->c_lflags &= Keyboard-immediate-mode-lflags
#?     (write-buffered Stderr "lflags before: ")
#?     (print-int32-buffered Stderr *(esi+0xc))
#?     (write-buffered Stderr Newline)
#?     (flush Stderr)
    8b/-> *(esi+0xc) 0/r32/eax  # Termios-c_lflag
    23/and *Keyboard-immediate-mode-lflags 0/r32/eax
    89/<- *(esi+0xc) 0/r32/eax  # Termios-c_lflag
#?     (write-buffered Stderr "lflags after: ")
#?     (print-int32-buffered Stderr *(esi+0xc))
#?     (write-buffered Stderr Newline)
#?     (flush Stderr)
    # ioctl(*Terminal-file-descriptor, TCSETS, terminal-info)
    89/<- %edx 6/r32/esi
    b9/copy-to-ecx 0x5402/imm32/TCSETS
    8b/-> *Terminal-file-descriptor 3/r32/ebx
    e8/call syscall_ioctl/disp32
$enable-keyboard-immediate-mode:end:
    # . reclaim locals
    81 0/subop/add %esp 0x100/imm32
    # . restore registers
    5f/pop-to-edi
    5e/pop-to-esi
    5b/pop-to-ebx
    5a/pop-to-edx
    59/pop-to-ecx
    58/pop-to-eax
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

enable-keyboard-type-mode:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # . save registers
    50/push-eax
    51/push-ecx
    52/push-edx
    53/push-ebx
    56/push-esi
    57/push-edi
    #
    (_maybe-open-terminal)
    # var terminal-info/esi: (addr termios)
    # termios is a type from the Linux kernel. We don't care how large it is.
    81 5/subop/subtract %esp 0x100/imm32
    89/<- %esi 4/r32/esp
    # ioctl(*Terminal-file-descriptor, TCGETS, terminal-info)
    89/<- %edx 6/r32/esi
    b9/copy-to-ecx 0x5401/imm32/TCGETS
    8b/-> *Terminal-file-descriptor 3/r32/ebx
    e8/call syscall_ioctl/disp32
    # terminal-info->c_iflags |= Keyboard-type-mode-iflags
    8b/-> *esi 0/r32/eax  # Termios-c_iflag
    0b/or *Keyboard-type-mode-iflags 0/r32/eax
    89/<- *esi 0/r32/eax  # Termios-c_iflag
    # terminal-info->c_lflags |= Keyboard-type-mode-lflags
    8b/-> *(esi+0xc) 0/r32/eax  # Termios-c_lflag
    0b/or *Keyboard-type-mode-lflags 0/r32/eax
    89/<- *(esi+0xc) 0/r32/eax  # Termios-c_lflag
    # ioctl(*Terminal-file-descriptor, TCSETS, terminal-info)
    89/<- %edx 6/r32/esi
    b9/copy-to-ecx 0x5402/imm32/TCSETS
    8b/-> *Terminal-file-descriptor 3/r32/ebx
    e8/call syscall_ioctl/disp32
$enable-keyboard-type-mode:end:
    # . reclaim locals
    81 0/subop/add %esp 0x100/imm32
    # . restore registers
    5f/pop-to-edi
    5e/pop-to-esi
    5b/pop-to-ebx
    5a/pop-to-edx
    59/pop-to-ecx
    58/pop-to-eax
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

read-key:  # -> eax: byte
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # . save registers
    51/push-ecx
    # var buf/ecx: (stream byte 1)
    68/push 0/imm32/data
    68/push 1/imm32/size  # 3 bytes of data unused
    68/push 0/imm32/read
    68/push 0/imm32/write
    89/<- %ecx 4/r32/esp
    #
    (read 2 %ecx)  # => eax
    8b/-> *(ecx+0xc) 0/r32/eax
$read-key:end:
    # . reclaim locals
    81 0/subop/add %esp 0x10/imm32
    # . restore registers
    59/pop-to-ecx
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

== data

# iflags:   octal     hex
#  IGNBRK  0000001   0x0001
#  BRKINT  0000002   0x0002
#  IGNPAR  0000004   0x0004
#  PARMRK  0000010   0x0008
#  INPCK   0000020   0x0010
#  ISTRIP  0000040   0x0020
#  INLCR   0000100   0x0040
#  IGNCR   0000200   0x0080
#  ICRNL   0000400   0x0100
#  IUCLC   0001000   0x0200
#  IXON    0002000   0x0400
#  IXANY   0004000   0x0800
#  IXOFF   0010000   0x1000
#  IMAXBEL 0020000   0x2000
#  IUTF8   0040000   0x4000

# lflags:
#  ISIG   0000001     0x0001
#  ICANON 0000002     0x0002
#  ECHO   0000010     0x0008
#  ECHOE  0000020     0x0010
#  ECHOK  0000040     0x0020
#  ECHONL 0000100     0x0040
#  NOFLSH 0000200     0x0080
#  TOSTOP 0000400     0x0100
#  IEXTEN 0100000     0x8000

# recipe for raw mode according to the termios.3 manpage on Linux:
#   termios_p->c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
#   termios_p->c_oflag &= ~OPOST;
#   termios_p->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
#   termios_p->c_cflag &= ~(CSIZE | PARENB);
#   termios_p->c_cflag |= CS8;

Keyboard-immediate-mode-iflags:  # (addr tcflag_t)
#?     0xfffffa14  # ~IGNBRK & ~BRKINT & ~PARMRK & ~ISTRIP & ~INLCR & ~IGNCR & ~ICRNL & ~IXON
    0xffffffff/imm32

Keyboard-immediate-mode-lflags:  # (addr tcflag_t)
#?     0xffff7fb4/imm32  # ~ICANON & ~ISIG & ~IEXTEN & ~ECHO & ~ECHONL
    0xffffffb5/imm32  # ~ICANON & ~ECHO & ~ECHONL

Keyboard-type-mode-iflags:  # (addr tcflag_t)
    0x00000000/imm32  # ~Keyboard-immediate-mode-iflags

Keyboard-type-mode-lflags:  # (addr tcflag_t)
    0x0000004a/imm32  # ~Keyboard-immediate-mode-lflags
/* Literal.Number.Oct */ .highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */ .highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */ .highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */ .highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */ .highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */ .highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */ .highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */ .highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */ .highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */ .highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */ .highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */ .highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */ .highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */ .highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */ .highlight .vc { color: #336699 } /* Name.Variable.Class */ .highlight .vg { color: #dd7700 } /* Name.Variable.Global */ .highlight .vi { color: #3333bb } /* Name.Variable.Instance */ .highlight .vm { color: #336699 } /* Name.Variable.Magic */ .highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
# 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_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):
			n = 0
			for dir in arg.directions:
				n += dir.down
			return n
		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):
			n = 0
			for dir in arg.directions:
				n += dir.down
			return n

		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()