1
# -*- test-case-name: twisted.manhole.ui.test.test_gtk2manhole -*-
2
# Copyright (c) 2001-2009 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)