1- """implementations of simple readline control sequences
1+ """implementations of simple readline edit operations
22
33just the ones that fit the model of transforming the current line
44and 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"""
66
77import re
8- char_sequences = {}
8+ import inspect
99
1010INDENT = 4
1111
1212#TODO Allow user config of keybindings for these actions
1313
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 ):
1915
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>' )
22146def left_arrow (cursor_offset , line ):
23147 return max (0 , cursor_offset - 1 ), line
24148
25- @on ('<Ctrl-f>' )
26- @on ('<RIGHT>' )
149+ @edit_keys . on ('<Ctrl-f>' )
150+ @edit_keys . on ('<RIGHT>' )
27151def right_arrow (cursor_offset , line ):
28152 return min (len (line ), cursor_offset + 1 ), line
29153
30- @on ('<Ctrl-a>' )
31- @on ('<HOME>' )
154+ @edit_keys . on ('<Ctrl-a>' )
155+ @edit_keys . on ('<HOME>' )
32156def beginning_of_line (cursor_offset , line ):
33157 return 0 , line
34158
35- @on ('<Ctrl-e>' )
36- @on ('<END>' )
159+ @edit_keys . on ('<Ctrl-e>' )
160+ @edit_keys . on ('<END>' )
37161def end_of_line (cursor_offset , line ):
38162 return len (line ), line
39163
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>' )
43167def forward_word (cursor_offset , line ):
44168 patt = r"\S\s"
45169 match = re .search (patt , line [cursor_offset :]+ ' ' )
46170 delta = match .end () - 1 if match else 0
47171 return (cursor_offset + delta , line )
48172
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-
55173def last_word_pos (string ):
56174 """returns the start index of the last word of given string"""
57175 patt = r'\S\s'
58176 match = re .search (patt , string [::- 1 ])
59177 index = match and len (string ) - match .end () + 1
60178 return index or 0
61179
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>' )
63187def delete (cursor_offset , line ):
64188 return (cursor_offset ,
65189 line [:cursor_offset ] + line [cursor_offset + 1 :])
66190
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' )
69194def backspace (cursor_offset , line ):
70195 if cursor_offset == 0 :
71196 return cursor_offset , line
@@ -76,72 +201,77 @@ def backspace(cursor_offset, line):
76201 return (cursor_offset - 1 ,
77202 line [:cursor_offset - 1 ] + line [cursor_offset :])
78203
79- @on ('<Ctrl-u>' )
204+ @edit_keys .on ('<Ctrl-u>' )
205+ @edit_keys .on (config = 'clear_line_key' )
80206def delete_from_cursor_back (cursor_offset , line ):
81207 return 0 , line [cursor_offset :]
82208
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
88211def delete_rest_of_word (cursor_offset , line ):
89212 m = re .search (r'\w\b' , line [cursor_offset :])
90213 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 ])
93217
94- @on ('<Ctrl-w>' )
218+ @edit_keys .on ('<Ctrl-w>' )
219+ @edit_keys .on (config = 'clear_word_key' )
220+ @kills_behind
95221def delete_word_to_cursor (cursor_offset , line ):
96222 matches = list (re .finditer (r'\s\S' , line [:cursor_offset ]))
97223 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 ]
99225
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 :]
103229
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>' )
105235def transpose_character_before_cursor (cursor_offset , line ):
106236 return (min (len (line ), cursor_offset + 1 ),
107237 line [:cursor_offset - 1 ] +
108238 (line [cursor_offset ] if len (line ) > cursor_offset else '' ) +
109239 line [cursor_offset - 1 ] +
110240 line [cursor_offset + 1 :])
111241
112- @on ('<Esc+t>' )
242+ @edit_keys . on ('<Esc+t>' )
113243def transpose_word_before_cursor (cursor_offset , line ):
114244 return cursor_offset , line #TODO Not implemented
115245
116246# bonus functions (not part of readline)
117247
118- @on ('<Esc+r>' )
248+ @edit_keys . on ('<Esc+r>' )
119249def delete_line (cursor_offset , line ):
120250 return 0 , ""
121251
122- @on ('<Esc+u>' )
252+ @edit_keys . on ('<Esc+u>' )
123253def uppercase_next_word (cursor_offset , line ):
124254 return cursor_offset , line #TODO Not implemented
125255
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>' )
127262def titlecase_next_word (cursor_offset , line ):
128263 return cursor_offset , line #TODO Not implemented
129264
130- @on ('<Esc+BACKSPACE>' )
131- @on ('<Meta-BACKSPACE>' )
265+ @edit_keys .on ('<Esc+BACKSPACE>' )
266+ @edit_keys .on ('<Meta-BACKSPACE>' )
267+ @kills_behind
132268def delete_word_from_cursor_back (cursor_offset , line ):
133269 """Whatever my option-delete does in bash on my mac"""
134270 if not line :
135271 return cursor_offset , line
136272 starts = [m .start () for m in list (re .finditer (r'\b\w' , line )) if m .start () < cursor_offset ]
137273 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+
147277
0 commit comments