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
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>' )
22122def left_arrow (cursor_offset , line ):
23123 return max (0 , cursor_offset - 1 ), line
24124
25- @on ('<Ctrl-f>' )
26- @on ('<RIGHT>' )
125+ @edit_keys . on ('<Ctrl-f>' )
126+ @edit_keys . on ('<RIGHT>' )
27127def right_arrow (cursor_offset , line ):
28128 return min (len (line ), cursor_offset + 1 ), line
29129
30- @on ('<Ctrl-a>' )
31- @on ('<HOME>' )
130+ @edit_keys . on ('<Ctrl-a>' )
131+ @edit_keys . on ('<HOME>' )
32132def beginning_of_line (cursor_offset , line ):
33133 return 0 , line
34134
35- @on ('<Ctrl-e>' )
36- @on ('<END>' )
135+ @edit_keys . on ('<Ctrl-e>' )
136+ @edit_keys . on ('<END>' )
37137def end_of_line (cursor_offset , line ):
38138 return len (line ), line
39139
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>' )
43143def forward_word (cursor_offset , line ):
44144 patt = r"\S\s"
45145 match = re .search (patt , line [cursor_offset :]+ ' ' )
46146 delta = match .end () - 1 if match else 0
47147 return (cursor_offset + delta , line )
48148
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-
55149def last_word_pos (string ):
56150 """returns the start index of the last word of given string"""
57151 patt = r'\S\s'
58152 match = re .search (patt , string [::- 1 ])
59153 index = match and len (string ) - match .end () + 1
60154 return index or 0
61155
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>' )
63163def delete (cursor_offset , line ):
64164 return (cursor_offset ,
65165 line [:cursor_offset ] + line [cursor_offset + 1 :])
66166
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' )
69170def backspace (cursor_offset , line ):
70171 if cursor_offset == 0 :
71172 return cursor_offset , line
@@ -76,59 +177,61 @@ def backspace(cursor_offset, line):
76177 return (cursor_offset - 1 ,
77178 line [:cursor_offset - 1 ] + line [cursor_offset :])
78179
79- @on ('<Ctrl-u>' )
180+ @edit_keys .on ('<Ctrl-u>' )
181+ @edit_keys .on (config = 'clear_line_key' )
80182def delete_from_cursor_back (cursor_offset , line ):
81183 return 0 , line [cursor_offset :]
82184
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
88186def delete_rest_of_word (cursor_offset , line ):
89187 m = re .search (r'\w\b' , line [cursor_offset :])
90188 if not m :
91189 return cursor_offset , line
92190 return cursor_offset , line [:cursor_offset ] + line [m .start ()+ cursor_offset + 1 :]
93191
94- @on ('<Ctrl-w>' )
192+ @edit_keys .on ('<Ctrl-w>' )
193+ @edit_keys .on (config = 'clear_word_key' )
95194def delete_word_to_cursor (cursor_offset , line ):
96195 matches = list (re .finditer (r'\s\S' , line [:cursor_offset ]))
97196 start = matches [- 1 ].start ()+ 1 if matches else 0
98197 return start , line [:start ] + line [cursor_offset :]
99198
100- @on ('<Esc+y>' )
199+ @edit_keys . on ('<Esc+y>' )
101200def yank_prev_prev_killed_text (cursor_offset , line ):
102201 return cursor_offset , line #TODO Not implemented
103202
104- @on ('<Ctrl-t>' )
203+ @edit_keys . on ('<Ctrl-t>' )
105204def transpose_character_before_cursor (cursor_offset , line ):
106205 return (min (len (line ), cursor_offset + 1 ),
107206 line [:cursor_offset - 1 ] +
108207 (line [cursor_offset ] if len (line ) > cursor_offset else '' ) +
109208 line [cursor_offset - 1 ] +
110209 line [cursor_offset + 1 :])
111210
112- @on ('<Esc+t>' )
211+ @edit_keys . on ('<Esc+t>' )
113212def transpose_word_before_cursor (cursor_offset , line ):
114213 return cursor_offset , line #TODO Not implemented
115214
116215# bonus functions (not part of readline)
117216
118- @on ('<Esc+r>' )
217+ @edit_keys . on ('<Esc+r>' )
119218def delete_line (cursor_offset , line ):
120219 return 0 , ""
121220
122- @on ('<Esc+u>' )
221+ @edit_keys . on ('<Esc+u>' )
123222def uppercase_next_word (cursor_offset , line ):
124223 return cursor_offset , line #TODO Not implemented
125224
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>' )
127230def titlecase_next_word (cursor_offset , line ):
128231 return cursor_offset , line #TODO Not implemented
129232
130- @on ('<Esc+BACKSPACE>' )
131- @on ('<Meta-BACKSPACE>' )
233+ @edit_keys . on ('<Esc+BACKSPACE>' )
234+ @edit_keys . on ('<Meta-BACKSPACE>' )
132235def delete_word_from_cursor_back (cursor_offset , line ):
133236 """Whatever my option-delete does in bash on my mac"""
134237 if not line :
@@ -138,10 +241,3 @@ def delete_word_from_cursor_back(cursor_offset, line):
138241 return starts [- 1 ], line [:starts [- 1 ]] + line [cursor_offset :]
139242 return cursor_offset , line
140243
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