1 # -*- test-case-name: twisted.manhole.ui.test.test_gtk2manhole -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
6 Manhole client with a GTK v2.x front-end.
9 __version__ = '$Revision: 1.9 $'[11:-2]
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
18 from twisted.manhole.service import IManholeClient
19 from zope.interface import implements
21 # The pygtk.require for version 2.0 has already been done by the reactor.
24 import code, types, inspect
27 # Make wrap-mode a run-time option.
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.
33 class OfflineError(Exception):
36 class ManholeWindow(components.Componentized, gtk2util.GladeKeeper):
37 gladefile = util.sibpath(__file__, "gtk2manhole.glade")
39 _widgets = ('input','output','manholeWindow')
43 gtk2util.GladeKeeper.__init__(self)
44 components.Componentized.__init__(self)
46 self.input = ConsoleInput(self._input)
47 self.input.toplevel = self
48 self.output = ConsoleOutput(self._output)
50 # Ugh. GladeKeeper actually isn't so good for composite objects.
51 # I want this connected to the ConsoleInput's handler, not something
53 self._input.connect("key_press_event", self.input._on_key_press_event)
55 def setDefaults(self, defaults):
56 self.defaults = defaults
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)
65 def _cbDisconnected(self, perspective):
66 self.output.append("%s went away. :(\n" % (perspective,), "local")
67 self._manholeWindow.set_title("Manhole")
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))
76 def _ebLogin(self, reason):
77 self.output.append("Login FAILED %s\n" % (reason.value,), "exception")
79 def _on_aboutMenuItem_activate(self, widget, *unused):
82 self.output.append("""\
83 a Twisted Manhole client
86 Python %(pythonVer)s on %(platform)s
87 GTK %(gtkVer)s / PyGTK %(pygtkVer)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__,
99 def _on_openMenuItem_activate(self, widget, userdata=None):
102 def _on_manholeWindow_delete_event(self, widget, *unused):
105 def _on_quitMenuItem_activate(self, widget, *unused):
108 def on_reload_self_activate(self, *unused):
109 from twisted.python import rebuild
110 rebuild.rebuild(inspect.getmodule(self.__class__))
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"},
126 # TODO: Factor Python console stuff back out to pywidgets.
130 def __init__(self, textView):
131 self.textView = textView
132 self.buffer = textView.get_buffer()
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)
141 self.buffer.tag_table.lookup("default").set_priority(0)
143 self._captureLocalLog()
145 def _captureLocalLog(self):
146 return log.startLogging(_Notafile(self, "log"), setStdout=False)
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?
155 self.buffer.insert_with_tags_by_name(self.buffer.get_end_iter(),
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)
162 def _scrollDown(self, *unused):
163 self.textView.scroll_to_iter(self.buffer.get_end_iter(), 0,
165 self._willScroll = None
169 def __init__(self, maxhist=10000):
170 self.ringbuffer = ['']
171 self.maxhist = maxhist
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] = ''
181 def move(self, prevnext=1):
183 Return next/previous item in the history, stopping at top/bottom.
185 hcpn = self.histCursor + prevnext
186 if hcpn >= 0 and hcpn < len(self.ringbuffer):
187 self.histCursor = hcpn
188 return self.ringbuffer[hcpn]
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)
199 textbuffer.set_text(newtext)
201 def histdown(self, textbuffer):
202 newtext = self.move(1)
205 textbuffer.set_text(newtext)
209 toplevel, rkeymap = None, None
212 def __init__(self, textView):
213 self.textView=textView
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
220 def _on_key_press_event(self, entry, event):
222 ksym = self.rkeymap.get(event.keyval, None)
225 for prefix, mask in [('ctrl', gtk.gdk.CONTROL_MASK), ('shift', gtk.gdk.SHIFT_MASK)]:
226 if event.state & mask:
230 ksym = '_'.join(mods + [ksym])
234 self, 'key_%s' % ksym, lambda *a, **kw: None)(entry, event)
241 buffer = self.textView.get_buffer()
242 iter1, iter2 = buffer.get_bounds()
243 text = buffer.get_text(iter1, iter2, False)
246 def setText(self, text):
247 self.textView.get_buffer().set_text(text)
249 def key_Return(self, entry, event):
250 text = self.getText()
251 # Figure out if that Return meant "next line" or "execute."
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
258 point = buffer.get_iter_at_line_offset(e.lineno, e.offset)
260 # TODO: Componentize!
261 self.toplevel.output.append(str(e), "exception")
262 except (OverflowError, ValueError), e:
263 self.toplevel.output.append(str(e), "exception")
267 # Don't insert Return as a newline in the buffer.
268 self.history.append(text)
270 # entry.emit_stop_by_name("key_press_event")
273 # not a complete code block
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)
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)
295 key_ctrl_n = key_Down
297 def key_ctrl_shift_F9(self, entry, event):
299 import pdb; pdb.set_trace()
302 buffer = self.textView.get_buffer()
303 buffer.delete(*buffer.get_bounds())
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!
312 return self.toplevel.getComponent(IManholeClient).do(text)
314 self.toplevel.output.append("Not connected, command not sent.\n",
320 Make some text appear as though it was typed in at a Python prompt.
322 lines = text.split('\n')
323 lines[0] = '>>> ' + lines[0]
324 return '\n... '.join(lines) + '\n'
327 """Curry to make failure.printTraceback work with the output widget."""
328 def __init__(self, output, kind):
332 def write(self, txt):
333 self.output.append(txt, self.kind)
338 class ManholeClient(components.Adapter, pb.Referenceable):
339 implements(IManholeClient)
346 def _cbLogin(self, perspective):
347 self.perspective = perspective
348 perspective.notifyOnDisconnect(self._cbDisconnected)
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,
359 self.original.output.append(str(content), kind)
361 def remote_receiveExplorer(self, xplorer):
364 def remote_listCapabilities(self):
365 return self.capabilities
367 def _cbDisconnected(self, perspective):
368 self.perspective = None
371 if self.perspective is None:
373 return self.perspective.callRemote("do", text)
375 components.registerAdapter(ManholeClient, ManholeWindow, IManholeClient)