1#!/usr/bin/env python 2# 3# pyconsole.py 4# 5# Copyright (C) 2004-2010 by Yevgen Muntyan <emuntyan@users.sourceforge.net> 6# Thanks to Geoffrey French for ideas. 7# 8# This file is part of medit. medit is free software; you can 9# redistribute it and/or modify it under the terms of the 10# GNU Lesser General Public License as published by the 11# Free Software Foundation; either version 2.1 of the License, 12# or (at your option) any later version. 13# 14# You should have received a copy of the GNU Lesser General Public 15# License along with medit. If not, see <http://www.gnu.org/licenses/>. 16# 17 18# This module 'runs' python interpreter in a TextView widget. 19# The main class is Console, usage is: 20# Console(locals=None, banner=None, completer=None, use_rlcompleter=True, start_script='') - 21# it creates the widget and 'starts' interactive session; see the end of 22# this file. If start_script is not empty, it pastes it as it was entered from keyboard. 23# 24# Console has "command" signal which is emitted when code is about to 25# be executed. You may connect to it using console.connect or console.connect_after 26# to get your callback ran before or after the code is executed. 27# 28# To modify output appearance, set attributes of console.stdout_tag and 29# console.stderr_tag. 30# 31# Console may subclass a type other than gtk.TextView, to allow syntax highlighting and stuff, 32# e.g.: 33# console_type = pyconsole.ConsoleType(moo.TextView) 34# console = console_type(use_rlcompleter=False, start_script="import moo\nimport gtk\n") 35# 36# This widget is not a replacement for real terminal with python running 37# inside: GtkTextView is not a terminal. 38# The use case is: you have a python program, you create this widget, 39# and inspect your program interiors. 40 41import gtk 42import gtk.gdk as gdk 43import gobject 44import pango 45import gtk.keysyms as _keys 46import code 47import sys 48import keyword 49import re 50 51# commonprefix() from posixpath 52def _commonprefix(m): 53 "Given a list of pathnames, returns the longest common leading component" 54 if not m: return '' 55 prefix = m[0] 56 for item in m: 57 for i in range(len(prefix)): 58 if prefix[:i+1] != item[:i+1]: 59 prefix = prefix[:i] 60 if i == 0: 61 return '' 62 break 63 return prefix 64 65class _ReadLine(object): 66 67 class Output(object): 68 def __init__(self, console, tag_name): 69 object.__init__(self) 70 self.buffer = console.get_buffer() 71 self.tag_name = tag_name 72 def write(self, text): 73 pos = self.buffer.get_iter_at_mark(self.buffer.get_insert()) 74 self.buffer.insert_with_tags_by_name(pos, text, self.tag_name) 75 76 class History(object): 77 def __init__(self): 78 object.__init__(self) 79 self.items = [''] 80 self.ptr = 0 81 self.edited = {} 82 83 def commit(self, text): 84 if text and self.items[-1] != text: 85 self.items.append(text) 86 self.ptr = 0 87 self.edited = {} 88 89 def get(self, dir, text): 90 if len(self.items) == 1: 91 return None 92 93 if text != self.items[self.ptr]: 94 self.edited[self.ptr] = text 95 elif self.edited.has_key(self.ptr): 96 del self.edited[self.ptr] 97 98 self.ptr = self.ptr + dir 99 if self.ptr >= len(self.items): 100 self.ptr = 0 101 elif self.ptr < 0: 102 self.ptr = len(self.items) - 1 103 104 try: 105 return self.edited[self.ptr] 106 except KeyError: 107 return self.items[self.ptr] 108 109 def __init__(self): 110 object.__init__(self) 111 112 self.set_wrap_mode(gtk.WRAP_CHAR) 113 self.modify_font(pango.FontDescription("Monospace")) 114 115 self.buffer = self.get_buffer() 116 self.buffer.connect("insert-text", self.on_buf_insert) 117 self.buffer.connect("delete-range", self.on_buf_delete) 118 self.buffer.connect("mark-set", self.on_buf_mark_set) 119 self.do_insert = False 120 self.do_delete = False 121 122 self.stdout_tag = self.buffer.create_tag("stdout", foreground="#006000") 123 self.stderr_tag = self.buffer.create_tag("stderr", foreground="#B00000") 124 self._stdout = _ReadLine.Output(self, "stdout") 125 self._stderr = _ReadLine.Output(self, "stderr") 126 127 self.cursor = self.buffer.create_mark("cursor", 128 self.buffer.get_start_iter(), 129 False) 130 insert = self.buffer.get_insert() 131 self.cursor.set_visible(True) 132 insert.set_visible(False) 133 134 self.ps = '' 135 self.in_raw_input = False 136 self.run_on_raw_input = None 137 self.tab_pressed = 0 138 self.history = _ReadLine.History() 139 self.nonword_re = re.compile("[^\w\._]") 140 141 def freeze_undo(self): 142 try: self.begin_not_undoable_action() 143 except: pass 144 145 def thaw_undo(self): 146 try: self.end_not_undoable_action() 147 except: pass 148 149 def raw_input(self, ps=None): 150 if ps: 151 self.ps = ps 152 else: 153 self.ps = '' 154 155 iter = self.buffer.get_iter_at_mark(self.buffer.get_insert()) 156 157 if ps: 158 self.freeze_undo() 159 self.buffer.insert(iter, self.ps) 160 self.thaw_undo() 161 162 self.__move_cursor_to(iter) 163 self.scroll_to_mark(self.cursor, 0.2) 164 165 self.in_raw_input = True 166 167 if self.run_on_raw_input: 168 run_now = self.run_on_raw_input 169 self.run_on_raw_input = None 170 self.buffer.insert_at_cursor(run_now + '\n') 171 172 def on_buf_mark_set(self, buffer, iter, mark): 173 if mark is not buffer.get_insert(): 174 return 175 start = self.__get_start() 176 end = self.__get_end() 177 if iter.compare(self.__get_start()) >= 0 and \ 178 iter.compare(self.__get_end()) <= 0: 179 buffer.move_mark_by_name("cursor", iter) 180 self.scroll_to_mark(self.cursor, 0.2) 181 182 def __insert(self, iter, text): 183 self.do_insert = True 184 self.buffer.insert(iter, text) 185 self.do_insert = False 186 187 def on_buf_insert(self, buf, iter, text, len): 188 if not self.in_raw_input or self.do_insert or not len: 189 return 190 buf.stop_emission("insert-text") 191 lines = text.splitlines() 192 need_eol = False 193 for l in lines: 194 if need_eol: 195 self._commit() 196 iter = self.__get_cursor() 197 else: 198 cursor = self.__get_cursor() 199 if iter.compare(self.__get_start()) < 0: 200 iter = cursor 201 elif iter.compare(self.__get_end()) > 0: 202 iter = cursor 203 else: 204 self.__move_cursor_to(iter) 205 need_eol = True 206 self.__insert(iter, l) 207 self.__move_cursor(0) 208 209 def __delete(self, start, end): 210 self.do_delete = True 211 self.buffer.delete(start, end) 212 self.do_delete = False 213 214 def on_buf_delete(self, buf, start, end): 215 if not self.in_raw_input or self.do_delete: 216 return 217 218 buf.stop_emission("delete-range") 219 220 start.order(end) 221 line_start = self.__get_start() 222 line_end = self.__get_end() 223 224 if start.compare(line_end) > 0: 225 return 226 if end.compare(line_start) < 0: 227 return 228 229 self.__move_cursor(0) 230 231 if start.compare(line_start) < 0: 232 start = line_start 233 if end.compare(line_end) > 0: 234 end = line_end 235 self.__delete(start, end) 236 237 def do_key_press_event(self, event, parent_type): 238 if not self.in_raw_input: 239 return parent_type.do_key_press_event(self, event) 240 241 tab_pressed = self.tab_pressed 242 self.tab_pressed = 0 243 handled = True 244 245 state = event.state & (gdk.SHIFT_MASK | 246 gdk.CONTROL_MASK | 247 gdk.MOD1_MASK) 248 keyval = event.keyval 249 250 if not state: 251 if keyval == _keys.Return: 252 self._commit() 253 elif keyval == _keys.Up: 254 self.__history(-1) 255 elif keyval == _keys.Down: 256 self.__history(1) 257 elif keyval == _keys.Left: 258 self.__move_cursor(-1) 259 elif keyval == _keys.Right: 260 self.__move_cursor(1) 261 elif keyval == _keys.Home: 262 self.__move_cursor(-10000) 263 elif keyval == _keys.End: 264 self.__move_cursor(10000) 265 elif keyval == _keys.Tab: 266 cursor = self.__get_cursor() 267 if cursor.starts_line(): 268 handled = False 269 else: 270 cursor.backward_char() 271 if cursor.get_char().isspace(): 272 handled = False 273 else: 274 self.tab_pressed = tab_pressed + 1 275 self.__complete() 276 else: 277 handled = False 278 elif state == gdk.CONTROL_MASK: 279 if keyval == _keys.u: 280 start = self.__get_start() 281 end = self.__get_cursor() 282 self.__delete(start, end) 283 else: 284 handled = False 285 else: 286 handled = False 287 288 if not handled: 289 return parent_type.do_key_press_event(self, event) 290 else: 291 return True 292 293 def __history(self, dir): 294 text = self._get_line() 295 new_text = self.history.get(dir, text) 296 if not new_text is None: 297 self.__replace_line(new_text) 298 self.__move_cursor(0) 299 self.scroll_to_mark(self.cursor, 0.2) 300 301 def __get_cursor(self): 302 return self.buffer.get_iter_at_mark(self.cursor) 303 def __get_start(self): 304 iter = self.__get_cursor() 305 iter.set_line_offset(len(self.ps)) 306 return iter 307 def __get_end(self): 308 iter = self.__get_cursor() 309 if not iter.ends_line(): 310 iter.forward_to_line_end() 311 return iter 312 313 def __get_text(self, start, end): 314 return self.buffer.get_text(start, end, False) 315 316 def __move_cursor_to(self, iter): 317 self.buffer.place_cursor(iter) 318 self.buffer.move_mark_by_name("cursor", iter) 319 320 def __move_cursor(self, howmany): 321 iter = self.__get_cursor() 322 end = self.__get_cursor() 323 if not end.ends_line(): 324 end.forward_to_line_end() 325 line_len = end.get_line_offset() 326 move_to = iter.get_line_offset() + howmany 327 move_to = min(max(move_to, len(self.ps)), line_len) 328 iter.set_line_offset(move_to) 329 self.__move_cursor_to(iter) 330 331 def __delete_at_cursor(self, howmany): 332 iter = self.__get_cursor() 333 end = self.__get_cursor() 334 if not end.ends_line(): 335 end.forward_to_line_end() 336 line_len = end.get_line_offset() 337 erase_to = iter.get_line_offset() + howmany 338 if erase_to > line_len: 339 erase_to = line_len 340 elif erase_to < len(self.ps): 341 erase_to = len(self.ps) 342 end.set_line_offset(erase_to) 343 self.__delete(iter, end) 344 345 def __get_width(self): 346 if not (self.flags() & gtk.REALIZED): 347 return 80 348 layout = pango.Layout(self.get_pango_context()) 349 letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 350 layout.set_text(letters) 351 pix_width = layout.get_pixel_size()[0] 352 return self.allocation.width * len(letters) / pix_width 353 354 def __print_completions(self, completions): 355 line_start = self.__get_text(self.__get_start(), self.__get_cursor()) 356 line_end = self.__get_text(self.__get_cursor(), self.__get_end()) 357 iter = self.buffer.get_end_iter() 358 self.__move_cursor_to(iter) 359 self.__insert(iter, "\n") 360 361 width = max(self.__get_width(), 4) 362 max_width = max([len(s) for s in completions]) 363 n_columns = max(int(width / (max_width + 1)), 1) 364 col_width = int(width / n_columns) 365 total = len(completions) 366 col_length = total / n_columns 367 if total % n_columns: 368 col_length = col_length + 1 369 col_length = max(col_length, 1) 370 371 if col_length == 1: 372 n_columns = total 373 col_width = width / total 374 375 for i in range(col_length): 376 for j in range(n_columns): 377 ind = i + j*col_length 378 if ind < total: 379 if j == n_columns - 1: 380 n_spaces = 0 381 else: 382 n_spaces = col_width - len(completions[ind]) 383 self.__insert(iter, completions[ind] + " " * n_spaces) 384 self.__insert(iter, "\n") 385 386 self.__insert(iter, "%s%s%s" % (self.ps, line_start, line_end)) 387 iter.set_line_offset(len(self.ps) + len(line_start)) 388 self.__move_cursor_to(iter) 389 self.scroll_to_mark(self.cursor, 0.2) 390 391 def __complete(self): 392 text = self.__get_text(self.__get_start(), self.__get_cursor()) 393 start = '' 394 word = text 395 nonwords = self.nonword_re.findall(text) 396 if nonwords: 397 last = text.rfind(nonwords[-1]) + len(nonwords[-1]) 398 start = text[:last] 399 word = text[last:] 400 401 completions = self.complete(word) 402 403 if completions: 404 prefix = _commonprefix(completions) 405 if prefix != word: 406 start_iter = self.__get_start() 407 start_iter.forward_chars(len(start)) 408 end_iter = start_iter.copy() 409 end_iter.forward_chars(len(word)) 410 self.__delete(start_iter, end_iter) 411 self.__insert(end_iter, prefix) 412 elif self.tab_pressed > 1: 413 self.freeze_undo() 414 self.__print_completions(completions) 415 self.thaw_undo() 416 self.tab_pressed = 0 417 418 def complete(self, text): 419 return None 420 421 def _get_line(self): 422 start = self.__get_start() 423 end = self.__get_end() 424 return self.buffer.get_text(start, end, False) 425 426 def __replace_line(self, new_text): 427 start = self.__get_start() 428 end = self.__get_end() 429 self.__delete(start, end) 430 self.__insert(end, new_text) 431 432 def _commit(self): 433 end = self.__get_cursor() 434 if not end.ends_line(): 435 end.forward_to_line_end() 436 text = self._get_line() 437 self.__move_cursor_to(end) 438 self.freeze_undo() 439 self.__insert(end, "\n") 440 self.in_raw_input = False 441 self.history.commit(text) 442 self.do_raw_input(text) 443 self.thaw_undo() 444 445 def do_raw_input(self, text): 446 pass 447 448 449class _Console(_ReadLine, code.InteractiveInterpreter): 450 def __init__(self, locals=None, banner=None, 451 completer=None, use_rlcompleter=True, 452 start_script=None): 453 _ReadLine.__init__(self) 454 455 456 code.InteractiveInterpreter.__init__(self, locals) 457 self.locals["__console__"] = self 458 459 self.start_script = start_script 460 self.completer = completer 461 self.banner = banner 462 463 if not self.completer and use_rlcompleter: 464 try: 465 import rlcompleter 466 self.completer = rlcompleter.Completer() 467 except ImportError: 468 pass 469 470 self.ps1 = ">>> " 471 self.ps2 = "... " 472 self.__start() 473 self.run_on_raw_input = start_script 474 self.raw_input(self.ps1) 475 476 def __start(self): 477 self.cmd_buffer = "" 478 479 self.freeze_undo() 480 self.thaw_undo() 481 self.buffer.set_text("") 482 483 if self.banner: 484 iter = self.buffer.get_start_iter() 485 self.buffer.insert_with_tags_by_name(iter, self.banner, "stdout") 486 if not iter.starts_line(): 487 self.buffer.insert(iter, "\n") 488 489 def clear(self, start_script=None): 490 if start_script is None: 491 start_script = self.start_script 492 else: 493 self.start_script = start_script 494 495 self.__start() 496 self.run_on_raw_input = start_script 497 498 def do_raw_input(self, text): 499 if self.cmd_buffer: 500 cmd = self.cmd_buffer + "\n" + text 501 else: 502 cmd = text 503 504 saved_stdout, saved_stderr = sys.stdout, sys.stderr 505 sys.stdout, sys.stderr = self._stdout, self._stderr 506 507 if self.runsource(cmd): 508 self.cmd_buffer = cmd 509 ps = self.ps2 510 else: 511 self.cmd_buffer = '' 512 ps = self.ps1 513 514 sys.stdout, sys.stderr = saved_stdout, saved_stderr 515 self.raw_input(ps) 516 517 def do_command(self, code): 518 try: 519 eval(code, self.locals) 520# In GeanyPy console, we don't want to exit the process on SystemExit 521# except SystemExit: 522# raise 523 except: 524 self.showtraceback() 525 526 def runcode(self, code): 527 if gtk.pygtk_version[1] < 8: 528 self.do_command(code) 529 else: 530 self.emit("command", code) 531 532 def exec_command(self, command): 533 if self._get_line(): 534 self._commit() 535 self.buffer.insert_at_cursor(command) 536 self._commit() 537 538 def complete_attr(self, start, end): 539 try: 540 obj = eval(start, self.locals) 541 strings = dir(obj) 542 543 if end: 544 completions = {} 545 for s in strings: 546 if s.startswith(end): 547 completions[s] = None 548 completions = completions.keys() 549 else: 550 completions = strings 551 552 completions.sort() 553 return [start + "." + s for s in completions] 554 except: 555 return None 556 557 def complete(self, text): 558 if self.completer: 559 completions = [] 560 i = 0 561 try: 562 while 1: 563 s = self.completer.complete(text, i) 564 if s: 565 completions.append(s) 566 i = i + 1 567 else: 568 completions.sort() 569 return completions 570 except NameError: 571 return None 572 573 dot = text.rfind(".") 574 if dot >= 0: 575 return self.complete_attr(text[:dot], text[dot+1:]) 576 577 completions = {} 578 strings = keyword.kwlist 579 580 if self.locals: 581 strings.extend(self.locals.keys()) 582 583 try: strings.extend(eval("globals()", self.locals).keys()) 584 except: pass 585 586 try: 587 exec "import __builtin__" in self.locals 588 strings.extend(eval("dir(__builtin__)", self.locals)) 589 except: 590 pass 591 592 for s in strings: 593 if s.startswith(text): 594 completions[s] = None 595 completions = completions.keys() 596 completions.sort() 597 return completions 598 599 600def ReadLineType(t=gtk.TextView): 601 class readline(t, _ReadLine): 602 def __init__(self, *args, **kwargs): 603 t.__init__(self) 604 _ReadLine.__init__(self, *args, **kwargs) 605 def do_key_press_event(self, event): 606 return _ReadLine.do_key_press_event(self, event, t) 607 gobject.type_register(readline) 608 return readline 609 610def ConsoleType(t=gtk.TextView): 611 class console_type(t, _Console): 612 __gsignals__ = { 613 'command' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (object,)), 614 'key-press-event' : 'override' 615 } 616 617 def __init__(self, *args, **kwargs): 618 if gtk.pygtk_version[1] < 8: 619 gobject.GObject.__init__(self) 620 else: 621 t.__init__(self) 622 _Console.__init__(self, *args, **kwargs) 623 624 def do_command(self, code): 625 return _Console.do_command(self, code) 626 627 def do_key_press_event(self, event): 628 return _Console.do_key_press_event(self, event, t) 629 630 if gtk.pygtk_version[1] < 8: 631 gobject.type_register(console_type) 632 633 return console_type 634 635ReadLine = ReadLineType() 636Console = ConsoleType() 637 638def _create_widget(start_script): 639 640 console = Console(banner="Geany Python Console", 641 use_rlcompleter=False, 642 start_script=start_script) 643 return console 644 645def _make_window(start_script="import geany\n"): 646 window = gtk.Window() 647 window.set_title("Python Console") 648 swin = gtk.ScrolledWindow() 649 swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS) 650 window.add(swin) 651 console = _create_widget(start_script) 652 swin.add(console) 653 window.set_default_size(500, 400) 654 window.show_all() 655 656 if not gtk.main_level(): 657 window.connect("destroy", gtk.main_quit) 658 gtk.main() 659 660 return console 661 662if __name__ == '__main__': 663 import sys 664 import os 665 sys.path.insert(0, os.getcwd()) 666 _make_window(sys.argv[1:] and '\n'.join(sys.argv[1:]) + '\n' or None) 667