root/trunk/src/ipython_view.py

Revision 10549, 17.8 kB (checked in by asterix, 6 weeks ago)

revert thorstenp patches for now. They introduce bugs.

Line 
1#!/usr/bin/python
2# -*- coding:utf-8 -*-
3## src/ipython_view.py
4##
5## Copyright (C) 2008 Yann Leboulanger <asterix AT lagaule.org>
6##
7## This file is part of Gajim.
8##
9## Gajim is free software; you can redistribute it and/or modify
10## it under the terms of the GNU General Public License as published
11## by the Free Software Foundation; version 3 only.
12##
13## Gajim is distributed in the hope that it will be useful,
14## but WITHOUT ANY WARRANTY; without even the implied warranty of
15## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16## GNU General Public License for more details.
17##
18## You should have received a copy of the GNU General Public License
19## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
20##
21## Copyright (c) 2007, IBM Corporation
22## All rights reserved.
23
24## Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
25
26## * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
27## * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
28## * Neither the name of the IBM Corporation nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
29
30## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
32'''
33Provides IPython console widget.
34
35@author: Eitan Isaacson
36@organization: IBM Corporation
37@copyright: Copyright (c) 2007 IBM Corporation
38@license: BSD
39
40All rights reserved. This program and the accompanying materials are made
41available under the terms of the BSD which accompanies this distribution, and
42is available at U{http://www.opensource.org/licenses/bsd-license.php}
43'''
44
45import gtk, gobject
46import re
47import sys
48import os
49import pango
50from StringIO import StringIO
51import thread
52
53try:
54  import IPython
55except ImportError:
56  IPython = None
57
58class IterableIPShell:
59  '''
60  Create an IPython instance. Does not start a blocking event loop,
61  instead allow single iterations. This allows embedding in GTK+
62  without blockage.
63
64  @ivar IP: IPython instance.
65  @type IP: IPython.iplib.InteractiveShell
66  @ivar iter_more: Indicates if the line executed was a complete command,
67  or we should wait for more.
68  @type iter_more: integer
69  @ivar history_level: The place in history where we currently are
70  when pressing up/down.
71  @type history_level: integer
72  @ivar complete_sep: Seperation delimeters for completion function.
73  @type complete_sep: _sre.SRE_Pattern
74  '''
75  def __init__(self,argv=[],user_ns=None,user_global_ns=None, 
76               cin=None, cout=None,cerr=None, input_func=None):
77    '''
78   
79   
80    @param argv: Command line options for IPython
81    @type argv: list
82    @param user_ns: User namespace.
83    @type user_ns: dictionary
84    @param user_global_ns: User global namespace.
85    @type user_global_ns: dictionary.
86    @param cin: Console standard input.
87    @type cin: IO stream
88    @param cout: Console standard output.
89    @type cout: IO stream
90    @param cerr: Console standard error.
91    @type cerr: IO stream
92    @param input_func: Replacement for builtin raw_input()
93    @type input_func: function
94    '''
95    if input_func:
96      IPython.iplib.raw_input_original = input_func
97    if cin:
98      IPython.Shell.Term.cin = cin
99    if cout:
100      IPython.Shell.Term.cout = cout
101    if cerr:
102      IPython.Shell.Term.cerr = cerr
103
104    # This is to get rid of the blockage that accurs during
105    # IPython.Shell.InteractiveShell.user_setup()
106    IPython.iplib.raw_input = lambda x: None
107
108    self.term = IPython.genutils.IOTerm(cin=cin, cout=cout, cerr=cerr)
109    os.environ['TERM'] = 'dumb'
110    excepthook = sys.excepthook
111    self.IP = IPython.Shell.make_IPython(
112      argv,user_ns=user_ns,
113      user_global_ns=user_global_ns,
114      embedded=True,
115      shell_class=IPython.Shell.InteractiveShell)
116    self.IP.system = lambda cmd: self.shell(self.IP.var_expand(cmd),
117                                            header='IPython system call: ',
118                                            verbose=self.IP.rc.system_verbose)
119    sys.excepthook = excepthook
120    self.iter_more = 0
121    self.history_level = 0
122    self.complete_sep =  re.compile('[\s\{\}\[\]\(\)]')
123
124  def execute(self):
125    '''
126    Executes the current line provided by the shell object.
127    '''
128    self.history_level = 0
129    orig_stdout = sys.stdout
130    sys.stdout = IPython.Shell.Term.cout
131    try:
132      line = self.IP.raw_input(None, self.iter_more)
133      if self.IP.autoindent:
134        self.IP.readline_startup_hook(None)
135    except KeyboardInterrupt:
136      self.IP.write('\nKeyboardInterrupt\n')
137      self.IP.resetbuffer()
138      # keep cache in sync with the prompt counter:
139      self.IP.outputcache.prompt_count -= 1
140       
141      if self.IP.autoindent:
142        self.IP.indent_current_nsp = 0
143      self.iter_more = 0
144    except:
145      self.IP.showtraceback()
146    else:
147      self.iter_more = self.IP.push(line)
148      if (self.IP.SyntaxTB.last_syntax_error and
149          self.IP.rc.autoedit_syntax):
150        self.IP.edit_syntax_error()
151    if self.iter_more:
152      self.prompt = str(self.IP.outputcache.prompt2).strip()
153      if self.IP.autoindent:
154        self.IP.readline_startup_hook(self.IP.pre_readline)
155    else:
156      self.prompt = str(self.IP.outputcache.prompt1).strip()
157    sys.stdout = orig_stdout
158
159  def historyBack(self):
160    '''
161    Provides one history command back.
162   
163    @return: The command string.
164    @rtype: string
165    '''
166    self.history_level -= 1
167    return self._getHistory()
168 
169  def historyForward(self):
170    '''
171    Provides one history command forward.
172   
173    @return: The command string.
174    @rtype: string
175    '''
176    self.history_level += 1
177    return self._getHistory()
178 
179  def _getHistory(self):
180    '''
181    Get's the command string of the current history level.
182   
183    @return: Historic command string.
184    @rtype: string
185    '''
186    try:
187      rv = self.IP.user_ns['In'][self.history_level].strip('\n')
188    except IndexError:
189      self.history_level = 0
190      rv = ''
191    return rv
192
193  def updateNamespace(self, ns_dict):
194    '''
195    Add the current dictionary to the shell namespace.
196   
197    @param ns_dict: A dictionary of symbol-values.
198    @type ns_dict: dictionary
199    '''
200    self.IP.user_ns.update(ns_dict)
201
202  def complete(self, line):
203    '''
204    Returns an auto completed line and/or posibilities for completion.
205   
206    @param line: Given line so far.
207    @type line: string
208   
209    @return: Line completed as for as possible,
210    and possible further completions.
211    @rtype: tuple
212    '''
213    split_line = self.complete_sep.split(line)
214    possibilities = self.IP.complete(split_line[-1])
215
216    try:
217        __builtins__.all
218    except AttributeError:
219        def all(iterable):
220            for element in iterable:
221                if not element:
222                    return False
223            return True
224
225    def common_prefix(seq):
226        """Returns the common prefix of a sequence of strings"""
227        return "".join(c for i, c in enumerate(seq[0])
228                if all(s.startswith(c, i) for s in seq))
229    if possibilities:
230      completed = line[:-len(split_line[-1])]+common_prefix(possibilities)
231    else:
232      completed = line
233    return completed, possibilities
234 
235
236  def shell(self, cmd,verbose=0,debug=0,header=''):
237    '''
238    Replacement method to allow shell commands without them blocking.
239   
240    @param cmd: Shell command to execute.
241    @type cmd: string
242    @param verbose: Verbosity
243    @type verbose: integer
244    @param debug: Debug level
245    @type debug: integer
246    @param header: Header to be printed before output
247    @type header: string
248    '''
249    stat = 0
250    if verbose or debug: print header+cmd
251    # flush stdout so we don't mangle python's buffering
252    if not debug:
253      input, output = os.popen4(cmd)
254      print output.read()
255      output.close()
256      input.close()
257
258class ConsoleView(gtk.TextView):
259  '''
260  Specialized text view for console-like workflow.
261
262  @cvar ANSI_COLORS: Mapping of terminal colors to X11 names.
263  @type ANSI_COLORS: dictionary
264
265  @ivar text_buffer: Widget's text buffer.
266  @type text_buffer: gtk.TextBuffer
267  @ivar color_pat: Regex of terminal color pattern
268  @type color_pat: _sre.SRE_Pattern
269  @ivar mark: Scroll mark for automatic scrolling on input.
270  @type mark: gtk.TextMark
271  @ivar line_start: Start of command line mark.
272  @type line_start: gtk.TextMark
273  '''
274  ANSI_COLORS =  {'0;30': 'Black',     '0;31': 'Red',
275                  '0;32': 'Green',     '0;33': 'Brown',
276                  '0;34': 'Blue',      '0;35': 'Purple',
277                  '0;36': 'Cyan',      '0;37': 'LightGray',
278                  '1;30': 'DarkGray',  '1;31': 'DarkRed',
279                  '1;32': 'SeaGreen',  '1;33': 'Yellow',
280                  '1;34': 'LightBlue', '1;35': 'MediumPurple',
281                  '1;36': 'LightCyan', '1;37': 'White'}
282
283  def __init__(self):
284    '''
285    Initialize console view.
286    '''
287    gtk.TextView.__init__(self)
288    self.modify_font(pango.FontDescription('Mono'))
289    self.set_cursor_visible(True)
290    self.text_buffer = self.get_buffer()
291    self.mark = self.text_buffer.create_mark('scroll_mark', 
292                                             self.text_buffer.get_end_iter(),
293                                             False)
294    for code in self.ANSI_COLORS:
295      self.text_buffer.create_tag(code, 
296                                  foreground=self.ANSI_COLORS[code], 
297                                  weight=700)
298    self.text_buffer.create_tag('0')
299    self.text_buffer.create_tag('notouch', editable=False)
300    self.color_pat = re.compile('\x01?\x1b\[(.*?)m\x02?')
301    self.line_start = \
302        self.text_buffer.create_mark('line_start', 
303                                     self.text_buffer.get_end_iter(), True)
304    self.connect('key-press-event', self.onKeyPress)
305   
306  def write(self, text, editable=False):
307    gobject.idle_add(self._write, text, editable)
308
309  def _write(self, text, editable=False):
310    '''
311    Write given text to buffer.
312   
313    @param text: Text to append.
314    @type text: string
315    @param editable: If true, added text is editable.
316    @type editable: boolean
317    '''
318    segments = self.color_pat.split(text)
319    segment = segments.pop(0)
320    start_mark = self.text_buffer.create_mark(None, 
321                                              self.text_buffer.get_end_iter(), 
322                                              True)
323    self.text_buffer.insert(self.text_buffer.get_end_iter(), segment)
324
325    if segments:
326      ansi_tags = self.color_pat.findall(text)
327      for tag in ansi_tags:
328        i = segments.index(tag)
329        self.text_buffer.insert_with_tags_by_name(self.text_buffer.get_end_iter(),
330                                             segments[i+1], tag)
331        segments.pop(i)
332    if not editable:
333      self.text_buffer.apply_tag_by_name('notouch',
334                                         self.text_buffer.get_iter_at_mark(start_mark),
335                                         self.text_buffer.get_end_iter())
336    self.text_buffer.delete_mark(start_mark)
337    self.scroll_mark_onscreen(self.mark)
338
339
340  def showPrompt(self, prompt):
341    gobject.idle_add(self._showPrompt, prompt)
342
343  def _showPrompt(self, prompt):
344    '''
345    Prints prompt at start of line.
346   
347    @param prompt: Prompt to print.
348    @type prompt: string
349    '''
350    self._write(prompt)
351    self.text_buffer.move_mark(self.line_start,
352                               self.text_buffer.get_end_iter())
353
354  def changeLine(self, text):
355    gobject.idle_add(self._changeLine, text)
356
357  def _changeLine(self, text):
358    '''
359    Replace currently entered command line with given text.
360   
361    @param text: Text to use as replacement.
362    @type text: string
363    '''
364    iter = self.text_buffer.get_iter_at_mark(self.line_start)
365    iter.forward_to_line_end()
366    self.text_buffer.delete(self.text_buffer.get_iter_at_mark(self.line_start), iter)
367    self._write(text, True)
368
369  def getCurrentLine(self):
370    '''
371    Get text in current command line.
372   
373    @return: Text of current command line.
374    @rtype: string
375    '''
376    rv = self.text_buffer.get_slice(
377      self.text_buffer.get_iter_at_mark(self.line_start),
378      self.text_buffer.get_end_iter(), False)
379    return rv
380
381  def showReturned(self, text):
382    gobject.idle_add(self._showReturned, text)
383
384  def _showReturned(self, text):
385    '''
386    Show returned text from last command and print new prompt.
387   
388    @param text: Text to show.
389    @type text: string
390    '''
391    iter = self.text_buffer.get_iter_at_mark(self.line_start)
392    iter.forward_to_line_end()
393    self.text_buffer.apply_tag_by_name(
394      'notouch', 
395      self.text_buffer.get_iter_at_mark(self.line_start),
396      iter)
397    self._write('\n'+text)
398    if text:
399      self._write('\n')
400    self._showPrompt(self.prompt)
401    self.text_buffer.move_mark(self.line_start,self.text_buffer.get_end_iter())
402    self.text_buffer.place_cursor(self.text_buffer.get_end_iter())
403
404  def onKeyPress(self, widget, event):
405    '''
406    Key press callback used for correcting behavior for console-like
407    interfaces. For example 'home' should go to prompt, not to begining of
408    line.
409   
410    @param widget: Widget that key press accored in.
411    @type widget: gtk.Widget
412    @param event: Event object
413    @type event: gtk.gdk.Event
414   
415    @return: Return True if event should not trickle.
416    @rtype: boolean
417    '''
418    insert_mark = self.text_buffer.get_insert()
419    insert_iter = self.text_buffer.get_iter_at_mark(insert_mark)
420    selection_mark = self.text_buffer.get_selection_bound()
421    selection_iter = self.text_buffer.get_iter_at_mark(selection_mark)
422    start_iter = self.text_buffer.get_iter_at_mark(self.line_start)
423    if event.keyval == gtk.keysyms.Home:
424      if event.state == 0: 
425        self.text_buffer.place_cursor(start_iter)
426        return True
427      elif event.state == gtk.gdk.SHIFT_MASK:
428        self.text_buffer.move_mark(insert_mark, start_iter)
429        return True
430    elif event.keyval == gtk.keysyms.Left:
431      insert_iter.backward_cursor_position()
432      if not insert_iter.editable(True):
433        return True
434    elif not event.string:
435      pass
436    elif start_iter.compare(insert_iter) <= 0 and \
437          start_iter.compare(selection_iter) <= 0:
438      pass
439    elif start_iter.compare(insert_iter) > 0 and \
440          start_iter.compare(selection_iter) > 0:
441      self.text_buffer.place_cursor(start_iter)
442    elif insert_iter.compare(selection_iter) < 0:
443      self.text_buffer.move_mark(insert_mark, start_iter)
444    elif insert_iter.compare(selection_iter) > 0:
445      self.text_buffer.move_mark(selection_mark, start_iter)             
446
447    return self.onKeyPressExtend(event)
448
449  def onKeyPressExtend(self, event):
450    '''
451    For some reason we can't extend onKeyPress directly (bug #500900).
452    '''
453    pass
454
455class IPythonView(ConsoleView, IterableIPShell):
456  '''
457  Sub-class of both modified IPython shell and L{ConsoleView} this makes
458  a GTK+ IPython console.
459  '''
460  def __init__(self):
461    '''
462    Initialize. Redirect I/O to console.
463    '''
464    ConsoleView.__init__(self)
465    self.cout = StringIO()
466    IterableIPShell.__init__(self, cout=self.cout,cerr=self.cout, 
467                             input_func=self.raw_input)
468#    self.connect('key_press_event', self.keyPress)
469    self.execute()
470    self.cout.truncate(0)
471    self.showPrompt(self.prompt)
472    self.interrupt = False
473
474  def raw_input(self, prompt=''):
475    '''
476    Custom raw_input() replacement. Get's current line from console buffer.
477   
478    @param prompt: Prompt to print. Here for compatability as replacement.
479    @type prompt: string
480   
481    @return: The current command line text.
482    @rtype: string
483    '''
484    if self.interrupt:
485      self.interrupt = False
486      raise KeyboardInterrupt
487    return self.getCurrentLine()
488
489  def onKeyPressExtend(self, event):
490    '''
491    Key press callback with plenty of shell goodness, like history,
492    autocompletions, etc.
493   
494    @param widget: Widget that key press occured in.
495    @type widget: gtk.Widget
496    @param event: Event object.
497    @type event: gtk.gdk.Event
498   
499    @return: True if event should not trickle.
500    @rtype: boolean
501    '''
502    if event.state & gtk.gdk.CONTROL_MASK and event.keyval == 99:
503      self.interrupt = True
504      self._processLine()
505      return True
506    elif event.keyval == gtk.keysyms.Return:
507      self._processLine()
508      return True
509    elif event.keyval == gtk.keysyms.Up:
510      self.changeLine(self.historyBack())
511      return True
512    elif event.keyval == gtk.keysyms.Down:
513      self.changeLine(self.historyForward())
514      return True
515    elif event.keyval == gtk.keysyms.Tab:
516      if not self.getCurrentLine().strip():
517        return False
518      completed, possibilities = self.complete(self.getCurrentLine())
519      if len(possibilities) > 1:
520        slice = self.getCurrentLine()
521        self.write('\n')
522        for symbol in possibilities:
523          self.write(symbol+'\n')
524        self.showPrompt(self.prompt)
525      self.changeLine(completed or slice)
526      return True
527
528  def _processLine(self):
529    '''
530    Process current command line.
531    '''
532    self.history_pos = 0
533    self.execute()
534    rv = self.cout.getvalue()
535    if rv: rv = rv.strip('\n')
536    self.showReturned(rv)
537    self.cout.truncate(0)
538   
539
540# vim: se ts=3:
Note: See TracBrowser for help on using the browser.