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