Initial import to Tizen
[profile/ivi/python-twisted.git] / twisted / manhole / ui / gtk2manhole.py
1 # -*- test-case-name: twisted.manhole.ui.test.test_gtk2manhole -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
4
5 """
6 Manhole client with a GTK v2.x front-end.
7 """
8
9 __version__ = '$Revision: 1.9 $'[11:-2]
10
11 from twisted import copyright
12 from twisted.internet import reactor
13 from twisted.python import components, failure, log, util
14 from twisted.python.reflect import prefixedMethodNames
15 from twisted.spread import pb
16 from twisted.spread.ui import gtk2util
17
18 from twisted.manhole.service import IManholeClient
19 from zope.interface import implements
20
21 # The pygtk.require for version 2.0 has already been done by the reactor.
22 import gtk
23
24 import code, types, inspect
25
26 # TODO:
27 #  Make wrap-mode a run-time option.
28 #  Explorer.
29 #  Code doesn't cleanly handle opening a second connection.  Fix that.
30 #  Make some acknowledgement of when a command has completed, even if
31 #     it has no return value so it doesn't print anything to the console.
32
33 class OfflineError(Exception):
34     pass
35
36 class ManholeWindow(components.Componentized, gtk2util.GladeKeeper):
37     gladefile = util.sibpath(__file__, "gtk2manhole.glade")
38
39     _widgets = ('input','output','manholeWindow')
40
41     def __init__(self):
42         self.defaults = {}
43         gtk2util.GladeKeeper.__init__(self)
44         components.Componentized.__init__(self)
45
46         self.input = ConsoleInput(self._input)
47         self.input.toplevel = self
48         self.output = ConsoleOutput(self._output)
49
50         # Ugh.  GladeKeeper actually isn't so good for composite objects.
51         # I want this connected to the ConsoleInput's handler, not something
52         # on this class.
53         self._input.connect("key_press_event", self.input._on_key_press_event)
54
55     def setDefaults(self, defaults):
56         self.defaults = defaults
57
58     def login(self):
59         client = self.getComponent(IManholeClient)
60         d = gtk2util.login(client, **self.defaults)
61         d.addCallback(self._cbLogin)
62         d.addCallback(client._cbLogin)
63         d.addErrback(self._ebLogin)
64
65     def _cbDisconnected(self, perspective):
66         self.output.append("%s went away. :(\n" % (perspective,), "local")
67         self._manholeWindow.set_title("Manhole")
68
69     def _cbLogin(self, perspective):
70         peer = perspective.broker.transport.getPeer()
71         self.output.append("Connected to %s\n" % (peer,), "local")
72         perspective.notifyOnDisconnect(self._cbDisconnected)
73         self._manholeWindow.set_title("Manhole - %s" % (peer))
74         return perspective
75
76     def _ebLogin(self, reason):
77         self.output.append("Login FAILED %s\n" % (reason.value,), "exception")
78
79     def _on_aboutMenuItem_activate(self, widget, *unused):
80         import sys
81         from os import path
82         self.output.append("""\
83 a Twisted Manhole client
84   Versions:
85     %(twistedVer)s
86     Python %(pythonVer)s on %(platform)s
87     GTK %(gtkVer)s / PyGTK %(pygtkVer)s
88     %(module)s %(modVer)s
89 http://twistedmatrix.com/
90 """ % {'twistedVer': copyright.longversion,
91        'pythonVer': sys.version.replace('\n', '\n      '),
92        'platform': sys.platform,
93        'gtkVer': ".".join(map(str, gtk.gtk_version)),
94        'pygtkVer': ".".join(map(str, gtk.pygtk_version)),
95        'module': path.basename(__file__),
96        'modVer': __version__,
97        }, "local")
98
99     def _on_openMenuItem_activate(self, widget, userdata=None):
100         self.login()
101
102     def _on_manholeWindow_delete_event(self, widget, *unused):
103         reactor.stop()
104
105     def _on_quitMenuItem_activate(self, widget, *unused):
106         reactor.stop()
107
108     def on_reload_self_activate(self, *unused):
109         from twisted.python import rebuild
110         rebuild.rebuild(inspect.getmodule(self.__class__))
111
112
113 tagdefs = {
114     'default': {"family": "monospace"},
115     # These are message types we get from the server.
116     'stdout': {"foreground": "black"},
117     'stderr': {"foreground": "#AA8000"},
118     'result': {"foreground": "blue"},
119     'exception': {"foreground": "red"},
120     # Messages generate locally.
121     'local': {"foreground": "#008000"},
122     'log': {"foreground": "#000080"},
123     'command': {"foreground": "#666666"},
124     }
125
126 # TODO: Factor Python console stuff back out to pywidgets.
127
128 class ConsoleOutput:
129     _willScroll = None
130     def __init__(self, textView):
131         self.textView = textView
132         self.buffer = textView.get_buffer()
133
134         # TODO: Make this a singleton tag table.
135         for name, props in tagdefs.iteritems():
136             tag = self.buffer.create_tag(name)
137             # This can be done in the constructor in newer pygtk (post 1.99.14)
138             for k, v in props.iteritems():
139                 tag.set_property(k, v)
140
141         self.buffer.tag_table.lookup("default").set_priority(0)
142
143         self._captureLocalLog()
144
145     def _captureLocalLog(self):
146         return log.startLogging(_Notafile(self, "log"), setStdout=False)
147
148     def append(self, text, kind=None):
149         # XXX: It seems weird to have to do this thing with always applying
150         # a 'default' tag.  Can't we change the fundamental look instead?
151         tags = ["default"]
152         if kind is not None:
153             tags.append(kind)
154
155         self.buffer.insert_with_tags_by_name(self.buffer.get_end_iter(),
156                                              text, *tags)
157         # Silly things, the TextView needs to update itself before it knows
158         # where the bottom is.
159         if self._willScroll is None:
160             self._willScroll = gtk.idle_add(self._scrollDown)
161
162     def _scrollDown(self, *unused):
163         self.textView.scroll_to_iter(self.buffer.get_end_iter(), 0,
164                                      True, 1.0, 1.0)
165         self._willScroll = None
166         return False
167
168 class History:
169     def __init__(self, maxhist=10000):
170         self.ringbuffer = ['']
171         self.maxhist = maxhist
172         self.histCursor = 0
173
174     def append(self, htext):
175         self.ringbuffer.insert(-1, htext)
176         if len(self.ringbuffer) > self.maxhist:
177             self.ringbuffer.pop(0)
178         self.histCursor = len(self.ringbuffer) - 1
179         self.ringbuffer[-1] = ''
180
181     def move(self, prevnext=1):
182         '''
183         Return next/previous item in the history, stopping at top/bottom.
184         '''
185         hcpn = self.histCursor + prevnext
186         if hcpn >= 0 and hcpn < len(self.ringbuffer):
187             self.histCursor = hcpn
188             return self.ringbuffer[hcpn]
189         else:
190             return None
191
192     def histup(self, textbuffer):
193         if self.histCursor == len(self.ringbuffer) - 1:
194             si, ei = textbuffer.get_start_iter(), textbuffer.get_end_iter()
195             self.ringbuffer[-1] = textbuffer.get_text(si,ei)
196         newtext = self.move(-1)
197         if newtext is None:
198             return
199         textbuffer.set_text(newtext)
200
201     def histdown(self, textbuffer):
202         newtext = self.move(1)
203         if newtext is None:
204             return
205         textbuffer.set_text(newtext)
206
207
208 class ConsoleInput:
209     toplevel, rkeymap = None, None
210     __debug = False
211
212     def __init__(self, textView):
213         self.textView=textView
214         self.rkeymap = {}
215         self.history = History()
216         for name in prefixedMethodNames(self.__class__, "key_"):
217             keysymName = name.split("_")[-1]
218             self.rkeymap[getattr(gtk.keysyms, keysymName)] = keysymName
219
220     def _on_key_press_event(self, entry, event):
221         stopSignal = False
222         ksym = self.rkeymap.get(event.keyval, None)
223
224         mods = []
225         for prefix, mask in [('ctrl', gtk.gdk.CONTROL_MASK), ('shift', gtk.gdk.SHIFT_MASK)]:
226             if event.state & mask:
227                 mods.append(prefix)
228
229         if mods:
230             ksym = '_'.join(mods + [ksym])
231
232         if ksym:
233             rvalue = getattr(
234                 self, 'key_%s' % ksym, lambda *a, **kw: None)(entry, event)
235
236         if self.__debug:
237             print ksym
238         return rvalue
239
240     def getText(self):
241         buffer = self.textView.get_buffer()
242         iter1, iter2 = buffer.get_bounds()
243         text = buffer.get_text(iter1, iter2, False)
244         return text
245
246     def setText(self, text):
247         self.textView.get_buffer().set_text(text)
248
249     def key_Return(self, entry, event):
250         text = self.getText()
251         # Figure out if that Return meant "next line" or "execute."
252         try:
253             c = code.compile_command(text)
254         except SyntaxError, e:
255             # This could conceivably piss you off if the client's python
256             # doesn't accept keywords that are known to the manhole's
257             # python.
258             point = buffer.get_iter_at_line_offset(e.lineno, e.offset)
259             buffer.place(point)
260             # TODO: Componentize!
261             self.toplevel.output.append(str(e), "exception")
262         except (OverflowError, ValueError), e:
263             self.toplevel.output.append(str(e), "exception")
264         else:
265             if c is not None:
266                 self.sendMessage()
267                 # Don't insert Return as a newline in the buffer.
268                 self.history.append(text)
269                 self.clear()
270                 # entry.emit_stop_by_name("key_press_event")
271                 return True
272             else:
273                 # not a complete code block
274                 return False
275
276         return False
277
278     def key_Up(self, entry, event):
279         # if I'm at the top, previous history item.
280         textbuffer = self.textView.get_buffer()
281         if textbuffer.get_iter_at_mark(textbuffer.get_insert()).get_line() == 0:
282             self.history.histup(textbuffer)
283             return True
284         return False
285
286     def key_Down(self, entry, event):
287         textbuffer = self.textView.get_buffer()
288         if textbuffer.get_iter_at_mark(textbuffer.get_insert()).get_line() == (
289             textbuffer.get_line_count() - 1):
290             self.history.histdown(textbuffer)
291             return True
292         return False
293
294     key_ctrl_p = key_Up
295     key_ctrl_n = key_Down
296
297     def key_ctrl_shift_F9(self, entry, event):
298         if self.__debug:
299             import pdb; pdb.set_trace()
300
301     def clear(self):
302         buffer = self.textView.get_buffer()
303         buffer.delete(*buffer.get_bounds())
304
305     def sendMessage(self):
306         buffer = self.textView.get_buffer()
307         iter1, iter2 = buffer.get_bounds()
308         text = buffer.get_text(iter1, iter2, False)
309         self.toplevel.output.append(pythonify(text), 'command')
310         # TODO: Componentize better!
311         try:
312             return self.toplevel.getComponent(IManholeClient).do(text)
313         except OfflineError:
314             self.toplevel.output.append("Not connected, command not sent.\n",
315                                         "exception")
316
317
318 def pythonify(text):
319     '''
320     Make some text appear as though it was typed in at a Python prompt.
321     '''
322     lines = text.split('\n')
323     lines[0] = '>>> ' + lines[0]
324     return '\n... '.join(lines) + '\n'
325
326 class _Notafile:
327     """Curry to make failure.printTraceback work with the output widget."""
328     def __init__(self, output, kind):
329         self.output = output
330         self.kind = kind
331
332     def write(self, txt):
333         self.output.append(txt, self.kind)
334
335     def flush(self):
336         pass
337
338 class ManholeClient(components.Adapter, pb.Referenceable):
339     implements(IManholeClient)
340
341     capabilities = {
342 #        "Explorer": 'Set',
343         "Failure": 'Set'
344         }
345
346     def _cbLogin(self, perspective):
347         self.perspective = perspective
348         perspective.notifyOnDisconnect(self._cbDisconnected)
349         return perspective
350
351     def remote_console(self, messages):
352         for kind, content in messages:
353             if isinstance(content, types.StringTypes):
354                 self.original.output.append(content, kind)
355             elif (kind == "exception") and isinstance(content, failure.Failure):
356                 content.printTraceback(_Notafile(self.original.output,
357                                                  "exception"))
358             else:
359                 self.original.output.append(str(content), kind)
360
361     def remote_receiveExplorer(self, xplorer):
362         pass
363
364     def remote_listCapabilities(self):
365         return self.capabilities
366
367     def _cbDisconnected(self, perspective):
368         self.perspective = None
369
370     def do(self, text):
371         if self.perspective is None:
372             raise OfflineError
373         return self.perspective.callRemote("do", text)
374
375 components.registerAdapter(ManholeClient, ManholeWindow, IManholeClient)