1#!/usr/bin/env python
2#
3#  pyconsole.py
4#
5#  Copyright (C) 2004-2010 by Yevgen Muntyan <emuntyan@users.sourceforge.net>
6#  Thanks to Geoffrey French for ideas.
7#
8#  This file is part of medit.  medit is free software; you can
9#  redistribute it and/or modify it under the terms of the
10#  GNU Lesser General Public License as published by the
11#  Free Software Foundation; either version 2.1 of the License,
12#  or (at your option) any later version.
13#
14#  You should have received a copy of the GNU Lesser General Public
15#  License along with medit.  If not, see <http://www.gnu.org/licenses/>.
16#
17
18# This module 'runs' python interpreter in a TextView widget.
19# The main class is Console, usage is:
20# Console(locals=None, banner=None, completer=None, use_rlcompleter=True, start_script='') -
21# it creates the widget and 'starts' interactive session; see the end of
22# this file. If start_script is not empty, it pastes it as it was entered from keyboard.
23#
24# Console has "command" signal which is emitted when code is about to
25# be executed. You may connect to it using console.connect or console.connect_after
26# to get your callback ran before or after the code is executed.
27#
28# To modify output appearance, set attributes of console.stdout_tag and
29# console.stderr_tag.
30#
31# Console may subclass a type other than gtk.TextView, to allow syntax highlighting and stuff,
32# e.g.:
33#   console_type = pyconsole.ConsoleType(moo.TextView)
34#   console = console_type(use_rlcompleter=False, start_script="import moo\nimport gtk\n")
35#
36# This widget is not a replacement for real terminal with python running
37# inside: GtkTextView is not a terminal.
38# The use case is: you have a python program, you create this widget,
39# and inspect your program interiors.
40
41import gtk
42import gtk.gdk as gdk
43import gobject
44import pango
45import gtk.keysyms as _keys
46import code
47import sys
48import keyword
49import re
50
51# commonprefix() from posixpath
52def _commonprefix(m):
53    "Given a list of pathnames, returns the longest common leading component"
54    if not m: return ''
55    prefix = m[0]
56    for item in m:
57        for i in range(len(prefix)):
58            if prefix[:i+1] != item[:i+1]:
59                prefix = prefix[:i]
60                if i == 0:
61                    return ''
62                break
63    return prefix
64
65class _ReadLine(object):
66
67    class Output(object):
68        def __init__(self, console, tag_name):
69            object.__init__(self)
70            self.buffer = console.get_buffer()
71            self.tag_name = tag_name
72        def write(self, text):
73            pos = self.buffer.get_iter_at_mark(self.buffer.get_insert())
74            self.buffer.insert_with_tags_by_name(pos, text, self.tag_name)
75
76    class History(object):
77        def __init__(self):
78            object.__init__(self)
79            self.items = ['']
80            self.ptr = 0
81            self.edited = {}
82
83        def commit(self, text):
84            if text and self.items[-1] != text:
85                self.items.append(text)
86            self.ptr = 0
87            self.edited = {}
88
89        def get(self, dir, text):
90            if len(self.items) == 1:
91                return None
92
93            if text != self.items[self.ptr]:
94                self.edited[self.ptr] = text
95            elif self.edited.has_key(self.ptr):
96                del self.edited[self.ptr]
97
98            self.ptr = self.ptr + dir
99            if self.ptr >= len(self.items):
100                self.ptr = 0
101            elif self.ptr < 0:
102                self.ptr = len(self.items) - 1
103
104            try:
105                return self.edited[self.ptr]
106            except KeyError:
107                return self.items[self.ptr]
108
109    def __init__(self):
110        object.__init__(self)
111
112        self.set_wrap_mode(gtk.WRAP_CHAR)
113        self.modify_font(pango.FontDescription("Monospace"))
114
115        self.buffer = self.get_buffer()
116        self.buffer.connect("insert-text", self.on_buf_insert)
117        self.buffer.connect("delete-range", self.on_buf_delete)
118        self.buffer.connect("mark-set", self.on_buf_mark_set)
119        self.do_insert = False
120        self.do_delete = False
121
122        self.stdout_tag = self.buffer.create_tag("stdout", foreground="#006000")
123        self.stderr_tag = self.buffer.create_tag("stderr", foreground="#B00000")
124        self._stdout = _ReadLine.Output(self, "stdout")
125        self._stderr = _ReadLine.Output(self, "stderr")
126
127        self.cursor = self.buffer.create_mark("cursor",
128                                              self.buffer.get_start_iter(),
129                                              False)
130        insert = self.buffer.get_insert()
131        self.cursor.set_visible(True)
132        insert.set_visible(False)
133
134        self.ps = ''
135        self.in_raw_input = False
136        self.run_on_raw_input = None
137        self.tab_pressed = 0
138        self.history = _ReadLine.History()
139        self.nonword_re = re.compile("[^\w\._]")
140
141    def freeze_undo(self):
142        try: self.begin_not_undoable_action()
143        except: pass
144
145    def thaw_undo(self):
146        try: self.end_not_undoable_action()
147        except: pass
148
149    def raw_input(self, ps=None):
150        if ps:
151            self.ps = ps
152        else:
153            self.ps = ''
154
155        iter = self.buffer.get_iter_at_mark(self.buffer.get_insert())
156
157        if ps:
158            self.freeze_undo()
159            self.buffer.insert(iter, self.ps)
160            self.thaw_undo()
161
162        self.__move_cursor_to(iter)
163        self.scroll_to_mark(self.cursor, 0.2)
164
165        self.in_raw_input = True
166
167        if self.run_on_raw_input:
168            run_now = self.run_on_raw_input
169            self.run_on_raw_input = None
170            self.buffer.insert_at_cursor(run_now + '\n')
171
172    def on_buf_mark_set(self, buffer, iter, mark):
173        if mark is not buffer.get_insert():
174            return
175        start = self.__get_start()
176        end = self.__get_end()
177        if iter.compare(self.__get_start()) >= 0 and \
178           iter.compare(self.__get_end()) <= 0:
179                buffer.move_mark_by_name("cursor", iter)
180                self.scroll_to_mark(self.cursor, 0.2)
181
182    def __insert(self, iter, text):
183        self.do_insert = True
184        self.buffer.insert(iter, text)
185        self.do_insert = False
186
187    def on_buf_insert(self, buf, iter, text, len):
188        if not self.in_raw_input or self.do_insert or not len:
189            return
190        buf.stop_emission("insert-text")
191        lines = text.splitlines()
192        need_eol = False
193        for l in lines:
194            if need_eol:
195                self._commit()
196                iter = self.__get_cursor()
197            else:
198                cursor = self.__get_cursor()
199                if iter.compare(self.__get_start()) < 0:
200                    iter = cursor
201                elif iter.compare(self.__get_end()) > 0:
202                    iter = cursor
203                else:
204                    self.__move_cursor_to(iter)
205            need_eol = True
206            self.__insert(iter, l)
207        self.__move_cursor(0)
208
209    def __delete(self, start, end):
210        self.do_delete = True
211        self.buffer.delete(start, end)
212        self.do_delete = False
213
214    def on_buf_delete(self, buf, start, end):
215        if not self.in_raw_input or self.do_delete:
216            return
217
218        buf.stop_emission("delete-range")
219
220        start.order(end)
221        line_start = self.__get_start()
222        line_end = self.__get_end()
223
224        if start.compare(line_end) > 0:
225            return
226        if end.compare(line_start) < 0:
227            return
228
229        self.__move_cursor(0)
230
231        if start.compare(line_start) < 0:
232            start = line_start
233        if end.compare(line_end) > 0:
234            end = line_end
235        self.__delete(start, end)
236
237    def do_key_press_event(self, event, parent_type):
238        if not self.in_raw_input:
239            return parent_type.do_key_press_event(self, event)
240
241        tab_pressed = self.tab_pressed
242        self.tab_pressed = 0
243        handled = True
244
245        state = event.state & (gdk.SHIFT_MASK |
246                                gdk.CONTROL_MASK |
247                                gdk.MOD1_MASK)
248        keyval = event.keyval
249
250        if not state:
251            if keyval == _keys.Return:
252                self._commit()
253            elif keyval == _keys.Up:
254                self.__history(-1)
255            elif keyval == _keys.Down:
256                self.__history(1)
257            elif keyval == _keys.Left:
258                self.__move_cursor(-1)
259            elif keyval == _keys.Right:
260                self.__move_cursor(1)
261            elif keyval == _keys.Home:
262                self.__move_cursor(-10000)
263            elif keyval == _keys.End:
264                self.__move_cursor(10000)
265            elif keyval == _keys.Tab:
266                cursor = self.__get_cursor()
267                if cursor.starts_line():
268                    handled = False
269                else:
270                    cursor.backward_char()
271                    if cursor.get_char().isspace():
272                        handled = False
273                    else:
274                        self.tab_pressed = tab_pressed + 1
275                        self.__complete()
276            else:
277                handled = False
278        elif state == gdk.CONTROL_MASK:
279            if keyval == _keys.u:
280                start = self.__get_start()
281                end = self.__get_cursor()
282                self.__delete(start, end)
283            else:
284                handled = False
285        else:
286            handled = False
287
288        if not handled:
289            return parent_type.do_key_press_event(self, event)
290        else:
291            return True
292
293    def __history(self, dir):
294        text = self._get_line()
295        new_text = self.history.get(dir, text)
296        if not new_text is None:
297            self.__replace_line(new_text)
298        self.__move_cursor(0)
299        self.scroll_to_mark(self.cursor, 0.2)
300
301    def __get_cursor(self):
302        return self.buffer.get_iter_at_mark(self.cursor)
303    def __get_start(self):
304        iter = self.__get_cursor()
305        iter.set_line_offset(len(self.ps))
306        return iter
307    def __get_end(self):
308        iter = self.__get_cursor()
309        if not iter.ends_line():
310            iter.forward_to_line_end()
311        return iter
312
313    def __get_text(self, start, end):
314        return self.buffer.get_text(start, end, False)
315
316    def __move_cursor_to(self, iter):
317        self.buffer.place_cursor(iter)
318        self.buffer.move_mark_by_name("cursor", iter)
319
320    def __move_cursor(self, howmany):
321        iter = self.__get_cursor()
322        end = self.__get_cursor()
323        if not end.ends_line():
324            end.forward_to_line_end()
325        line_len = end.get_line_offset()
326        move_to = iter.get_line_offset() + howmany
327        move_to = min(max(move_to, len(self.ps)), line_len)
328        iter.set_line_offset(move_to)
329        self.__move_cursor_to(iter)
330
331    def __delete_at_cursor(self, howmany):
332        iter = self.__get_cursor()
333        end = self.__get_cursor()
334        if not end.ends_line():
335            end.forward_to_line_end()
336        line_len = end.get_line_offset()
337        erase_to = iter.get_line_offset() + howmany
338        if erase_to > line_len:
339            erase_to = line_len
340        elif erase_to < len(self.ps):
341            erase_to = len(self.ps)
342        end.set_line_offset(erase_to)
343        self.__delete(iter, end)
344
345    def __get_width(self):
346        if not (self.flags() & gtk.REALIZED):
347            return 80
348        layout = pango.Layout(self.get_pango_context())
349        letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
350        layout.set_text(letters)
351        pix_width = layout.get_pixel_size()[0]
352        return self.allocation.width * len(letters) / pix_width
353
354    def __print_completions(self, completions):
355        line_start = self.__get_text(self.__get_start(), self.__get_cursor())
356        line_end = self.__get_text(self.__get_cursor(), self.__get_end())
357        iter = self.buffer.get_end_iter()
358        self.__move_cursor_to(iter)
359        self.__insert(iter, "\n")
360
361        width = max(self.__get_width(), 4)
362        max_width = max([len(s) for s in completions])
363        n_columns = max(int(width / (max_width + 1)), 1)
364        col_width = int(width / n_columns)
365        total = len(completions)
366        col_length = total / n_columns
367        if total % n_columns:
368            col_length = col_length + 1
369        col_length = max(col_length, 1)
370
371        if col_length == 1:
372            n_columns = total
373            col_width = width / total
374
375        for i in range(col_length):
376            for j in range(n_columns):
377                ind = i + j*col_length
378                if ind < total:
379                    if j == n_columns - 1:
380                        n_spaces = 0
381                    else:
382                        n_spaces = col_width - len(completions[ind])
383                    self.__insert(iter, completions[ind] + " " * n_spaces)
384            self.__insert(iter, "\n")
385
386        self.__insert(iter, "%s%s%s" % (self.ps, line_start, line_end))
387        iter.set_line_offset(len(self.ps) + len(line_start))
388        self.__move_cursor_to(iter)
389        self.scroll_to_mark(self.cursor, 0.2)
390
391    def __complete(self):
392        text = self.__get_text(self.__get_start(), self.__get_cursor())
393        start = ''
394        word = text
395        nonwords = self.nonword_re.findall(text)
396        if nonwords:
397            last = text.rfind(nonwords[-1]) + len(nonwords[-1])
398            start = text[:last]
399            word = text[last:]
400
401        completions = self.complete(word)
402
403        if completions:
404            prefix = _commonprefix(completions)
405            if prefix != word:
406                start_iter = self.__get_start()
407                start_iter.forward_chars(len(start))
408                end_iter = start_iter.copy()
409                end_iter.forward_chars(len(word))
410                self.__delete(start_iter, end_iter)
411                self.__insert(end_iter, prefix)
412            elif self.tab_pressed > 1:
413                self.freeze_undo()
414                self.__print_completions(completions)
415                self.thaw_undo()
416                self.tab_pressed = 0
417
418    def complete(self, text):
419        return None
420
421    def _get_line(self):
422        start = self.__get_start()
423        end = self.__get_end()
424        return self.buffer.get_text(start, end, False)
425
426    def __replace_line(self, new_text):
427        start = self.__get_start()
428        end = self.__get_end()
429        self.__delete(start, end)
430        self.__insert(end, new_text)
431
432    def _commit(self):
433        end = self.__get_cursor()
434        if not end.ends_line():
435            end.forward_to_line_end()
436        text = self._get_line()
437        self.__move_cursor_to(end)
438        self.freeze_undo()
439        self.__insert(end, "\n")
440        self.in_raw_input = False
441        self.history.commit(text)
442        self.do_raw_input(text)
443        self.thaw_undo()
444
445    def do_raw_input(self, text):
446        pass
447
448
449class _Console(_ReadLine, code.InteractiveInterpreter):
450    def __init__(self, locals=None, banner=None,
451                 completer=None, use_rlcompleter=True,
452                 start_script=None):
453        _ReadLine.__init__(self)
454
455
456        code.InteractiveInterpreter.__init__(self, locals)
457        self.locals["__console__"] = self
458
459        self.start_script = start_script
460        self.completer = completer
461        self.banner = banner
462
463        if not self.completer and use_rlcompleter:
464            try:
465                import rlcompleter
466                self.completer = rlcompleter.Completer()
467            except ImportError:
468                pass
469
470        self.ps1 = ">>> "
471        self.ps2 = "... "
472        self.__start()
473        self.run_on_raw_input = start_script
474        self.raw_input(self.ps1)
475
476    def __start(self):
477        self.cmd_buffer = ""
478
479        self.freeze_undo()
480        self.thaw_undo()
481        self.buffer.set_text("")
482
483        if self.banner:
484            iter = self.buffer.get_start_iter()
485            self.buffer.insert_with_tags_by_name(iter, self.banner, "stdout")
486            if not iter.starts_line():
487                self.buffer.insert(iter, "\n")
488
489    def clear(self, start_script=None):
490        if start_script is None:
491            start_script = self.start_script
492        else:
493            self.start_script = start_script
494
495        self.__start()
496        self.run_on_raw_input = start_script
497
498    def do_raw_input(self, text):
499        if self.cmd_buffer:
500            cmd = self.cmd_buffer + "\n" + text
501        else:
502            cmd = text
503
504        saved_stdout, saved_stderr = sys.stdout, sys.stderr
505        sys.stdout, sys.stderr = self._stdout, self._stderr
506
507        if self.runsource(cmd):
508            self.cmd_buffer = cmd
509            ps = self.ps2
510        else:
511            self.cmd_buffer = ''
512            ps = self.ps1
513
514        sys.stdout, sys.stderr = saved_stdout, saved_stderr
515        self.raw_input(ps)
516
517    def do_command(self, code):
518        try:
519            eval(code, self.locals)
520# In GeanyPy console, we don't want to exit the process on SystemExit
521#        except SystemExit:
522#            raise
523        except:
524            self.showtraceback()
525
526    def runcode(self, code):
527        if gtk.pygtk_version[1] < 8:
528            self.do_command(code)
529        else:
530            self.emit("command", code)
531
532    def exec_command(self, command):
533        if self._get_line():
534            self._commit()
535        self.buffer.insert_at_cursor(command)
536        self._commit()
537
538    def complete_attr(self, start, end):
539        try:
540            obj = eval(start, self.locals)
541            strings = dir(obj)
542
543            if end:
544                completions = {}
545                for s in strings:
546                    if s.startswith(end):
547                        completions[s] = None
548                completions = completions.keys()
549            else:
550                completions = strings
551
552            completions.sort()
553            return [start + "." + s for s in completions]
554        except:
555            return None
556
557    def complete(self, text):
558        if self.completer:
559            completions = []
560            i = 0
561            try:
562                while 1:
563                    s = self.completer.complete(text, i)
564                    if s:
565                        completions.append(s)
566                        i = i + 1
567                    else:
568                        completions.sort()
569                        return completions
570            except NameError:
571                return None
572
573        dot = text.rfind(".")
574        if dot >= 0:
575            return self.complete_attr(text[:dot], text[dot+1:])
576
577        completions = {}
578        strings = keyword.kwlist
579
580        if self.locals:
581            strings.extend(self.locals.keys())
582
583        try: strings.extend(eval("globals()", self.locals).keys())
584        except: pass
585
586        try:
587            exec "import __builtin__" in self.locals
588            strings.extend(eval("dir(__builtin__)", self.locals))
589        except:
590            pass
591
592        for s in strings:
593            if s.startswith(text):
594                completions[s] = None
595        completions = completions.keys()
596        completions.sort()
597        return completions
598
599
600def ReadLineType(t=gtk.TextView):
601    class readline(t, _ReadLine):
602        def __init__(self, *args, **kwargs):
603            t.__init__(self)
604            _ReadLine.__init__(self, *args, **kwargs)
605        def do_key_press_event(self, event):
606            return _ReadLine.do_key_press_event(self, event, t)
607    gobject.type_register(readline)
608    return readline
609
610def ConsoleType(t=gtk.TextView):
611    class console_type(t, _Console):
612        __gsignals__ = {
613            'command' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (object,)),
614            'key-press-event' : 'override'
615          }
616
617        def __init__(self, *args, **kwargs):
618            if gtk.pygtk_version[1] < 8:
619                gobject.GObject.__init__(self)
620            else:
621                t.__init__(self)
622            _Console.__init__(self, *args, **kwargs)
623
624        def do_command(self, code):
625            return _Console.do_command(self, code)
626
627        def do_key_press_event(self, event):
628            return _Console.do_key_press_event(self, event, t)
629
630    if gtk.pygtk_version[1] < 8:
631        gobject.type_register(console_type)
632
633    return console_type
634
635ReadLine = ReadLineType()
636Console = ConsoleType()
637
638def _create_widget(start_script):
639
640    console = Console(banner="Geany Python Console",
641                          use_rlcompleter=False,
642                          start_script=start_script)
643    return console
644
645def _make_window(start_script="import geany\n"):
646    window = gtk.Window()
647    window.set_title("Python Console")
648    swin = gtk.ScrolledWindow()
649    swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
650    window.add(swin)
651    console = _create_widget(start_script)
652    swin.add(console)
653    window.set_default_size(500, 400)
654    window.show_all()
655
656    if not gtk.main_level():
657        window.connect("destroy", gtk.main_quit)
658        gtk.main()
659
660    return console
661
662if __name__ == '__main__':
663    import sys
664    import os
665    sys.path.insert(0, os.getcwd())
666    _make_window(sys.argv[1:] and '\n'.join(sys.argv[1:]) + '\n' or None)
667