1"""
2Backend to the console plugin.
3
4@author: Eitan Isaacson
5@organization: IBM Corporation
6@copyright: Copyright (c) 2007 IBM Corporation
7@license: BSD
8
9All rights reserved. This program and the accompanying materials are made
10available under the terms of the BSD which accompanies this distribution, and
11is available at U{http://www.opensource.org/licenses/bsd-license.php}
12"""
13# this file is a modified version of source code from the Accerciser project
14# http://live.gnome.org/accerciser
15
16from __future__ import print_function
17import gtk, gobject
18import re
19import sys
20import os
21from gi.repository import Pango
22from StringIO import StringIO
23import IPython
24
25from pkg_resources import parse_version
26
27try:
28  import IPython
29except ImportError:
30  ##@ var IPython
31  #
32  IPython = None
33
34## IterableIPShell class
35class IterableIPShell:
36  ## @var IP
37  #  IP
38  ## @var iter_more
39  #  iterate more
40  ## @var history_level
41  #  history level
42  ## @var complete_sep
43  #  separators
44  ## @var prompt
45  #  prompt
46  ## @var header
47  #  header
48  ## @var config
49  #  config
50  ## @var user_ns
51  #  user_ns
52  ## @var old_stdout
53  #  saved stdout
54  ## @var old_stderr
55  #  saved stderr
56  ## @var system
57  #  system
58  ## @var cfg
59  # configuration
60  ## @var colors
61  #  colors
62  ## @var raw_input_original
63  #  original raw input
64  ## @var stdin
65  #  cin
66  ## @var stdout
67  #  cout
68  ## @var stderr
69  #  cerr
70  ## @var raw_input
71  #  raw input
72  ## @var excepthook
73  #  exception hook
74  ## Constructor
75  def __init__(self,argv=None,user_ns=None,user_global_ns=None,
76               cin=None, cout=None,cerr=None, input_func=None):
77    """! Initializer
78
79    @param self: this object
80    @param argv: Command line options for IPython
81    @param user_ns: User namespace.
82    @param user_global_ns: User global namespace.
83    @param cin: Console standard input.
84    @param cout: Console standard output.
85    @param cerr: Console standard error.
86    @param input_func: Replacement for builtin raw_input()
87    @return none
88    """
89    io = IPython.utils.io
90    if input_func:
91      if parse_version(IPython.release.version) >= parse_version("1.2.1"):
92        IPython.terminal.interactiveshell.raw_input_original = input_func
93      else:
94        IPython.frontend.terminal.interactiveshell.raw_input_original = input_func
95    if cin:
96      io.stdin = io.IOStream(cin)
97    if cout:
98      io.stdout = io.IOStream(cout)
99    if cerr:
100      io.stderr = io.IOStream(cerr)
101
102    # This is to get rid of the blockage that occurs during
103    # IPython.Shell.InteractiveShell.user_setup()
104
105    io.raw_input = lambda x: None
106
107    os.environ['TERM'] = 'dumb'
108    excepthook = sys.excepthook
109
110    from IPython.config.loader import Config
111    cfg = Config()
112    cfg.InteractiveShell.colors = "Linux"
113
114    # InteractiveShell's __init__ overwrites io.stdout,io.stderr with
115    # sys.stdout, sys.stderr, this makes sure they are right
116    #
117    old_stdout, old_stderr = sys.stdout, sys.stderr
118    sys.stdout, sys.stderr = io.stdout.stream, io.stderr.stream
119
120    # InteractiveShell inherits from SingletonConfigurable, so use instance()
121    #
122    if parse_version(IPython.release.version) >= parse_version("1.2.1"):
123      self.IP = IPython.terminal.embed.InteractiveShellEmbed.instance(\
124              config=cfg, user_ns=user_ns)
125    else:
126      self.IP = IPython.frontend.terminal.embed.InteractiveShellEmbed.instance(\
127              config=cfg, user_ns=user_ns)
128
129    sys.stdout, sys.stderr = old_stdout, old_stderr
130
131    self.IP.system = lambda cmd: self.shell(self.IP.var_expand(cmd),
132                                            header='IPython system call: ')
133#                                            local_ns=user_ns)
134                                            #global_ns=user_global_ns)
135                                            #verbose=self.IP.rc.system_verbose)
136
137    self.IP.raw_input = input_func
138    sys.excepthook = excepthook
139    self.iter_more = 0
140    self.history_level = 0
141    self.complete_sep =  re.compile('[\s\{\}\[\]\(\)]')
142    self.updateNamespace({'exit':lambda:None})
143    self.updateNamespace({'quit':lambda:None})
144    self.IP.readline_startup_hook(self.IP.pre_readline)
145    # Workaround for updating namespace with sys.modules
146    #
147    self.__update_namespace()
148
149  def __update_namespace(self):
150    """!
151    Update self.IP namespace for autocompletion with sys.modules
152    """
153    for k, v in list(sys.modules.items()):
154        if not '.' in k:
155          self.IP.user_ns.update({k:v})
156
157  def execute(self):
158    """!
159    Executes the current line provided by the shell object.
160    """
161    self.history_level = 0
162    orig_stdout = sys.stdout
163    sys.stdout = IPython.utils.io.stdout
164
165    orig_stdin = sys.stdin
166    sys.stdin = IPython.utils.io.stdin;
167    self.prompt = self.generatePrompt(self.iter_more)
168
169    self.IP.hooks.pre_prompt_hook()
170    if self.iter_more:
171        try:
172            self.prompt = self.generatePrompt(True)
173        except:
174            self.IP.showtraceback()
175        if self.IP.autoindent:
176            self.IP.rl_do_indent = True
177
178    try:
179      line = self.IP.raw_input(self.prompt)
180    except KeyboardInterrupt:
181      self.IP.write('\nKeyboardInterrupt\n')
182      self.IP.input_splitter.reset()
183    except:
184      self.IP.showtraceback()
185    else:
186      self.IP.input_splitter.push(line)
187      self.iter_more = self.IP.input_splitter.push_accepts_more()
188      self.prompt = self.generatePrompt(self.iter_more)
189      if (self.IP.SyntaxTB.last_syntax_error and
190          self.IP.autoedit_syntax):
191          self.IP.edit_syntax_error()
192      if not self.iter_more:
193          if parse_version(IPython.release.version) >= parse_version("2.0.0-dev"):
194            source_raw = self.IP.input_splitter.raw_reset()
195          else:
196            source_raw = self.IP.input_splitter.source_raw_reset()[1]
197          self.IP.run_cell(source_raw, store_history=True)
198          self.IP.rl_do_indent = False
199      else:
200          # TODO: Auto-indent
201          #
202          self.IP.rl_do_indent = True
203          pass
204
205    sys.stdout = orig_stdout
206    sys.stdin = orig_stdin
207
208  def generatePrompt(self, is_continuation):
209    """!
210    Generate prompt depending on is_continuation value
211
212    @param is_continuation
213    @return: The prompt string representation
214
215    """
216
217    # Backwards compatibility with ipyton-0.11
218    #
219    ver = IPython.__version__
220    if '0.11' in ver:
221        prompt = self.IP.hooks.generate_prompt(is_continuation)
222    else:
223        if is_continuation:
224            prompt = self.IP.prompt_manager.render('in2')
225        else:
226            prompt = self.IP.prompt_manager.render('in')
227
228    return prompt
229
230
231  def historyBack(self):
232    """!
233    Provides one history command back.
234
235    @param self this object
236    @return: The command string.
237    """
238    self.history_level -= 1
239    if not self._getHistory():
240      self.history_level +=1
241    return self._getHistory()
242
243  def historyForward(self):
244    """!
245    Provides one history command forward.
246
247    @param self this object
248    @return: The command string.
249    """
250    if self.history_level < 0:
251      self.history_level += 1
252    return self._getHistory()
253
254  def _getHistory(self):
255    """!
256    Gets the command string of the current history level.
257
258    @param self this object
259    @return: Historic command string.
260    """
261    try:
262      rv = self.IP.user_ns['In'][self.history_level].strip('\n')
263    except IndexError:
264      rv = ''
265    return rv
266
267  def updateNamespace(self, ns_dict):
268    """!
269    Add the current dictionary to the shell namespace.
270
271    @param ns_dict: A dictionary of symbol-values.
272    @return none
273    """
274    self.IP.user_ns.update(ns_dict)
275
276  def complete(self, line):
277    """!
278    Returns an auto completed line and/or possibilities for completion.
279
280    @param line: Given line so far.
281    @return: Line completed as for as possible, and possible further completions.
282    """
283    split_line = self.complete_sep.split(line)
284    if split_line[-1]:
285      possibilities = self.IP.complete(split_line[-1])
286    else:
287      completed = line
288      possibilities = ['', []]
289    if possibilities:
290      def _commonPrefix(str1, str2):
291        """!
292        Reduction function. returns common prefix of two given strings.
293
294        @param str1: First string.
295        @param str2: Second string
296        @return: Common prefix to both strings.
297        """
298        for i in range(len(str1)):
299          if not str2.startswith(str1[:i+1]):
300            return str1[:i]
301        return str1
302      if possibilities[1]:
303        common_prefix = reduce(_commonPrefix, possibilities[1]) or line[-1]
304        completed = line[:-len(split_line[-1])]+common_prefix
305      else:
306        completed = line
307    else:
308      completed = line
309    return completed, possibilities[1]
310
311
312  def shell(self, cmd,verbose=0,debug=0,header=''):
313    """!
314    Replacement method to allow shell commands without them blocking.
315
316    @param cmd: Shell command to execute.
317    @param verbose: Verbosity
318    @param debug: Debug level
319    @param header: Header to be printed before output
320    @return none
321    """
322    stat = 0
323    if verbose or debug: print(header+cmd)
324    # flush stdout so we don't mangle python's buffering
325    if not debug:
326      input, output = os.popen4(cmd)
327      print(output.read())
328      output.close()
329      input.close()
330
331## ConsoleView class
332class ConsoleView(Gtk.TextView):
333  ## @var ANSI_COLORS
334  #  color list
335  ## @var text_buffer
336  #  text buffer
337  ## @var mark
338  #  scroll mark
339  ## @var color_pat
340  #  color pattern
341  ## @var line_start
342  #  line start
343  """
344  Specialized text view for console-like workflow.
345
346  @cvar ANSI_COLORS: Mapping of terminal colors to X11 names.
347  @type ANSI_COLORS: dictionary
348
349  @ivar text_buffer: Widget's text buffer.
350  @type text_buffer: Gtk.TextBuffer
351  @ivar color_pat: Regex of terminal color pattern
352  @type color_pat: _sre.SRE_Pattern
353  @ivar mark: Scroll mark for automatic scrolling on input.
354  @type mark: Gtk.TextMark
355  @ivar line_start: Start of command line mark.
356  @type line_start: Gtk.TextMark
357  """
358  ANSI_COLORS =  {'0;30': 'Black',     '0;31': 'Red',
359                  '0;32': 'Green',     '0;33': 'Brown',
360                  '0;34': 'Blue',      '0;35': 'Purple',
361                  '0;36': 'Cyan',      '0;37': 'LightGray',
362                  '1;30': 'DarkGray',  '1;31': 'DarkRed',
363                  '1;32': 'SeaGreen',  '1;33': 'Yellow',
364                  '1;34': 'LightBlue', '1;35': 'MediumPurple',
365                  '1;36': 'LightCyan', '1;37': 'White'}
366
367  def __init__(self):
368    """
369    Initialize console view.
370    """
371    GObject.GObject.__init__(self)
372    self.modify_font(Pango.FontDescription('Mono'))
373    self.set_cursor_visible(True)
374    self.text_buffer = self.get_buffer()
375    self.mark = self.text_buffer.create_mark('scroll_mark',
376                                             self.text_buffer.get_end_iter(),
377                                             False)
378    for code in self.ANSI_COLORS:
379      self.text_buffer.create_tag(code,
380                                  foreground=self.ANSI_COLORS[code],
381                                  weight=700)
382    self.text_buffer.create_tag('0')
383    self.text_buffer.create_tag('notouch', editable=False)
384    self.color_pat = re.compile('\x01?\x1b\[(.*?)m\x02?')
385    self.line_start = \
386        self.text_buffer.create_mark('line_start',
387                                     self.text_buffer.get_end_iter(), True)
388    self.connect('key-press-event', self.onKeyPress)
389
390  def write(self, text, editable=False):
391    """!
392    Write given text to buffer.
393
394    @param text: Text to append.
395    @param editable: If true, added text is editable.
396    @return none
397    """
398    GObject.idle_add(self._write, text, editable)
399
400  def _write(self, text, editable=False):
401    """!
402    Write given text to buffer.
403
404    @param text: Text to append.
405    @param editable: If true, added text is editable.
406    @return none
407    """
408    segments = self.color_pat.split(text)
409    segment = segments.pop(0)
410    start_mark = self.text_buffer.create_mark(None,
411                                              self.text_buffer.get_end_iter(),
412                                              True)
413    self.text_buffer.insert(self.text_buffer.get_end_iter(), segment)
414
415    if segments:
416      ansi_tags = self.color_pat.findall(text)
417      for tag in ansi_tags:
418        i = segments.index(tag)
419        self.text_buffer.insert_with_tags_by_name(self.text_buffer.get_end_iter(),
420                                                  segments[i+1], str(tag))
421        segments.pop(i)
422    if not editable:
423      self.text_buffer.apply_tag_by_name('notouch',
424                                         self.text_buffer.get_iter_at_mark(start_mark),
425                                         self.text_buffer.get_end_iter())
426    self.text_buffer.delete_mark(start_mark)
427    self.scroll_mark_onscreen(self.mark)
428
429  def showPrompt(self, prompt):
430    """!
431    Prints prompt at start of line.
432
433    @param prompt: Prompt to print.
434    @return none
435    """
436    GObject.idle_add(self._showPrompt, prompt)
437
438  def _showPrompt(self, prompt):
439    """!
440    Prints prompt at start of line.
441
442    @param prompt: Prompt to print.
443    @return none
444    """
445    self._write(prompt)
446    self.text_buffer.move_mark(self.line_start,
447                               self.text_buffer.get_end_iter())
448
449  def changeLine(self, text):
450    """!
451    Replace currently entered command line with given text.
452
453    @param text: Text to use as replacement.
454    @return none
455    """
456    GObject.idle_add(self._changeLine, text)
457
458  def _changeLine(self, text):
459    """!
460    Replace currently entered command line with given text.
461
462    @param text: Text to use as replacement.
463    @return none
464    """
465    iter = self.text_buffer.get_iter_at_mark(self.line_start)
466    iter.forward_to_line_end()
467    self.text_buffer.delete(self.text_buffer.get_iter_at_mark(self.line_start), iter)
468    self._write(text, True)
469
470  def getCurrentLine(self):
471    """!
472    Get text in current command line.
473
474    @return Text of current command line.
475    """
476    rv = self.text_buffer.get_slice(
477      self.text_buffer.get_iter_at_mark(self.line_start),
478      self.text_buffer.get_end_iter(), False)
479    return rv
480
481  def showReturned(self, text):
482    """!
483    Show returned text from last command and print new prompt.
484
485    @param text: Text to show.
486    @return none
487    """
488    GObject.idle_add(self._showReturned, text)
489
490  def _showReturned(self, text):
491    """!
492    Show returned text from last command and print new prompt.
493
494    @param text: Text to show.
495    @return none
496    """
497    iter = self.text_buffer.get_iter_at_mark(self.line_start)
498    iter.forward_to_line_end()
499    self.text_buffer.apply_tag_by_name(
500      'notouch',
501      self.text_buffer.get_iter_at_mark(self.line_start),
502      iter)
503    self._write('\n'+text)
504    if text:
505      self._write('\n')
506    self._showPrompt(self.prompt)
507    self.text_buffer.move_mark(self.line_start,self.text_buffer.get_end_iter())
508    self.text_buffer.place_cursor(self.text_buffer.get_end_iter())
509
510    if self.IP.rl_do_indent:
511      indentation = self.IP.input_splitter.indent_spaces * ' '
512      self.text_buffer.insert_at_cursor(indentation)
513
514  def onKeyPress(self, widget, event):
515    """!
516    Key press callback used for correcting behavior for console-like
517    interfaces. For example 'home' should go to prompt, not to beginning of
518    line.
519
520    @param widget: Widget that key press accored in.
521    @param event: Event object
522    @return Return True if event should not trickle.
523    """
524    insert_mark = self.text_buffer.get_insert()
525    insert_iter = self.text_buffer.get_iter_at_mark(insert_mark)
526    selection_mark = self.text_buffer.get_selection_bound()
527    selection_iter = self.text_buffer.get_iter_at_mark(selection_mark)
528    start_iter = self.text_buffer.get_iter_at_mark(self.line_start)
529    if event.keyval == Gdk.KEY_Home:
530      if event.get_state() & Gdk.ModifierType.CONTROL_MASK or event.get_state() & Gdk.ModifierType.MOD1_MASK:
531        pass
532      elif event.get_state() & Gdk.ModifierType.SHIFT_MASK:
533        self.text_buffer.move_mark(insert_mark, start_iter)
534        return True
535      else:
536        self.text_buffer.place_cursor(start_iter)
537        return True
538    elif event.keyval == Gdk.KEY_Left:
539      insert_iter.backward_cursor_position()
540      if not insert_iter.editable(True):
541        return True
542    elif not event.string:
543      pass
544    elif start_iter.compare(insert_iter) <= 0 and \
545          start_iter.compare(selection_iter) <= 0:
546      pass
547    elif start_iter.compare(insert_iter) > 0 and \
548          start_iter.compare(selection_iter) > 0:
549      self.text_buffer.place_cursor(start_iter)
550    elif insert_iter.compare(selection_iter) < 0:
551      self.text_buffer.move_mark(insert_mark, start_iter)
552    elif insert_iter.compare(selection_iter) > 0:
553      self.text_buffer.move_mark(selection_mark, start_iter)
554
555    return self.onKeyPressExtend(event)
556
557  def onKeyPressExtend(self, event):
558    """!
559    For some reason we can't extend onKeyPress directly (bug #500900).
560    @param event key press
561    @return none
562    """
563    pass
564
565## IPythonView class
566class IPythonView(ConsoleView, IterableIPShell):
567  ## @var cout
568  #  cout
569  ## @var interrupt
570  #  interrupt
571  ## @var execute
572  #  execute
573  ## @var prompt
574  #  prompt
575  ## @var showPrompt
576  #  show prompt
577  ## @var history_pos
578  #  history list
579  ## @var window
580  #  GTK Window
581  """
582  Sub-class of both modified IPython shell and L{ConsoleView} this makes
583  a GTK+ IPython console.
584  """
585  def __init__(self):
586    """
587    Initialize. Redirect I/O to console.
588    """
589    ConsoleView.__init__(self)
590    self.cout = StringIO()
591    IterableIPShell.__init__(self, cout=self.cout,cerr=self.cout,
592                             input_func=self.raw_input)
593    self.interrupt = False
594    self.execute()
595    self.prompt = self.generatePrompt(False)
596    self.cout.truncate(0)
597    self.showPrompt(self.prompt)
598
599  def raw_input(self, prompt=''):
600    """!
601    Custom raw_input() replacement. Gets current line from console buffer.
602
603    @param prompt: Prompt to print. Here for compatibility as replacement.
604    @return The current command line text.
605    """
606    if self.interrupt:
607      self.interrupt = False
608      raise KeyboardInterrupt
609    return self.getCurrentLine()
610
611  def onKeyPressExtend(self, event):
612    """!
613    Key press callback with plenty of shell goodness, like history,
614    autocompletions, etc.
615
616    @param event: Event object.
617    @return True if event should not trickle.
618    """
619
620    if event.get_state() & Gdk.ModifierType.CONTROL_MASK and event.keyval == 99:
621      self.interrupt = True
622      self._processLine()
623      return True
624    elif event.keyval == Gdk.KEY_Return:
625      self._processLine()
626      return True
627    elif event.keyval == Gdk.KEY_Up:
628      self.changeLine(self.historyBack())
629      return True
630    elif event.keyval == Gdk.KEY_Down:
631      self.changeLine(self.historyForward())
632      return True
633    elif event.keyval == Gdk.KEY_Tab:
634      if not self.getCurrentLine().strip():
635        return False
636      completed, possibilities = self.complete(self.getCurrentLine())
637      if len(possibilities) > 1:
638        slice = self.getCurrentLine()
639        self.write('\n')
640        for symbol in possibilities:
641          self.write(symbol+'\n')
642        self.showPrompt(self.prompt)
643      self.changeLine(completed or slice)
644      return True
645
646  def _processLine(self):
647    """!
648    Process current command line.
649    @return none
650    """
651    self.history_pos = 0
652    self.execute()
653    rv = self.cout.getvalue()
654    if rv: rv = rv.strip('\n')
655    self.showReturned(rv)
656    self.cout.truncate(0)
657    self.cout.seek(0)
658
659if __name__ == "__main__":
660  window = Gtk.Window()
661  window.set_default_size(640, 320)
662  window.connect('delete-event', lambda x, y: Gtk.main_quit())
663  window.add(IPythonView())
664  window.show_all()
665  Gtk.main()
666
667