##// END OF EJS Templates
Proper redirection of keystrokes to subprocesses.
Gael Varoquaux -
Show More
@@ -1,55 +1,55 b''
1 1 # encoding: utf-8
2 2 """
3 3 Object for encapsulating process execution by using callbacks for stdout,
4 4 stderr and stdin.
5 5 """
6 6 __docformat__ = "restructuredtext en"
7 7
8 8 #-------------------------------------------------------------------------------
9 9 # Copyright (C) 2008 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-------------------------------------------------------------------------------
14 14
15 15 #-------------------------------------------------------------------------------
16 16 # Imports
17 17 #-------------------------------------------------------------------------------
18 18 from killable_process import Popen, PIPE
19 19 from threading import Thread
20 20 from time import sleep
21 21
22 22 class PipedProcess(Thread):
23 23
24 24 def __init__(self, command_string, out_callback,
25 25 end_callback=None,):
26 26 self.command_string = command_string
27 27 self.out_callback = out_callback
28 28 self.end_callback = end_callback
29 29 Thread.__init__(self)
30 30
31 31
32 32 def run(self):
33 33 """ Start the process and hook up the callbacks.
34 34 """
35 35 process = Popen((self.command_string + ' 2>&1', ), shell=True,
36 36 universal_newlines=True,
37 stdout=PIPE, stdin=PIPE)
37 stdout=PIPE, stdin=PIPE, )
38 38 self.process = process
39 39 while True:
40 40 out_char = process.stdout.read(1)
41 41 if out_char == '':
42 42 if process.poll() is not None:
43 43 # The process has finished
44 44 break
45 45 else:
46 46 # The process is not giving any interesting
47 47 # output. No use polling it immediatly.
48 48 sleep(0.1)
49 49 else:
50 50 self.out_callback(out_char)
51 51
52 52 if self.end_callback is not None:
53 53 self.end_callback()
54 54
55 55
@@ -1,75 +1,77 b''
1 1 """
2 2 Entry point for a simple application giving a graphical frontend to
3 3 ipython.
4 4 """
5 5
6 6 import wx
7 7 from wx_frontend import WxController
8 8 import __builtin__
9 9
10 10 class IPythonXController(WxController):
11 11 """ Sub class of WxController that adds some application-specific
12 12 bindings.
13 13 """
14 14
15 debug = False
16
15 17 def __init__(self, *args, **kwargs):
16 18 WxController.__init__(self, *args, **kwargs)
17 19 self.ipython0.ask_exit = self.do_exit
18 20
19 21
20 22 def _on_key_down(self, event, skip=True):
21 23 # Intercept Ctrl-D to quit
22 24 if event.KeyCode == ord('D') and event.ControlDown() and \
23 25 self.get_current_edit_buffer()=='' and \
24 26 self._input_state == 'readline':
25 27 wx.CallAfter(self.ask_exit)
26 28 else:
27 29 WxController._on_key_down(self, event, skip=skip)
28 30
29 31
30 32 def ask_exit(self):
31 33 """ Ask the user whether to exit.
32 34 """
33 35 self.write('\n')
34 36 self.capture_output()
35 37 self.ipython0.shell.exit()
36 38 self.release_output()
37 39 wx.Yield()
38 40 if not self.ipython0.exit_now:
39 41 self.new_prompt(self.prompt % (self.last_result['number'] + 1))
40 42
41 43
42 44 def do_exit(self):
43 45 """ Exits the interpreter, kills the windows.
44 46 """
45 47 WxController.do_exit(self)
46 48 self.release_output()
47 49 wx.CallAfter(wx.Exit)
48 50
49 51
50 52
51 53 class IPythonX(wx.Frame):
52 54 """ Main frame of the IPythonX app.
53 55 """
54 56
55 57 def __init__(self, parent, id, title):
56 58 wx.Frame.__init__(self, parent, id, title, size=(300,250))
57 59 self._sizer = wx.BoxSizer(wx.VERTICAL)
58 60 self.shell = IPythonXController(self)
59 61 self._sizer.Add(self.shell, 1, wx.EXPAND)
60 62 self.SetSizer(self._sizer)
61 63 self.SetAutoLayout(1)
62 64 self.Show(True)
63 65
64 66
65 67 def main():
66 68 app = wx.PySimpleApp()
67 69 frame = IPythonX(None, wx.ID_ANY, 'IPythonX')
68 70 frame.shell.SetFocus()
69 71 frame.shell.app = app
70 72 frame.SetSize((680, 460))
71 73
72 74 app.MainLoop()
73 75
74 76 if __name__ == '__main__':
75 77 main()
@@ -1,398 +1,407 b''
1 1 # encoding: utf-8 -*- test-case-name:
2 2 # FIXME: Need to add tests.
3 3 # ipython1.frontend.cocoa.tests.test_cocoa_frontend -*-
4 4
5 5 """Classes to provide a Wx frontend to the
6 6 IPython.kernel.core.interpreter.
7 7
8 8 """
9 9
10 10 __docformat__ = "restructuredtext en"
11 11
12 12 #-------------------------------------------------------------------------------
13 13 # Copyright (C) 2008 The IPython Development Team
14 14 #
15 15 # Distributed under the terms of the BSD License. The full license is in
16 16 # the file COPYING, distributed as part of this software.
17 17 #-------------------------------------------------------------------------------
18 18
19 19 #-------------------------------------------------------------------------------
20 20 # Imports
21 21 #-------------------------------------------------------------------------------
22 22
23 23
24 24 import wx
25 25 import re
26 26 from wx import stc
27 27 from console_widget import ConsoleWidget
28 28 import __builtin__
29 29 from time import sleep
30 30 import sys
31 31 import signal
32 32
33 33 from threading import Lock
34 34
35 35 from IPython.frontend.piped_process import PipedProcess
36 36 from IPython.frontend.prefilterfrontend import PrefilterFrontEnd
37 37
38 38 #_COMMAND_BG = '#FAFAF1' # Nice green
39 39 _RUNNING_BUFFER_BG = '#FDFFD3' # Nice yellow
40 40 _ERROR_BG = '#FFF1F1' # Nice red
41 41
42 42 _RUNNING_BUFFER_MARKER = 31
43 43 _ERROR_MARKER = 30
44 44
45 45 #-------------------------------------------------------------------------------
46 46 # Classes to implement the Wx frontend
47 47 #-------------------------------------------------------------------------------
48 48 class WxController(PrefilterFrontEnd, ConsoleWidget):
49 49
50 50 output_prompt = \
51 51 '\x01\x1b[0;31m\x02Out[\x01\x1b[1;31m\x02%i\x01\x1b[0;31m\x02]: \x01\x1b[0m\x02'
52 52
53 53 # Print debug info on what is happening to the console.
54 54 debug = True
55 55
56 56 # The title of the terminal, as captured through the ANSI escape
57 57 # sequences.
58 58
59 59 def _set_title(self, title):
60 60 return self.Parent.SetTitle(title)
61 61
62 62 def _get_title(self):
63 63 return self.Parent.GetTitle()
64 64
65 65 title = property(_get_title, _set_title)
66 66
67 67 #--------------------------------------------------------------------------
68 68 # Private Attributes
69 69 #--------------------------------------------------------------------------
70 70
71 71 # A flag governing the behavior of the input. Can be:
72 72 #
73 73 # 'readline' for readline-like behavior with a prompt
74 74 # and an edit buffer.
75 75 # 'subprocess' for sending the raw input directly to a
76 76 # subprocess.
77 77 # 'buffering' for buffering of the input, that will be used
78 78 # when the input state switches back to another state.
79 79 _input_state = 'readline'
80 80
81 81 # Attribute to store reference to the pipes of a subprocess, if we
82 82 # are running any.
83 83 _running_process = False
84 84
85 85 # A queue for writing fast streams to the screen without flooding the
86 86 # event loop
87 87 _out_buffer = []
88 88
89 89 # A lock to lock the _out_buffer to make sure we don't empty it
90 90 # while it is being swapped
91 91 _out_buffer_lock = Lock()
92 92
93 93 #--------------------------------------------------------------------------
94 94 # Public API
95 95 #--------------------------------------------------------------------------
96 96
97 97 def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
98 98 size=wx.DefaultSize, style=wx.CLIP_CHILDREN,
99 99 *args, **kwds):
100 100 """ Create Shell instance.
101 101 """
102 102 ConsoleWidget.__init__(self, parent, id, pos, size, style)
103 103 PrefilterFrontEnd.__init__(self)
104 104
105 105 # Marker for running buffer.
106 106 self.MarkerDefine(_RUNNING_BUFFER_MARKER, stc.STC_MARK_BACKGROUND,
107 107 background=_RUNNING_BUFFER_BG)
108 108 # Marker for tracebacks.
109 109 self.MarkerDefine(_ERROR_MARKER, stc.STC_MARK_BACKGROUND,
110 110 background=_ERROR_BG)
111 111
112 112 # A time for flushing the write buffer
113 113 BUFFER_FLUSH_TIMER_ID = 100
114 114 self._buffer_flush_timer = wx.Timer(self, BUFFER_FLUSH_TIMER_ID)
115 115 wx.EVT_TIMER(self, BUFFER_FLUSH_TIMER_ID, self._buffer_flush)
116 116
117 117 def do_completion(self):
118 118 """ Do code completion.
119 119 """
120 120 if self.debug:
121 121 print >>sys.__stdout__, "do_completion",
122 122 line = self.get_current_edit_buffer()
123 123 new_line, completions = self.complete(line)
124 124 if len(completions)>1:
125 125 self.write_completion(completions)
126 126 self.replace_current_edit_buffer(new_line)
127 127 if self.debug:
128 128 print >>sys.__stdout__, completions
129 129
130 130
131 131 def do_calltip(self):
132 132 if self.debug:
133 133 print >>sys.__stdout__, "do_calltip"
134 134 separators = re.compile('[\s\{\}\[\]\(\)\= ,:]')
135 135 symbol = self.get_current_edit_buffer()
136 136 symbol_string = separators.split(symbol)[-1]
137 137 base_symbol_string = symbol_string.split('.')[0]
138 138 if base_symbol_string in self.shell.user_ns:
139 139 symbol = self.shell.user_ns[base_symbol_string]
140 140 elif base_symbol_string in self.shell.user_global_ns:
141 141 symbol = self.shell.user_global_ns[base_symbol_string]
142 142 elif base_symbol_string in __builtin__.__dict__:
143 143 symbol = __builtin__.__dict__[base_symbol_string]
144 144 else:
145 145 return False
146 146 for name in symbol_string.split('.')[1:] + ['__doc__']:
147 147 symbol = getattr(symbol, name)
148 148 try:
149 149 self.AutoCompCancel()
150 150 wx.Yield()
151 151 self.CallTipShow(self.GetCurrentPos(), symbol)
152 152 except:
153 153 # The retrieve symbol couldn't be converted to a string
154 154 pass
155 155
156 156
157 157 def popup_completion(self, create=False):
158 158 """ Updates the popup completion menu if it exists. If create is
159 159 true, open the menu.
160 160 """
161 161 if self.debug:
162 162 print >>sys.__stdout__, "popup_completion",
163 163 line = self.get_current_edit_buffer()
164 164 if (self.AutoCompActive() and not line[-1] == '.') \
165 165 or create==True:
166 166 suggestion, completions = self.complete(line)
167 167 offset=0
168 168 if completions:
169 169 complete_sep = re.compile('[\s\{\}\[\]\(\)\= ,:]')
170 170 residual = complete_sep.split(line)[-1]
171 171 offset = len(residual)
172 172 self.pop_completion(completions, offset=offset)
173 173 if self.debug:
174 174 print >>sys.__stdout__, completions
175 175
176 176
177 177 def new_prompt(self, prompt):
178 178 self._input_state = 'readline'
179 179 ConsoleWidget.new_prompt(self, prompt)
180 180
181 181
182 182 def raw_input(self, prompt):
183 183 """ A replacement from python's raw_input.
184 184 """
185 185 self.new_prompt(prompt)
186 186 self.waiting = True
187 187 self.__old_on_enter = self._on_enter
188 188 def my_on_enter():
189 189 self.waiting = False
190 190 self._on_enter = my_on_enter
191 191 # XXX: Busy waiting, ugly.
192 192 while self.waiting:
193 193 wx.Yield()
194 194 sleep(0.1)
195 195 self._on_enter = self.__old_on_enter
196 196 self._input_state = 'buffering'
197 197 return self.get_current_edit_buffer().rstrip('\n')
198 198
199 199
200 200 def execute(self, python_string, raw_string=None):
201 201 self._input_state = 'buffering'
202 202 self.CallTipCancel()
203 203 self._cursor = wx.BusyCursor()
204 204 if raw_string is None:
205 205 raw_string = python_string
206 206 end_line = self.current_prompt_line \
207 207 + max(1, len(raw_string.split('\n'))-1)
208 208 for i in range(self.current_prompt_line, end_line):
209 209 self.MarkerAdd(i, _RUNNING_BUFFER_MARKER)
210 210 # Update the display:
211 211 wx.Yield()
212 212 self.GotoPos(self.GetLength())
213 213 PrefilterFrontEnd.execute(self, python_string, raw_string=raw_string)
214 214
215 215
216 216 def capture_output(self):
217 217 self.__old_raw_input = __builtin__.raw_input
218 218 __builtin__.raw_input = self.raw_input
219 219 PrefilterFrontEnd.capture_output(self)
220 220
221 221
222 222 def release_output(self):
223 223 __builtin__.raw_input = self.__old_raw_input
224 224 PrefilterFrontEnd.capture_output(self)
225 225
226 226
227 227 def after_execute(self):
228 228 PrefilterFrontEnd.after_execute(self)
229 229 if hasattr(self, '_cursor'):
230 230 del self._cursor
231 231
232 232
233 233 def system_call(self, command_string):
234 234 self._input_state = 'subprocess'
235 235 self._running_process = PipedProcess(command_string,
236 236 out_callback=self.buffered_write,
237 237 end_callback = self._end_system_call)
238 238 self._running_process.start()
239 239 # XXX: another one of these polling loops to have a blocking
240 240 # call
241 241 wx.Yield()
242 242 while self._running_process:
243 243 wx.Yield()
244 244 sleep(0.1)
245 245 # Be sure to flush the buffer.
246 246 self._buffer_flush(event=None)
247 247
248 248
249 249 def buffered_write(self, text):
250 250 """ A write method for streams, that caches the stream in order
251 251 to avoid flooding the event loop.
252 252
253 253 This can be called outside of the main loop, in separate
254 254 threads.
255 255 """
256 256 self._out_buffer_lock.acquire()
257 257 self._out_buffer.append(text)
258 258 self._out_buffer_lock.release()
259 259 if not self._buffer_flush_timer.IsRunning():
260 260 wx.CallAfter(self._buffer_flush_timer.Start, 100) # milliseconds
261 261
262 262
263 263 def show_traceback(self):
264 264 start_line = self.GetCurrentLine()
265 265 PrefilterFrontEnd.show_traceback(self)
266 266 wx.Yield()
267 267 for i in range(start_line, self.GetCurrentLine()):
268 268 self.MarkerAdd(i, _ERROR_MARKER)
269 269
270 270
271 271 #--------------------------------------------------------------------------
272 272 # Private API
273 273 #--------------------------------------------------------------------------
274 274
275 275 def _on_key_down(self, event, skip=True):
276 276 """ Capture the character events, let the parent
277 277 widget handle them, and put our logic afterward.
278 278 """
279 print >>sys.__stderr__, event.KeyCode
280 279 current_line_number = self.GetCurrentLine()
281 280 if event.KeyCode in (ord('c'), ord('C')) and event.ControlDown():
282 281 # Capture Control-C
283 282 if self._input_state == 'subprocess':
284 283 if self.debug:
285 284 print >>sys.__stderr__, 'Killing running process'
286 285 self._running_process.process.kill()
287 286 elif self._input_state == 'buffering':
288 287 if self.debug:
289 288 print >>sys.__stderr__, 'Raising KeyboardException'
290 289 raise KeyboardException
291 290 # XXX: We need to make really sure we
292 291 # get back to a prompt.
293 elif self._input_state == 'subprocess' and event.KeyCode<256 \
294 and event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN):
292 elif self._input_state == 'subprocess' and (
293 ( event.KeyCode<256 and
294 event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN, wx.MOD_SHIFT))
295 or
296 ( event.KeyCode in (ord('d'), ord('D')) and
297 event.ControlDown())):
295 298 # We are running a process, we redirect keys.
296 299 ConsoleWidget._on_key_down(self, event, skip=skip)
297 if self.debug:
298 print >>sys.__stderr__, chr(event.KeyCode)
299 self._running_process.process.stdin.write(chr(event.KeyCode))
300 char = chr(event.KeyCode)
301 # Deal with some inconsistency in wx keycodes:
302 if char == '\r':
303 char = '\n'
304 elif not event.ShiftDown():
305 char = char.lower()
306 if event.ControlDown() and event.KeyCode in (ord('d'), ord('D')):
307 char = '\04'
308 self._running_process.process.stdin.write(char)
300 309 self._running_process.process.stdin.flush()
301 310 elif event.KeyCode in (ord('('), 57):
302 311 # Calltips
303 312 event.Skip()
304 313 self.do_calltip()
305 314 elif self.AutoCompActive():
306 315 event.Skip()
307 316 if event.KeyCode in (wx.WXK_BACK, wx.WXK_DELETE):
308 317 wx.CallAfter(self.popup_completion, create=True)
309 318 elif not event.KeyCode in (wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT,
310 319 wx.WXK_RIGHT):
311 320 wx.CallAfter(self.popup_completion)
312 321 else:
313 322 # Up history
314 323 if event.KeyCode == wx.WXK_UP and (
315 324 ( current_line_number == self.current_prompt_line and
316 325 event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN) )
317 326 or event.ControlDown() ):
318 327 new_buffer = self.get_history_previous(
319 328 self.get_current_edit_buffer())
320 329 if new_buffer is not None:
321 330 self.replace_current_edit_buffer(new_buffer)
322 331 if self.GetCurrentLine() > self.current_prompt_line:
323 332 # Go to first line, for seemless history up.
324 333 self.GotoPos(self.current_prompt_pos)
325 334 # Down history
326 335 elif event.KeyCode == wx.WXK_DOWN and (
327 336 ( current_line_number == self.LineCount -1 and
328 337 event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN) )
329 338 or event.ControlDown() ):
330 339 new_buffer = self.get_history_next()
331 340 if new_buffer is not None:
332 341 self.replace_current_edit_buffer(new_buffer)
333 342 # Tab-completion
334 343 elif event.KeyCode == ord('\t'):
335 344 last_line = self.get_current_edit_buffer().split('\n')[-1]
336 345 if not re.match(r'^\s*$', last_line):
337 346 self.do_completion()
338 347 else:
339 348 event.Skip()
340 349 else:
341 350 ConsoleWidget._on_key_down(self, event, skip=skip)
342 351
343 352
344 353 def _on_key_up(self, event, skip=True):
345 354 if event.KeyCode in (59, ord('.')):
346 355 # Intercepting '.'
347 356 event.Skip()
348 357 self.popup_completion(create=True)
349 358 else:
350 359 ConsoleWidget._on_key_up(self, event, skip=skip)
351 360
352 361
353 362 def _on_enter(self):
354 363 if self.debug:
355 364 print >>sys.__stdout__, repr(self.get_current_edit_buffer())
356 365 PrefilterFrontEnd._on_enter(self)
357 366
358 367
359 368 def _end_system_call(self):
360 369 """ Called at the end of a system call.
361 370 """
362 371 print >>sys.__stderr__, 'End of system call'
363 372 self._input_state = 'buffering'
364 373 self._running_process = False
365 374
366 375
367 376 def _buffer_flush(self, event):
368 377 """ Called by the timer to flush the write buffer.
369 378
370 379 This is always called in the mainloop, by the wx timer.
371 380 """
372 381 self._out_buffer_lock.acquire()
373 382 _out_buffer = self._out_buffer
374 383 self._out_buffer = []
375 384 self._out_buffer_lock.release()
376 385 self.write(''.join(_out_buffer), refresh=False)
377 386 self._buffer_flush_timer.Stop()
378 387
379 388
380 389 if __name__ == '__main__':
381 390 class MainWindow(wx.Frame):
382 391 def __init__(self, parent, id, title):
383 392 wx.Frame.__init__(self, parent, id, title, size=(300,250))
384 393 self._sizer = wx.BoxSizer(wx.VERTICAL)
385 394 self.shell = WxController(self)
386 395 self._sizer.Add(self.shell, 1, wx.EXPAND)
387 396 self.SetSizer(self._sizer)
388 397 self.SetAutoLayout(1)
389 398 self.Show(True)
390 399
391 400 app = wx.PySimpleApp()
392 401 frame = MainWindow(None, wx.ID_ANY, 'Ipython')
393 402 frame.shell.SetFocus()
394 403 frame.SetSize((680, 460))
395 404 self = frame.shell
396 405
397 406 app.MainLoop()
398 407
General Comments 0
You need to be logged in to leave comments. Login now