1
- """implementations of simple readline control sequences
1
+ """implementations of simple readline edit operations
2
2
3
3
just the ones that fit the model of transforming the current line
4
4
and the cursor location
5
- in the order of description at http://www.bigsmoke.us/readline/shortcuts"""
5
+ based on http://www.bigsmoke.us/readline/shortcuts"""
6
6
7
7
import re
8
- char_sequences = {}
8
+ import inspect
9
9
10
10
INDENT = 4
11
11
12
12
#TODO Allow user config of keybindings for these actions
13
13
14
- def on (seq ):
15
- def add_to_char_sequences (func ):
16
- char_sequences [seq ] = func
17
- return func
18
- return add_to_char_sequences
19
-
20
- @on ('<Ctrl-b>' )
21
- @on ('<LEFT>' )
14
+ class AbstractEdits (object ):
15
+
16
+ default_kwargs = {
17
+ 'line' : 'hello world' ,
18
+ 'cursor_offset' : 5 ,
19
+ 'cut_buffer' : 'there' ,
20
+ 'indent' : 4 ,
21
+ }
22
+
23
+ def __contains__ (self , key ):
24
+ try :
25
+ self [key ]
26
+ except KeyError :
27
+ return False
28
+ else :
29
+ return True
30
+
31
+ def add (self , key , func ):
32
+ if key in self :
33
+ raise ValueError ('key %r already has a mapping' % (key ,))
34
+ params = inspect .getargspec (func )[0 ]
35
+ args = dict ((k , v ) for k , v in self .default_kwargs .items () if k in params )
36
+ r = func (** args )
37
+ if len (r ) == 2 :
38
+ self .simple_edits [key ] = func
39
+ elif len (r ) == 3 :
40
+ self .cut_buffer_edits [key ] = func
41
+ else :
42
+ raise ValueError ('return type of function %r not recognized' % (func ,))
43
+
44
+ def add_config_attr (self , config_attr , func ):
45
+ if config_attr in self .awaiting_config :
46
+ raise ValueError ('config attrribute %r already has a mapping' % (config_attr ,))
47
+ self .awaiting_config [config_attr ] = func
48
+
49
+ def call (self , key , ** kwargs ):
50
+ func = self [key ]
51
+ params = inspect .getargspec (func )[0 ]
52
+ args = dict ((k , v ) for k , v in kwargs .items () if k in params )
53
+ return func (** args )
54
+
55
+ def __getitem__ (self , key ):
56
+ if key in self .simple_edits : return self .simple_edits [key ]
57
+ if key in self .cut_buffer_edits : return self .cut_buffer_edits [key ]
58
+ raise KeyError ("key %r not mapped" % (key ,))
59
+
60
+
61
+ class UnconfiguredEdits (AbstractEdits ):
62
+ """Maps key to edit functions, and bins them by what parameters they take.
63
+
64
+ Only functions with specific signatures can be added:
65
+ * func(**kwargs) -> cursor_offset, line
66
+ * func(**kwargs) -> cursor_offset, line, cut_buffer
67
+ where kwargs are in among the keys of Edits.default_kwargs
68
+ These functions will be run to determine their return type, so no side effects!
69
+
70
+ More concrete Edits instances can be created by applying a config with
71
+ Edits.mapping_with_config() - this creates a new Edits instance
72
+ that uses a config file to assign config_attr bindings.
73
+
74
+ Keys can't be added twice, config attributes can't be added twice.
75
+ """
76
+
77
+ def __init__ (self ):
78
+ self .simple_edits = {}
79
+ self .cut_buffer_edits = {}
80
+ self .awaiting_config = {}
81
+
82
+ def mapping_with_config (self , config , key_dispatch ):
83
+ """Creates a new mapping object by applying a config object"""
84
+ return ConfiguredEdits (self .simple_edits , self .cut_buffer_edits ,
85
+ self .awaiting_config , config , key_dispatch )
86
+
87
+ def on (self , key = None , config = None ):
88
+ if (key is None and config is None or
89
+ key is not None and config is not None ):
90
+ raise ValueError ("Must use exactly one of key, config" )
91
+ if key is not None :
92
+ def add_to_keybinds (func ):
93
+ self .add (key , func )
94
+ return func
95
+ return add_to_keybinds
96
+ else :
97
+ def add_to_config (func ):
98
+ self .add_config_attr (config , func )
99
+ return func
100
+ return add_to_config
101
+
102
+ class ConfiguredEdits (AbstractEdits ):
103
+ def __init__ (self , simple_edits , cut_buffer_edits , awaiting_config , config , key_dispatch ):
104
+ self .simple_edits = dict (simple_edits )
105
+ self .cut_buffer_edits = dict (cut_buffer_edits )
106
+ for attr , func in awaiting_config .items ():
107
+ super (ConfiguredEdits , self ).add (key_dispatch [getattr (config , attr )], func )
108
+
109
+ def add_config_attr (self , config_attr , func ):
110
+ raise NotImplementedError ("Config already set on this mapping" )
111
+
112
+ def add (self , key , func ):
113
+ raise NotImplementedError ("Config already set on this mapping" )
114
+
115
+ edit_keys = UnconfiguredEdits ()
116
+
117
+ # Because the edits.on decorator runs the functions, functions which depend
118
+ # on other functions must be declared after their dependencies
119
+
120
+ @edit_keys .on ('<Ctrl-b>' )
121
+ @edit_keys .on ('<LEFT>' )
22
122
def left_arrow (cursor_offset , line ):
23
123
return max (0 , cursor_offset - 1 ), line
24
124
25
- @on ('<Ctrl-f>' )
26
- @on ('<RIGHT>' )
125
+ @edit_keys . on ('<Ctrl-f>' )
126
+ @edit_keys . on ('<RIGHT>' )
27
127
def right_arrow (cursor_offset , line ):
28
128
return min (len (line ), cursor_offset + 1 ), line
29
129
30
- @on ('<Ctrl-a>' )
31
- @on ('<HOME>' )
130
+ @edit_keys . on ('<Ctrl-a>' )
131
+ @edit_keys . on ('<HOME>' )
32
132
def beginning_of_line (cursor_offset , line ):
33
133
return 0 , line
34
134
35
- @on ('<Ctrl-e>' )
36
- @on ('<END>' )
135
+ @edit_keys . on ('<Ctrl-e>' )
136
+ @edit_keys . on ('<END>' )
37
137
def end_of_line (cursor_offset , line ):
38
138
return len (line ), line
39
139
40
- @on ('<Esc+f>' )
41
- @on ('<Ctrl-RIGHT>' )
42
- @on ('<Esc+RIGHT>' )
140
+ @edit_keys . on ('<Esc+f>' )
141
+ @edit_keys . on ('<Ctrl-RIGHT>' )
142
+ @edit_keys . on ('<Esc+RIGHT>' )
43
143
def forward_word (cursor_offset , line ):
44
144
patt = r"\S\s"
45
145
match = re .search (patt , line [cursor_offset :]+ ' ' )
46
146
delta = match .end () - 1 if match else 0
47
147
return (cursor_offset + delta , line )
48
148
49
- @on ('<Esc+b>' )
50
- @on ('<Ctrl-LEFT>' )
51
- @on ('<Esc+LEFT>' )
52
- def back_word (cursor_offset , line ):
53
- return (last_word_pos (line [:cursor_offset ]), line )
54
-
55
149
def last_word_pos (string ):
56
150
"""returns the start index of the last word of given string"""
57
151
patt = r'\S\s'
58
152
match = re .search (patt , string [::- 1 ])
59
153
index = match and len (string ) - match .end () + 1
60
154
return index or 0
61
155
62
- @on ('<PADDELETE>' )
156
+ @edit_keys .on ('<Esc+b>' )
157
+ @edit_keys .on ('<Ctrl-LEFT>' )
158
+ @edit_keys .on ('<Esc+LEFT>' )
159
+ def back_word (cursor_offset , line ):
160
+ return (last_word_pos (line [:cursor_offset ]), line )
161
+
162
+ @edit_keys .on ('<PADDELETE>' )
63
163
def delete (cursor_offset , line ):
64
164
return (cursor_offset ,
65
165
line [:cursor_offset ] + line [cursor_offset + 1 :])
66
166
67
- @on ('<Ctrl-h>' )
68
- @on ('<BACKSPACE>' )
167
+ @edit_keys .on ('<Ctrl-h>' )
168
+ @edit_keys .on ('<BACKSPACE>' )
169
+ @edit_keys .on (config = 'delete_key' )
69
170
def backspace (cursor_offset , line ):
70
171
if cursor_offset == 0 :
71
172
return cursor_offset , line
@@ -76,59 +177,61 @@ def backspace(cursor_offset, line):
76
177
return (cursor_offset - 1 ,
77
178
line [:cursor_offset - 1 ] + line [cursor_offset :])
78
179
79
- @on ('<Ctrl-u>' )
180
+ @edit_keys .on ('<Ctrl-u>' )
181
+ @edit_keys .on (config = 'clear_line_key' )
80
182
def delete_from_cursor_back (cursor_offset , line ):
81
183
return 0 , line [cursor_offset :]
82
184
83
- @on ('<Ctrl-k>' )
84
- def delete_from_cursor_forward (cursor_offset , line ):
85
- return cursor_offset , line [:cursor_offset ]
86
-
87
- @on ('<Esc+d>' ) # option-d
185
+ @edit_keys .on ('<Esc+d>' ) # option-d
88
186
def delete_rest_of_word (cursor_offset , line ):
89
187
m = re .search (r'\w\b' , line [cursor_offset :])
90
188
if not m :
91
189
return cursor_offset , line
92
190
return cursor_offset , line [:cursor_offset ] + line [m .start ()+ cursor_offset + 1 :]
93
191
94
- @on ('<Ctrl-w>' )
192
+ @edit_keys .on ('<Ctrl-w>' )
193
+ @edit_keys .on (config = 'clear_word_key' )
95
194
def delete_word_to_cursor (cursor_offset , line ):
96
195
matches = list (re .finditer (r'\s\S' , line [:cursor_offset ]))
97
196
start = matches [- 1 ].start ()+ 1 if matches else 0
98
197
return start , line [:start ] + line [cursor_offset :]
99
198
100
- @on ('<Esc+y>' )
199
+ @edit_keys . on ('<Esc+y>' )
101
200
def yank_prev_prev_killed_text (cursor_offset , line ):
102
201
return cursor_offset , line #TODO Not implemented
103
202
104
- @on ('<Ctrl-t>' )
203
+ @edit_keys . on ('<Ctrl-t>' )
105
204
def transpose_character_before_cursor (cursor_offset , line ):
106
205
return (min (len (line ), cursor_offset + 1 ),
107
206
line [:cursor_offset - 1 ] +
108
207
(line [cursor_offset ] if len (line ) > cursor_offset else '' ) +
109
208
line [cursor_offset - 1 ] +
110
209
line [cursor_offset + 1 :])
111
210
112
- @on ('<Esc+t>' )
211
+ @edit_keys . on ('<Esc+t>' )
113
212
def transpose_word_before_cursor (cursor_offset , line ):
114
213
return cursor_offset , line #TODO Not implemented
115
214
116
215
# bonus functions (not part of readline)
117
216
118
- @on ('<Esc+r>' )
217
+ @edit_keys . on ('<Esc+r>' )
119
218
def delete_line (cursor_offset , line ):
120
219
return 0 , ""
121
220
122
- @on ('<Esc+u>' )
221
+ @edit_keys . on ('<Esc+u>' )
123
222
def uppercase_next_word (cursor_offset , line ):
124
223
return cursor_offset , line #TODO Not implemented
125
224
126
- @on ('<Esc+c>' )
225
+ @edit_keys .on ('<Ctrl-k>' )
226
+ def delete_from_cursor_forward (cursor_offset , line ):
227
+ return cursor_offset , line [:cursor_offset ]
228
+
229
+ @edit_keys .on ('<Esc+c>' )
127
230
def titlecase_next_word (cursor_offset , line ):
128
231
return cursor_offset , line #TODO Not implemented
129
232
130
- @on ('<Esc+BACKSPACE>' )
131
- @on ('<Meta-BACKSPACE>' )
233
+ @edit_keys . on ('<Esc+BACKSPACE>' )
234
+ @edit_keys . on ('<Meta-BACKSPACE>' )
132
235
def delete_word_from_cursor_back (cursor_offset , line ):
133
236
"""Whatever my option-delete does in bash on my mac"""
134
237
if not line :
@@ -138,10 +241,3 @@ def delete_word_from_cursor_back(cursor_offset, line):
138
241
return starts [- 1 ], line [:starts [- 1 ]] + line [cursor_offset :]
139
242
return cursor_offset , line
140
243
141
- def get_updated_char_sequences (key_dispatch , config ):
142
- updated_char_sequences = dict (char_sequences )
143
- updated_char_sequences [key_dispatch [config .delete_key ]] = backspace
144
- updated_char_sequences [key_dispatch [config .clear_word_key ]] = delete_word_to_cursor
145
- updated_char_sequences [key_dispatch [config .clear_line_key ]] = delete_from_cursor_back
146
- return updated_char_sequences
147
-
0 commit comments