Completed
Push — master ( 27a45c...f38886 )
by Lambda
57s
created

Key.represent()   A

Complexity

Conditions 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
1
"""Key module."""
2
from curses import ascii  # type: ignore
3
from typing import cast, Union, Tuple, Dict, NamedTuple     # noqa: F401
4
from neovim import Nvim
5
from .util import ensure_bytes, ensure_str, int2chr
6
7
8
KeyCode = Union[int, bytes]
9
KeyExpr = Union[KeyCode, str]
10
11
12
ESCAPE_QUOTE = str.maketrans({
13
    '"': '\\"',
14
})
15
16
CTRL_KEY = b'\x80\xfc\x04'
17
META_KEY = b'\x80\xfc\x08'
18
19
# https://github.com/vim/vim/blob/d58b0f982ad758c59abe47627216a15497e9c3c1/src/gui_w32.c#L389-L456
20
SPECIAL_KEYS = {k: getattr(ascii, k) for k in ascii.controlnames}
21
SPECIAL_KEYS.update(dict(
22
    BSLASH=ord('\\'),
23
    LT=ord('<'),
24
    UP=b'\x80ku',
25
    DOWN=b'\x80kd',
26
    LEFT=b'\x80kl',
27
    RIGHT=b'\x80kr',
28
    F1=b'\x80k1',
29
    F2=b'\x80k2',
30
    F3=b'\x80k3',
31
    F4=b'\x80k4',
32
    F5=b'\x80k5',
33
    F6=b'\x80k6',
34
    F7=b'\x80k7',
35
    F8=b'\x80k8',
36
    F9=b'\x80k9',
37
    F10=b'\x80k;',
38
    F11=b'\x80F1',
39
    F12=b'\x80F2',
40
    F13=b'\x80F3',
41
    F14=b'\x80F4',
42
    F15=b'\x80F5',
43
    F16=b'\x80F6',
44
    F17=b'\x80F7',
45
    F18=b'\x80F8',
46
    F19=b'\x80F9',
47
    F20=b'\x80FA',
48
    F21=b'\x80FB',
49
    F22=b'\x80FC',
50
    F23=b'\x80FD',
51
    F24=b'\x80FE',
52
    HELP=b'\x80%1',
53
    BACKSPACE=b'\x80kb',
54
    INSERT=b'\x80kI',
55
    DELETE=b'\x80kD',
56
    HOME=b'\x80kh',
57
    END=b'\x80@7',
58
    PAGEUP=b'\x80kP',
59
    PAGEDOWN=b'\x80kN',
60
))
61
SPECIAL_KEYS_REVRESE = {v: k for k, v in SPECIAL_KEYS.items()}
62
# Add aliases used in Vim. This requires to be AFTER making swap dictionary
63
SPECIAL_KEYS.update(dict(
64
    NOP=SPECIAL_KEYS['NUL'],
65
    RETURN=SPECIAL_KEYS['CR'],
66
    ENTER=SPECIAL_KEYS['CR'],
67
    SPACE=SPECIAL_KEYS['SP'],
68
    BS=SPECIAL_KEYS['BACKSPACE'],
69
    INS=SPECIAL_KEYS['INSERT'],
70
    DEL=SPECIAL_KEYS['DELETE'],
71
))
72
73
74
#KeyBase = NamedTuple('KeyBase', [
75
#    ('code', KeyCode),
76
#    ('char', str),
77
#])
78
from collections import namedtuple
79
KeyBase = namedtuple('KeyBase', ['code', 'char'])
80
81
82
class Key(KeyBase):
83
    """Key class which indicate a single key.
84
85
    Attributes:
86
        code (int or bytes): A code of the key. A bytes is used when the key is
87
            a special key in Vim (a key which starts from 0x80 in getchar()).
88
        char (str): A printable represantation of the key. It might be an empty
89
            string when the key is not printable.
90
    """
91
92
    __slots__ = ()  # type: Tuple[str, ...]
93
    __cached = {}   # type: Dict[KeyExpr, Key]
94
95
    def __str__(self) -> str:
96
        return self.char
97
98
    @classmethod
99
    def represent(cls, nvim: Nvim, code: KeyCode) -> str:
100
        if code in SPECIAL_KEYS_REVRESE:
101
            char = SPECIAL_KEYS_REVRESE.get(code).capitalize()
102
            return '<%s>' % char
103
        else:
104
            return ensure_str(nvim, code)
105
106
107
    @classmethod
108
    def parse(cls, nvim: Nvim, expr: KeyExpr) -> 'Key':
109
        """Parse a key expression and return a Key instance.
110
111
        It returns a Key instance of a key expression. The instance is cached
112
        to individual expression so that the instance is exactly equal when
113
        same expression is spcified.
114
115
        Args:
116
            expr (int, bytes, or str): A key expression.
117
118
        Example:
119
            >>> from unittest.mock import MagicMock
120
            >>> nvim = MagicMock()
121
            >>> nvim.options = {'encoding': 'utf-8'}
122
            >>> Key.parse(nvim, ord('a'))
123
            Key(code=97, char='a')
124
            >>> Key.parse(nvim, '<Insert>')
125
            Key(code=b'\x80kI', char='')
126
127
        Returns:
128
            Key: A Key instance.
129
        """
130
        if expr not in cls.__cached:
131
            code = _resolve(nvim, expr)
132
            if isinstance(code, int):
133
                char = int2chr(nvim, code)
134
            elif not code.startswith(b'\x80'):
135
                char = ensure_str(nvim, code)
136
            else:
137
                char = ''
138
            cls.__cached[expr] = cls(code, char)
139
        return cls.__cached[expr]
140
141
142
def _resolve(nvim: Nvim, expr: KeyExpr) -> KeyCode:
143
    if isinstance(expr, int):
144
        return expr
145
    elif isinstance(expr, str):
146
        return _resolve(nvim, ensure_bytes(nvim, expr))
147
    elif isinstance(expr, bytes):
148
        if len(expr) == 1:
149
            return ord(expr)
150
        elif expr.startswith(b'\x80'):
151
            return expr
152
    else:
153
        raise AttributeError((
154
            '`expr` (%s) requires to be an instance of int|bytes|str but '
155
            '"%s" has specified.'
156
        ) % (expr, type(expr)))
157
    # Special key
158
    if expr.startswith(b'<') or expr.endswith(b'>'):
159
        inner = expr[1:-1]
160
        code = _resolve_from_special_keys(nvim, inner)
161
        if code != inner:
162
            return code
163
    return expr
164
165
166
def _resolve_from_special_keys(nvim: Nvim, inner: bytes) -> KeyCode:
167
    inner_upper = inner.upper()
168
    inner_upper_str = ensure_str(nvim, inner_upper)
169
    if inner_upper_str in SPECIAL_KEYS:
170
        return SPECIAL_KEYS[inner_upper_str]
171
    elif inner_upper.startswith(b'C-'):
172
        if len(inner) == 3:
173
            if inner_upper[-1] in b'@ABCDEFGHIKLMNOPQRSTUVWXYZ[\\]^_?':
174
                return ascii.ctrl(inner[-1])
175
        return b''.join([
176
            CTRL_KEY,
177
            cast(bytes, _resolve_from_special_keys(nvim, inner[2:])),
178
        ])
179
    elif inner_upper.startswith(b'M-') or inner_upper.startswith(b'A-'):
180
        return b''.join([
181
            META_KEY,
182
            cast(bytes, _resolve_from_special_keys(nvim, inner[2:])),
183
        ])
184
    elif inner_upper == b'LEADER':
185
        leader = nvim.vars['mapleader']
186
        leader = ensure_bytes(nvim, leader)
187
        return _resolve(nvim, leader)
188
    elif inner_upper == b'LOCALLEADER':
189
        leader = nvim.vars['maplocalleader']
190
        leader = ensure_bytes(nvim, leader)
191
        return _resolve(nvim, leader)
192
    return inner
193