OLD | NEW |
| (Empty) |
1 # -*- Python -*- | |
2 # $Id: gtk2manhole.py,v 1.9 2003/09/07 19:58:09 acapnotic Exp $ | |
3 # Copyright (c) 2001-2004 Twisted Matrix Laboratories. | |
4 # See LICENSE for details. | |
5 | |
6 | |
7 """Manhole client with a GTK v2.x front-end. | |
8 """ | |
9 | |
10 __version__ = '$Revision: 1.9 $'[11:-2] | |
11 | |
12 from twisted import copyright | |
13 from twisted.internet import reactor | |
14 from twisted.python import components, failure, log, util | |
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 dir(gtk.keysyms): | |
217 try: | |
218 self.rkeymap[getattr(gtk.keysyms, name)] = name | |
219 except TypeError: | |
220 pass | |
221 | |
222 def _on_key_press_event(self, entry, event): | |
223 stopSignal = False | |
224 ksym = self.rkeymap.get(event.keyval, None) | |
225 | |
226 mods = [] | |
227 for prefix, mask in [('ctrl', gtk.gdk.CONTROL_MASK), ('shift', gtk.gdk.S
HIFT_MASK)]: | |
228 if event.state & mask: | |
229 mods.append(prefix) | |
230 | |
231 if mods: | |
232 ksym = '_'.join(mods + [ksym]) | |
233 | |
234 if ksym: | |
235 rvalue = getattr( | |
236 self, 'key_%s' % ksym, lambda *a, **kw: None)(entry, event) | |
237 | |
238 if self.__debug: | |
239 print ksym | |
240 return rvalue | |
241 | |
242 def getText(self): | |
243 buffer = self.textView.get_buffer() | |
244 iter1, iter2 = buffer.get_bounds() | |
245 text = buffer.get_text(iter1, iter2, False) | |
246 return text | |
247 | |
248 def setText(self, text): | |
249 self.textView.get_buffer().set_text(text) | |
250 | |
251 def key_Return(self, entry, event): | |
252 text = self.getText() | |
253 # Figure out if that Return meant "next line" or "execute." | |
254 try: | |
255 c = code.compile_command(text) | |
256 except SyntaxError, e: | |
257 # This could conceivably piss you off if the client's python | |
258 # doesn't accept keywords that are known to the manhole's | |
259 # python. | |
260 point = buffer.get_iter_at_line_offset(e.lineno, e.offset) | |
261 buffer.place(point) | |
262 # TODO: Componentize! | |
263 self.toplevel.output.append(str(e), "exception") | |
264 except (OverflowError, ValueError), e: | |
265 self.toplevel.output.append(str(e), "exception") | |
266 else: | |
267 if c is not None: | |
268 self.sendMessage() | |
269 # Don't insert Return as a newline in the buffer. | |
270 self.history.append(text) | |
271 self.clear() | |
272 # entry.emit_stop_by_name("key_press_event") | |
273 return True | |
274 else: | |
275 # not a complete code block | |
276 return False | |
277 | |
278 return False | |
279 | |
280 def key_Up(self, entry, event): | |
281 # if I'm at the top, previous history item. | |
282 textbuffer = self.textView.get_buffer() | |
283 if textbuffer.get_iter_at_mark(textbuffer.get_insert()).get_line() == 0: | |
284 self.history.histup(textbuffer) | |
285 return True | |
286 return False | |
287 | |
288 def key_Down(self, entry, event): | |
289 textbuffer = self.textView.get_buffer() | |
290 if textbuffer.get_iter_at_mark(textbuffer.get_insert()).get_line() == ( | |
291 textbuffer.get_line_count() - 1): | |
292 self.history.histdown(textbuffer) | |
293 return True | |
294 return False | |
295 | |
296 key_ctrl_p = key_Up | |
297 key_ctrl_n = key_Down | |
298 | |
299 def key_ctrl_shift_F9(self, entry, event): | |
300 if self.__debug: | |
301 import pdb; pdb.set_trace() | |
302 | |
303 def clear(self): | |
304 buffer = self.textView.get_buffer() | |
305 buffer.delete(*buffer.get_bounds()) | |
306 | |
307 def sendMessage(self): | |
308 buffer = self.textView.get_buffer() | |
309 iter1, iter2 = buffer.get_bounds() | |
310 text = buffer.get_text(iter1, iter2, False) | |
311 self.toplevel.output.append(pythonify(text), 'command') | |
312 # TODO: Componentize better! | |
313 try: | |
314 return self.toplevel.getComponent(IManholeClient).do(text) | |
315 except OfflineError: | |
316 self.toplevel.output.append("Not connected, command not sent.\n", | |
317 "exception") | |
318 | |
319 | |
320 def pythonify(text): | |
321 ''' | |
322 Make some text appear as though it was typed in at a Python prompt. | |
323 ''' | |
324 lines = text.split('\n') | |
325 lines[0] = '>>> ' + lines[0] | |
326 return '\n... '.join(lines) + '\n' | |
327 | |
328 class _Notafile: | |
329 """Curry to make failure.printTraceback work with the output widget.""" | |
330 def __init__(self, output, kind): | |
331 self.output = output | |
332 self.kind = kind | |
333 | |
334 def write(self, txt): | |
335 self.output.append(txt, self.kind) | |
336 | |
337 def flush(self): | |
338 pass | |
339 | |
340 class ManholeClient(components.Adapter, pb.Referenceable): | |
341 implements(IManholeClient) | |
342 | |
343 capabilities = { | |
344 # "Explorer": 'Set', | |
345 "Failure": 'Set' | |
346 } | |
347 | |
348 def _cbLogin(self, perspective): | |
349 self.perspective = perspective | |
350 perspective.notifyOnDisconnect(self._cbDisconnected) | |
351 return perspective | |
352 | |
353 def remote_console(self, messages): | |
354 for kind, content in messages: | |
355 if isinstance(content, types.StringTypes): | |
356 self.original.output.append(content, kind) | |
357 elif (kind == "exception") and isinstance(content, failure.Failure): | |
358 content.printTraceback(_Notafile(self.original.output, | |
359 "exception")) | |
360 else: | |
361 self.original.output.append(str(content), kind) | |
362 | |
363 def remote_receiveExplorer(self, xplorer): | |
364 pass | |
365 | |
366 def remote_listCapabilities(self): | |
367 return self.capabilities | |
368 | |
369 def _cbDisconnected(self, perspective): | |
370 self.perspective = None | |
371 | |
372 def do(self, text): | |
373 if self.perspective is None: | |
374 raise OfflineError | |
375 return self.perspective.callRemote("do", text) | |
376 | |
377 components.registerAdapter(ManholeClient, ManholeWindow, IManholeClient) | |
OLD | NEW |