| 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 |