File: readlike.py

package info (click to toggle)
readlike 0.1.3-3
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 116 kB
  • sloc: python: 152; makefile: 6
file content (279 lines) | stat: -rw-r--r-- 8,484 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
"""
GNU Readline-like line editing functions

Only two public functions are exposed:

    - edit(), which returns the result of a key input on a line
    - keys(), which returns key names that correspond to commands

Private functions corresponding to each implemented Readline command are
accessible by replacing hyphens in the command name with underscores and
prefixing an underscore, as in _end_of_line() for `end-of-line'.

This module is especially well-suited to interfacing with Urwid, since
the edit() function's `key' argument uses the same format that Urwid
widget keypress() methods receive.

Implemented commands and their correspondings keys are as follows:

    backward-char            ctrl b, left
    backward-delete-char     ctrl h, backspace
    backward-kill-word       ctrl meta h, meta backspace
    backward-word            meta b, meta left
    beginning-of-line        ctrl a, home
    capitalize-word          meta c
    delete-char              ctrl d, delete
    delete-horizontal-space  meta \\
    downcase-word            meta l
    end-of-line              ctrl e, end
    forward-char             ctrl f, right
    forward-word             meta f, meta right
    kill-line                ctrl k
    kill-word                meta d, meta delete
    transpose-chars          ctrl t
    transpose-words          meta t
    unix-line-discard        ctrl u
    unix-word-rubout         ctrl w
    upcase-word              meta u

For more information about each command, see readline(3) or use pydoc on
the specific private functions.
"""

__all__ = ['edit', 'keys']


def _backward_char(text, pos):
    """Move pos back a character."""
    return text, max(0, pos - 1)


def _backward_delete_char(text, pos):
    """Delete the character behind pos."""
    if pos == 0:
        return text, pos
    return text[:pos - 1] + text[pos:], pos - 1


def _backward_kill_word(text, pos):
    """"
    Kill the word behind pos. Word boundaries are the same as those
    used by _backward_word.
    """
    text, new_pos = _backward_word(text, pos)
    return text[:new_pos] + text[pos:], new_pos


def _backward_word(text, pos):
    """
    Move pos back to the start of the current or previous word. Words
    are composed of alphanumeric characters (letters and digits).
    """
    while pos > 0 and not text[pos - 1].isalnum():
        pos -= 1
    while pos > 0 and text[pos - 1].isalnum():
        pos -= 1
    return text, pos


def _beginning_of_line(text, pos):
    """Move pos to the start of text."""
    return text, 0


def _capitalize_word(text, pos):
    """Capitalize the current (or following) word."""
    while pos < len(text) and not text[pos].isalnum():
        pos += 1
    if pos < len(text):
        text = text[:pos] + text[pos].upper() + text[pos + 1:]
    while pos < len(text) and text[pos].isalnum():
        pos += 1
    return text, pos


def _delete_char(text, pos):
    """Delete the character at pos."""
    return text[:pos] + text[pos + 1:], pos


def _delete_horizontal_space(text, pos):
    """Delete all spaces and tabs around pos."""
    while pos > 0 and text[pos - 1].isspace():
        pos -= 1
    end_pos = pos
    while end_pos < len(text) and text[end_pos].isspace():
        end_pos += 1
    return text[:pos] + text[end_pos:], pos


def _downcase_word(text, pos):
    """Lowercase the current (or following) word."""
    text, new_pos = _forward_word(text, pos)
    return text[:pos] + text[pos:new_pos].lower() + text[new_pos:], new_pos


def _end_of_line(text, pos):
    """Move pos to the end of text."""
    return text, len(text)


def _forward_char(text, pos):
    """Move pos forward a character."""
    return text, min(pos + 1, len(text))


def _forward_word(text, pos):
    """
    Move pos forward to the end of the next word. Words are composed of
    alphanumeric characters (letters and digits).
    """
    while pos < len(text) and not text[pos].isalnum():
        pos += 1
    while pos < len(text) and text[pos].isalnum():
        pos += 1
    return text, pos


def _kill_line(text, pos):
    """Kill from pos to the end of text."""
    return text[:pos], pos


def _kill_word(text, pos):
    """
    Kill from pos to the end of the current word, or if between words,
    to the end of the next word. Word boundaries are the same as those
    used by _forward_word.
    """
    text, end_pos = _forward_word(text, pos)
    return text[:pos] + text[end_pos:], pos


def _transpose_chars(text, pos):
    """
    Drag the character before pos forward over the character at pos,
    moving pos forward as well. If pos is at the end of text, then this
    transposes the two characters before pos.
    """
    if len(text) < 2 or pos == 0:
        return text, pos
    if pos == len(text):
        return text[:pos - 2] + text[pos - 1] + text[pos - 2], pos
    return text[:pos - 1] + text[pos] + text[pos - 1] + text[pos + 1:], pos + 1


def _transpose_words(text, pos):
    """
    Drag the word before pos past the word after pos, moving pos over
    that word as well. If pos is at the end of text, this transposes the
    last two words in text.
    """
    text, end2 = _forward_word(text, pos)
    text, start2 = _backward_word(text, end2)
    text, start1 = _backward_word(text, start2)
    text, end1 = _forward_word(text, start1)
    if start1 == start2:
        return text, pos
    return text[:start1] + text[start2:end2] + text[end1:start2:] + \
        text[start1:end1] + text[end2:], end2


def _unix_line_discard(text, pos):
    """Kill backward from pos to the beginning of text."""
    return text[pos:], 0


def _unix_word_rubout(text, pos):
    """
    Kill the word behind pos, using white space as a word boundary.
    """
    words = text[:pos].rsplit(None, 1)
    if len(words) < 2:
        return text[pos:], 0
    else:
        index = text.rfind(words[1], 0, pos)
        return text[:index] + text[pos:], index


def _upcase_word(text, pos):
    """Uppercase the current (or following) word."""
    text, new_pos = _forward_word(text, pos)
    return text[:pos] + text[pos:new_pos].upper() + text[new_pos:], new_pos


_key_bindings = {
    # TODO: C-I/tab (complete) - 0.2.0
    # TODO: C-N (next-history) - 0.2.0
    # TODO: C-P (previous-history) - 0.2.0
    # TODO: C-Y (yank) - 0.3.0
    # TODO: M-* (insert-completions) - 0.2.0
    # TODO: M-. (yank-last-arg) - 0.3.0
    # TODO: M-< (beginning-of-history) - 0.2.0
    # TODO: M-> (end-of-history) - 0.2.0
    # TODO: M-C-I (tab-insert) - 0.2.0
    # TODO: M-C-Y (yank-nth-arg) - 0.3.0
    # TODO: M-C-[ (complete) - 0.2.0
    # TODO: M-Y (yank-pop) - 0.3.0
    # TODO: M-_ (yank-last-arg) - 0.3.0
    'backspace': _backward_delete_char,
    'ctrl a': _beginning_of_line,
    'ctrl b': _backward_char,
    'ctrl d': _delete_char,
    'ctrl e': _end_of_line,
    'ctrl f': _forward_char,
    'ctrl h': _backward_delete_char,
    'ctrl k': _kill_line,
    'ctrl meta h': _backward_kill_word,
    'ctrl t': _transpose_chars,
    'ctrl u': _unix_line_discard,
    'ctrl w': _unix_word_rubout,
    'delete': _delete_char,
    'end': _end_of_line,
    'home': _beginning_of_line,
    'left': _backward_char,
    'meta \\': _delete_horizontal_space,
    'meta b': _backward_word,
    'meta backspace': _backward_kill_word,
    'meta c': _capitalize_word,
    'meta d': _kill_word,
    'meta delete': _kill_word,
    'meta f': _forward_word,
    'meta l': _downcase_word,
    'meta left': _backward_word,
    'meta right': _forward_word,
    'meta t': _transpose_words,
    'meta u': _upcase_word,
    'right': _forward_char,
}

_keys = frozenset(_key_bindings.keys())


def edit(text, pos, key):
    """
    Process a key input in the context of a line, and return the
    resulting text and cursor position.

    `text' and `key' must be of type str or unicode, and `pos' must be
    an int in the range [0, len(text)].

    If `key' is in keys(), the corresponding command is executed on the
    line. Otherwise, if `key' is a single character, that character is
    inserted at the cursor position. If neither condition is met, `text'
    and `pos' are returned unmodified.
    """
    if key in _key_bindings:
        return _key_bindings[key](text, pos)
    elif len(key) == 1:
        return text[:pos] + key + text[pos:], pos + 1
    else:
        return text, pos


def keys():
    """
    Return a frozenset of strings that describe key inputs corresponding
    to line editing commands.
    """
    return _keys