1#!/usr/bin/env python
2
3#   Interactive Python-GTK Console
4#   Copyright (C),
5#   1998 James Henstridge
6#   2004 John Finlay
7#
8#    This program is free software; you can redistribute it and/or modify
9#   it under the terms of the GNU General Public License as published by
10#   the Free Software Foundation; either version 2 of the License, or
11#   (at your option) any later version.
12#
13#   This program 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 this program; if not, write to the Free Software
20#   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
21
22# This module implements an interactive python session in a GTK window.  To
23# start the session, use the gtk_console command.  Its specification is:
24#   gtk_console(namespace, title, copyright)
25# where namespace is a dictionary representing the namespace of the session,
26#       title     is the title on the window and
27#       copyright is any additional copyright info to print.
28#
29# As well as the starting attributes in namespace, the session will also
30# have access to the list __history__, which is the command history.
31
32import sys, string, traceback
33import pygtk
34pygtk.require('2.0')
35import gobject
36import gtk
37
38stdout = sys.stdout
39
40if not hasattr(sys, 'ps1'): sys.ps1 = '>>> '
41if not hasattr(sys, 'ps2'): sys.ps2 = '... '
42
43# some functions to help recognise breaks between commands
44def remQuotStr(s):
45    '''Returns s with any quoted strings removed (leaving quote marks)'''
46    r = ''
47    inq = 0
48    qt = ''
49    prev = '_'
50    while len(s):
51        s0, s = s[0], s[1:]
52        if inq and (s0 != qt or prev == '\\'):
53            prev = s0
54            continue
55        prev = s0
56        if s0 in '\'"':
57            if inq:
58                inq = 0
59            else:
60                inq = 1
61                qt = s0
62        r = r + s0
63    return r
64
65def bracketsBalanced(s):
66    '''Returns true iff the brackets in s are balanced'''
67    s = filter(lambda x: x in '()[]{}', s)
68    stack = []
69    brackets = {'(':')', '[':']', '{':'}'}
70    while len(s) != 0:
71        if s[0] in ")]}":
72            if len(stack) != 0 and brackets[stack[-1]] == s[0]:
73                del stack[-1]
74            else:
75                return 0
76        else:
77            stack.append(s[0])
78        s = s[1:]
79    return len(stack) == 0
80
81class gtkoutfile:
82    '''A fake output file object.  It sends output to a GTK TextView widget,
83    and if asked for a file number, returns one set on instance creation'''
84    def __init__(self, w, fn, font):
85        self.__fn = fn
86        self.__w = w
87        self.__b = w.get_buffer()
88        self.__ins = self.__b.get_mark('insert')
89        self.__font = font
90    def close(self): pass
91    flush = close
92    def fileno(self):    return self.__fn
93    def isatty(self):    return 0
94    def read(self, a):   return ''
95    def readline(self):  return ''
96    def readlines(self): return []
97    def write(self, s):
98        #stdout.write(str(self.__w.get_point()) + '\n')
99        iter = self.__b.get_iter_at_mark(self.__ins)
100        self.__b.insert_with_tags(iter, s, self.__font)
101        self.__w.scroll_to_mark(self.__ins, 0.0)
102        self.__w.queue_draw()
103    def writelines(self, l):
104        iter = self.__b.get_iter_at_mark(self.__ins)
105        for s in l:
106            self.__b.insert_with_tags(iter, s, self.__font)
107        self.__w.scroll_to_mark(self.__ins, 0.0)
108        self.__w.queue_draw()
109    def seek(self, a):   raise IOError, (29, 'Illegal seek')
110    def tell(self):      raise IOError, (29, 'Illegal seek')
111    truncate = tell
112
113class Console(gtk.VBox):
114    def __init__(self, namespace={}, copyright='', quit_cb=None):
115        gtk.VBox.__init__(self, spacing=2)
116        self.set_border_width(2)
117        self.copyright = copyright
118        #self.set_size_request(475, 300)
119
120        self.quit_cb = quit_cb
121
122        self.inp = gtk.HBox()
123        self.pack_start(self.inp)
124        self.inp.show()
125
126        self.scrolledwin = gtk.ScrolledWindow()
127        self.scrolledwin.show()
128        self.inp.pack_start(self.scrolledwin, padding=1)
129        self.text = gtk.TextView()
130        self.text.set_editable(False)
131        self.text.set_wrap_mode(gtk.WRAP_WORD)
132        self.text.set_size_request(500, 400)
133        self.scrolledwin.add(self.text)
134        self.text.show()
135        self.buffer = self.text.get_buffer()
136        #create the tags we will use
137        self.normal = self.buffer.create_tag('Normal', font='Helvetica 10',
138                                            foreground='black')
139        self.title = self.buffer.create_tag('Title', font='Helvetica Bold 10',
140                                            foreground='darkgreen')
141        self.error = self.buffer.create_tag('Error', font='Helvetica 12',
142                                           foreground='red')
143        self.command = self.buffer.create_tag('Command',
144                                             font='Helvetica Bold 10',
145                                            foreground='blue')
146
147        self.inputbox = gtk.HBox(spacing=2)
148        self.pack_end(self.inputbox, expand=False)
149        self.inputbox.show()
150
151        self.prompt = gtk.Label(sys.ps1)
152        self.prompt.set_padding(xpad=2, ypad=0)
153        self.prompt.set_size_request(26, -1)
154        self.inputbox.pack_start(self.prompt, fill=False, expand=False)
155        self.prompt.show()
156
157        self.closer = gtk.Button(stock=gtk.STOCK_CLOSE)
158        self.closer.connect("clicked", self.quit)
159        self.inputbox.pack_end(self.closer, fill=False, expand=False)
160        self.closer.show()
161
162        self.line = gtk.Entry()
163        self.line.set_size_request(400,-1)
164        self.line.connect("key_press_event", self.key_function)
165        self.inputbox.pack_start(self.line, padding=2)
166        self.line.show()
167
168        # now let the text box be resized
169        self.text.set_size_request(0, 0)
170        self.line.set_size_request(0, -1)
171
172        self.namespace = namespace
173
174        self.cmd = ''
175        self.cmd2 = ''
176
177        # set up hooks for standard output.
178        self.stdout = gtkoutfile(self.text, sys.stdout.fileno(),
179                                self.normal)
180        self.stderr = gtkoutfile(self.text, sys.stderr.fileno(),
181                                self.error)
182
183        # set up command history
184        self.history = ['']
185        self.histpos = 0
186        self.namespace['__history__'] = self.history
187
188    def init(self):
189        self.text.realize()
190        self.insert = self.buffer.get_mark('insert')
191        iter = self.buffer.get_iter_at_mark(self.insert)
192        self.buffer.insert_with_tags(iter, 'Python %s\n%s\n\n' %
193                                     (sys.version, sys.copyright) +
194                                     'Interactive Python-GTK Console - \n' +
195                                     'Copyright (C)\n' \
196                                     '1998 James Henstridge\n' \
197                                     '2004 John Finlay\n\n' +
198                                     self.copyright + '\n', self.title)
199        self.text.scroll_to_mark(self.insert, 0.0)
200        self.line.grab_focus()
201
202    def quit(self, *args):
203        self.hide()
204        self.destroy()
205        if self.quit_cb: self.quit_cb()
206
207    def key_function(self, entry, event):
208        if event.keyval == gtk.keysyms.Return:
209            self.line.emit_stop_by_name("key_press_event")
210            self.eval()
211        if event.keyval == gtk.keysyms.Tab:
212            self.line.emit_stop_by_name("key_press_event")
213            self.line.append_text('\t')
214            gobject.idle_add(self.focus_text)
215        elif event.keyval in (gtk.keysyms.KP_Up, gtk.keysyms.Up):
216            self.line.emit_stop_by_name("key_press_event")
217            self.historyUp()
218            gobject.idle_add(self.focus_text)
219        elif event.keyval in (gtk.keysyms.KP_Down, gtk.keysyms.Down):
220            self.line.emit_stop_by_name("key_press_event")
221            self.historyDown()
222            gobject.idle_add(self.focus_text)
223        elif event.keyval in (gtk.keysyms.D, gtk.keysyms.d) and \
224             event.state & gtk.gdk.CONTROL_MASK:
225            self.line.emit_stop_by_name("key_press_event")
226            self.ctrld()
227
228    def focus_text(self):
229        self.line.grab_focus()
230        return False  # don't requeue this handler
231
232    def ctrld(self):
233        self.quit()
234
235    def historyUp(self):
236        if self.histpos > 0:
237            l = self.line.get_text()
238            if len(l) > 0 and l[0] == '\n': l = l[1:]
239            if len(l) > 0 and l[-1] == '\n': l = l[:-1]
240            self.history[self.histpos] = l
241            self.histpos = self.histpos - 1
242            self.line.set_text(self.history[self.histpos])
243
244    def historyDown(self):
245        if self.histpos < len(self.history) - 1:
246            l = self.line.get_text()
247            if len(l) > 0 and l[0] == '\n': l = l[1:]
248            if len(l) > 0 and l[-1] == '\n': l = l[:-1]
249            self.history[self.histpos] = l
250            self.histpos = self.histpos + 1
251            self.line.set_text(self.history[self.histpos])
252
253    def eval(self):
254        l = self.line.get_text() + '\n'
255        if len(l) > 1 and l[0] == '\n': l = l[1:]
256        self.histpos = len(self.history) - 1
257        if len(l) > 0 and l[-1] == '\n':
258            self.history[self.histpos] = l[:-1]
259        else:
260            self.history[self.histpos] = l
261        self.line.set_text('')
262        iter = self.buffer.get_iter_at_mark(self.insert)
263        self.buffer.insert_with_tags(iter, self.prompt.get() + l, self.command)
264        self.text.scroll_to_mark(self.insert, 0.0)
265        if l == '\n':
266            self.run(self.cmd)
267            self.cmd = ''
268            self.cmd2 = ''
269            return
270        self.histpos = self.histpos + 1
271        self.history.append('')
272        self.cmd = self.cmd + l
273        self.cmd2 = self.cmd2 + remQuotStr(l)
274        l = string.rstrip(l)
275        if not bracketsBalanced(self.cmd2) or l[-1] == ':' or \
276                        l[-1] == '\\' or l[0] in ' \11':
277            self.prompt.set_text(sys.ps2)
278            self.prompt.queue_draw()
279            return
280        self.run(self.cmd)
281        self.cmd = ''
282        self.cmd2 = ''
283
284    def run(self, cmd):
285        sys.stdout, self.stdout = self.stdout, sys.stdout
286        sys.stderr, self.stderr = self.stderr, sys.stderr
287        try:
288            try:
289                r = eval(cmd, self.namespace, self.namespace)
290                if r is not None:
291                    print `r`
292            except SyntaxError:
293                exec cmd in self.namespace
294        except:
295            if hasattr(sys, 'last_type') and \
296                            sys.last_type == SystemExit:
297                self.quit()
298            else:
299                traceback.print_exc()
300        self.prompt.set_text(sys.ps1)
301        self.prompt.queue_draw()
302        sys.stdout, self.stdout = self.stdout, sys.stdout
303        sys.stderr, self.stderr = self.stderr, sys.stderr
304
305def gtk_console(ns, title='Python', copyright='', menu=None):
306    win = gtk.Window()
307    win.set_size_request(475, 300)
308    win.connect("destroy", lambda w: gtk.main_quit())
309    win.connect("delete_event", lambda w,e: gtk.main_quit())
310    win.set_title(title)
311    cons = Console(namespace=ns, copyright=copyright,
312                   quit_cb=lambda w: gtk.main_quit())
313    if menu:
314        box = gtk.VBox()
315        win.add(box)
316        box.show()
317        box.pack_start(menu, expand=False)
318        menu.show()
319        box.pack_start(cons)
320    else:
321        win.add(cons)
322    cons.show()
323    win.show()
324    win.set_size_request(0,0)
325    cons.init()
326    gtk.main()
327
328if __name__ == '__main__':
329    gtk_console({'__builtins__': __builtins__, '__name__': '__main__',
330            '__doc__': None})
331