Package fortuneengine :: Package pyconsole :: Module pyconsole
[hide private]
[frames] | no frames]

Source Code for Module fortuneengine.pyconsole.pyconsole

  1  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
  2  # pyconsole - a simple console for pygame based applications 
  3  # 
  4  # Copyright (C) 2006  John Schanck 
  5  # 
  6  # This program is free software; you can redistribute it and/or modify 
  7  # it under the terms of the GNU General Public License as published by 
  8  # the Free Software Foundation; either version 2 of the License, or 
  9  # (at your option) any later version. 
 10  # 
 11  # This program is distributed in the hope that it will be useful, 
 12  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 13  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 14  # GNU General Public License for more details. 
 15  # 
 16  # You should have received a copy of the GNU General Public License along 
 17  # with this program; if not, write to the Free Software Foundation, Inc., 
 18  # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
 19  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
 20   
 21   
 22  import pygame, os, sys 
 23  from pygame.locals import * 
 24   
 25  import re       # Python's Regexp library. Used in pyconsole for parsing 
 26  import textwrap # Used for proper word wrapping 
 27  from string import ascii_letters 
 28  from code import InteractiveConsole     # Gives us access to the python interpreter 
 29   
 30   
 31  __version__ = "0.7" 
 32   
 33  WIDTH=0 
 34  HEIGHT=1 
 35   
 36  OUT = 0 
 37  IN = 1 
 38  ERR = 2 
 39   
 40  PYCONSOLE = 1 
 41  PYTHON = 2 
 42   
 43  path = os.path.abspath(os.path.dirname(__file__)) 
 44  font_path = os.path.join(path, "fonts") 
 45  cfg_path = os.path.join(path, "pyconsole.cfg") 
 46   
 47   
 48  re_token = re.compile(r"""[\"].*?[\"]|[\{].*?[\}]|[\(].*?[\)]|[\[].*?[\]]|\S+""") 
 49  re_is_list = re.compile(r'^[{\[(]') 
 50  re_is_number = re.compile(r""" 
 51                          (?x) 
 52                          [-]?[0][x][0-9a-fA-F]+[lLjJ]? |     #  Hexadecimal 
 53                          [-]?[0][0-7]+[lLjJ]? |              #  Octal 
 54                          [-]?[\d]+(?:[.][\d]*)?[lLjJ]?       #  Decimal (Int or float) 
 55                          """) 
 56  re_is_assign = re.compile(r'[$](?P<name>[a-zA-Z_]+\S*)\s*[=]\s*(?P<value>.+)') 
 57  re_is_comment =  re.compile(r'\s*#.*') 
 58  re_is_var = re.compile(r'^[$][a-zA-Z_]+\w*\Z') 
 59   
 60   
 61   
62 -class Writable(list):
63 line_pointer = -1
64 - def write(self, line):
65 self.append(str(line))
66 - def reset(self):
67 self.__init__()
68 - def readline(self, size=-1):
69 # Python's interactive help likes to try and call this, which causes the program to crash 70 # I see no reason to implement interactive help. 71 raise NotImplementedError
72
73 -class ParseError(Exception):
74 - def __init__(self, token):
75 self.token = token
76 - def at_token(self):
77 return self.token
78
79 -def balanced(t):
80 stack = [] 81 pairs = {"\'":"\'", '\"':'\"', "{":"}", "[":"]", "(":")"} 82 for char in t: 83 if stack and char == pairs[stack[-1]]: 84 stack.pop() 85 elif char in pairs: 86 stack.append(char) 87 return not bool(stack)
88
89 -class Console:
90 - def __init__(self, screen, rect, functions={}, key_calls={}, vars={}, syntax={}):
91 if not pygame.display.get_init(): 92 raise pygame.error, "Display not initialized. Initialize the display before creating a Console" 93 94 if not pygame.font.get_init(): 95 pygame.font.init() 96 97 self.parent_screen = screen 98 self.rect = pygame.Rect(rect) 99 self.size = self.rect.size 100 101 self.user_vars = vars 102 self.user_syntax = syntax 103 self.user_namespace = {} 104 105 self.variables = {\ 106 "bg_alpha":int,\ 107 "bg_color": list,\ 108 "txt_color_i": list,\ 109 "txt_color_o": list,\ 110 "ps1": str,\ 111 "ps2": str,\ 112 "ps3": str,\ 113 "active": bool,\ 114 "repeat_rate": list,\ 115 "preserve_events":bool,\ 116 "python_mode":bool,\ 117 "motd":list 118 } 119 120 self.load_cfg() 121 122 self.set_interpreter() 123 124 #pygame.key.set_repeat(*self.repeat_rate) 125 126 self.bg_layer = pygame.Surface(self.size) 127 self.bg_layer.set_alpha(self.bg_alpha) 128 129 self.txt_layer = pygame.Surface(self.size) 130 self.txt_layer.set_colorkey(self.bg_color) 131 132 try: 133 self.font = pygame.font.Font(os.path.join(font_path,"default.ttf"), 14) 134 except IOError: 135 self.font = pygame.font.SysFont("monospace", 14) 136 137 self.font_height = self.font.get_linesize() 138 self.max_lines = (self.size[HEIGHT] / self.font_height) - 1 139 140 self.max_chars = (self.size[WIDTH]/(self.font.size(ascii_letters)[WIDTH]/len(ascii_letters))) - 1 141 self.txt_wrapper = textwrap.TextWrapper() 142 143 self.c_out = self.motd 144 self.c_hist = [""] 145 self.c_hist_pos = 0 146 self.c_in = "" 147 self.c_pos = 0 148 self.c_draw_pos = 0 149 self.c_scroll = 0 150 151 152 self.changed = True 153 154 self.func_calls = {} 155 self.key_calls = {} 156 157 self.add_func_calls({"echo":self.output, "clear": self.clear, "help":self.help}) 158 self.add_func_calls(functions) 159 160 self.add_key_calls({"l":self.clear, "c":self.clear_input, "w":self.set_active}) 161 self.add_key_calls(key_calls)
162 163 164 ################## 165 #-Initialization-#
166 - def load_cfg(self):
167 '''\ 168 Loads the config file path/pygame-console.cfg\ 169 All variables are initialized to their defaults,\ 170 then new values will be loaded from the config file if it exists. 171 ''' 172 self.init_default_cfg() 173 if os.path.exists(cfg_path): 174 for line in file(cfg_path): 175 tokens = self.tokenize(line) 176 if re_is_comment.match(line): 177 continue 178 elif len(tokens) != 2: 179 continue 180 self.safe_set_attr(tokens[0],tokens[1])
181
182 - def init_default_cfg(self):
183 self.bg_alpha = 255 184 self.bg_color = [0x0,0x0,0x0] 185 self.txt_color_i = [0xFF,0xFF,0xFF] 186 self.txt_color_o = [0xCC,0xCC,0xCC] 187 self.ps1 = "] " 188 self.ps2 = ">>> " 189 self.ps3 = "... " 190 self.active = False 191 self.repeat_rate = [500,30] 192 self.python_mode = False 193 self.preserve_events = False 194 self.motd = ["[PyConsole 0.5]"]
195
196 - def safe_set_attr(self, name, value):
197 '''\ 198 Safely set the console variables 199 ''' 200 if name in self.variables: 201 if isinstance(value, self.variables[name]) or not self.variables[name]: 202 self.__dict__[name] = value
203
204 - def add_func_calls(self, functions):
205 '''\ 206 Add functions to the func_calls dictionary. 207 Arguments: 208 functions -- dictionary of functions to add. 209 ''' 210 if isinstance(functions,dict): 211 self.func_calls.update(functions) 212 self.user_namespace.update(self.func_calls)
213
214 - def add_key_calls(self, functions):
215 '''\ 216 Add functions to the key_calls dictionary. 217 Arguments: 218 functions -- dictionary of key_calls to add. 219 ''' 220 if isinstance(functions,dict): 221 self.key_calls.update(functions)
222
223 - def output(self, text):
224 '''\ 225 Prepare text to be displayed 226 Arguments: 227 text -- Text to be displayed 228 ''' 229 if not str(text): 230 return; 231 232 try: 233 self.changed = True 234 if not isinstance(text,str): 235 text = str(text) 236 text = text.expandtabs() 237 text = text.splitlines() 238 self.txt_wrapper.width = self.max_chars 239 for line in text: 240 for w in self.txt_wrapper.wrap(line): 241 self.c_out.append(w) 242 except: 243 pass
244
245 - def set_active(self,b=None):
246 '''\ 247 Activate or Deactivate the console 248 Arguments: 249 b -- Optional boolean argument, True=Activate False=Deactivate 250 ''' 251 if not b: 252 self.active = not self.active 253 else: 254 self.active = b
255 256
257 - def format_input_line(self):
258 '''\ 259 Format input line to be displayed 260 ''' 261 # The \v here is sort of a hack, it's just a character that isn't recognized by the font engine 262 text = self.c_in[:self.c_pos] + "\v" + self.c_in[self.c_pos+1:] 263 n_max = self.max_chars - len(self.c_ps) 264 vis_range = self.c_draw_pos, self.c_draw_pos + n_max 265 return self.c_ps + text[vis_range[0]:vis_range[1]]
266
267 - def draw(self):
268 '''\ 269 Draw the console to the parent screen 270 ''' 271 if not self.active: 272 return; 273 274 if self.changed: 275 self.changed = False 276 # Draw Output 277 self.txt_layer.fill(self.bg_color) 278 lines = self.c_out[-(self.max_lines+self.c_scroll):len(self.c_out)-self.c_scroll] 279 y_pos = self.size[HEIGHT]-(self.font_height*(len(lines)+1)) 280 281 for line in lines: 282 tmp_surf = self.font.render(line, True, self.txt_color_o) 283 self.txt_layer.blit(tmp_surf, (1, y_pos, 0, 0)) 284 y_pos += self.font_height 285 # Draw Input 286 tmp_surf = self.font.render(self.format_input_line(), True, self.txt_color_i) 287 self.txt_layer.blit(tmp_surf, (1,self.size[HEIGHT]-self.font_height,0,0)) 288 # Clear background and blit text to it 289 self.bg_layer.fill(self.bg_color) 290 self.bg_layer.blit(self.txt_layer,(0,0,0,0)) 291 292 # Draw console to parent screen 293 # self.parent_screen.fill(self.txt_color_i, (self.rect.x-1, self.rect.y-1, self.size[WIDTH]+2, self.size[HEIGHT]+2)) 294 pygame.draw.rect(self.parent_screen, self.txt_color_i, (self.rect.x-1, self.rect.y-1, self.size[WIDTH]+2, self.size[HEIGHT]+2), 1) 295 self.parent_screen.blit(self.bg_layer,self.rect)
296 297 ####################################################################### 298 # Functions to communicate with the console and the python interpreter#
299 - def set_interpreter(self):
300 if self.python_mode: 301 self.output("Entering Python mode") 302 self.python_mode = True 303 self.python_interpreter = InteractiveConsole() 304 self.tmp_fds = [] 305 self.py_fds = [Writable() for i in range(3)] 306 self.c_ps = self.ps2 307 else: 308 self.output("Entering Pyconsole mode") 309 self.python_mode = False 310 self.c_ps = self.ps1
311
312 - def catch_output(self):
313 if not self.tmp_fds: 314 self.tmp_fds = [sys.stdout, sys.stdin, sys.stderr] 315 sys.stdout, sys.stdin, sys.stderr = self.py_fds
316
317 - def release_output(self):
318 if self.tmp_fds: 319 sys.stdout, sys.stdin, sys.stderr = self.tmp_fds 320 self.tmp_fds = [] 321 [fd.reset() for fd in self.py_fds]
322
323 - def submit_input(self, text):
324 '''\ 325 Submit input 326 1) Move input to output 327 2) Evaluate input 328 3) Clear input line 329 ''' 330 331 self.clear_input() 332 self.output(self.c_ps + text) 333 self.c_scroll = 0 334 if self.python_mode: 335 self.send_python(text) 336 else: 337 self.send_pyconsole(text)
338
339 - def send_python(self, text):
340 '''\ 341 Sends input the the python interpreter in effect giving the user the ability to do anything python can. 342 ''' 343 self.c_ps = self.ps2 344 self.catch_output() 345 if text: 346 self.add_to_history(text) 347 r = self.python_interpreter.push(text) 348 if r: 349 self.c_ps = self.ps3 350 else: 351 code = "".join(self.py_fds[OUT]) 352 self.python_interpreter.push("\n") 353 self.python_interpreter.runsource(code) 354 for i in self.py_fds[OUT]+self.py_fds[ERR]: 355 self.output(i) 356 self.release_output()
357
358 - def send_pyconsole(self, text):
359 '''\ 360 Sends input to pyconsole to be interpreted 361 ''' 362 if not text: # Output a blank row if nothing is entered 363 self.output("") 364 return; 365 366 self.add_to_history(text) 367 368 #Determine if the statement is an assignment 369 assign = re_is_assign.match(text) 370 try: 371 #If it is tokenize only the "value" part of $name = value 372 if assign: 373 tokens = self.tokenize(assign.group('value')) 374 else: 375 tokens = self.tokenize(text) 376 except ParseError, e: 377 self.output(r'At Token: "%s"' % e.at_token()) 378 return; 379 380 if tokens == None: 381 return 382 383 #Evaluate 384 try: 385 out = None 386 # A variable alone on a line 387 if (len(tokens) is 1) and re_is_var.match(text) and not assign: 388 out = tokens[0] 389 # Statement in the form $name = value 390 elif (len(tokens) is 1) and assign: 391 self.setvar(assign.group('name'), tokens[0]) 392 else: 393 # Function 394 out = self.func_calls[tokens[0]](*tokens[1:]) 395 # Assignment from function's return value 396 if assign: 397 self.setvar(assign.group('name'), out) 398 399 if not out == None: 400 self.output(out) 401 except (KeyError,TypeError): 402 self.output("Unknown Command: " + str(tokens[0])) 403 self.output(r'Type "help" for a list of commands.')
404 405 406 407 #################################################### 408 #-Functions for sharing variables with the console-#
409 - def setvar(self, name, value):
410 '''\ 411 Sets the value of a variable 412 ''' 413 if self.user_vars.has_key(name) or not self.__dict__.has_key(name): 414 self.user_vars.update({name:value}) 415 self.user_namespace.update(self.user_vars) 416 elif self.__dict__.has_key(name): 417 self.__dict__.update({name:value})
418
419 - def getvar(self, name):
420 '''\ 421 Gets the value of a variable, this is useful for people that want to access console variables from within their game 422 ''' 423 if self.user_vars.has_key(name) or not self.__dict__.has_key(name): 424 return self.user_vars[name] 425 elif self.__dict__.has_key(name): 426 return self.__dict__[name]
427
428 - def setvars(self, vars):
429 try: 430 self.user_vars.update(vars) 431 self.user_namespace.update(self.user_vars) 432 except TypeError: 433 self.output("setvars requires a dictionary")
434
435 - def getvars(self, opt_dict=None):
436 if opt_dict: 437 opt_dict.update(self.user_vars) 438 else: 439 return self.user_vars
440 441
442 - def add_to_history(self, text):
443 '''\ 444 Add specified text to the history 445 ''' 446 self.c_hist.insert(-1,text) 447 self.c_hist_pos = len(self.c_hist)-1
448
449 - def clear_input(self):
450 '''\ 451 Clear input line and reset cursor position 452 ''' 453 self.c_in = "" 454 self.c_pos = 0 455 self.c_draw_pos = 0
456
457 - def set_pos(self, newpos):
458 '''\ 459 Moves cursor safely 460 ''' 461 self.c_pos = newpos 462 if (self.c_pos - self.c_draw_pos) >= (self.max_chars - len(self.c_ps)): 463 self.c_draw_pos = max(0, self.c_pos - (self.max_chars - len(self.c_ps))) 464 elif self.c_draw_pos > self.c_pos: 465 self.c_draw_pos = self.c_pos - (self.max_chars/2) 466 if self.c_draw_pos < 0: 467 self.c_draw_pos = 0 468 self.c_pos = 0
469
470 - def str_insert(self, text, strn):
471 '''\ 472 Insert characters at the current cursor position 473 ''' 474 foo = text[:self.c_pos] + strn + text[self.c_pos:] 475 self.set_pos(self.c_pos + len(strn)) 476 return foo
477
478 - def process_input(self, event):
479 '''\ 480 Loop through pygame events and evaluate them 481 ''' 482 if not self.active: 483 return False; 484 485 if event.type == KEYDOWN: 486 self.changed = True 487 ## Special Character Manipulation 488 if event.key == K_TAB: 489 self.c_in = self.str_insert(self.c_in, " ") 490 elif event.key == K_BACKSPACE: 491 if self.c_pos > 0: 492 self.c_in = self.c_in[:self.c_pos-1] + self.c_in[self.c_pos:] 493 self.set_pos(self.c_pos-1) 494 elif event.key == K_DELETE: 495 if self.c_pos < len(self.c_in): 496 self.c_in = self.c_in[:self.c_pos] + self.c_in[self.c_pos+1:] 497 elif event.key == K_RETURN or event.key == 271: 498 self.submit_input(self.c_in) 499 ## Changing Cursor Position 500 elif event.key == K_LEFT: 501 if self.c_pos > 0: 502 self.set_pos(self.c_pos-1) 503 elif event.key == K_RIGHT: 504 if self.c_pos < len(self.c_in): 505 self.set_pos(self.c_pos+1) 506 elif event.key == K_HOME: 507 self.set_pos(0) 508 elif event.key == K_END: 509 self.set_pos(len(self.c_in)) 510 ## History Navigation 511 elif event.key == K_UP: 512 if len(self.c_out): 513 if self.c_hist_pos > 0: 514 self.c_hist_pos -= 1 515 self.c_in = self.c_hist[self.c_hist_pos] 516 self.set_pos(len(self.c_in)) 517 elif event.key == K_DOWN: 518 if len(self.c_out): 519 if self.c_hist_pos < len(self.c_hist)-1: 520 self.c_hist_pos += 1 521 self.c_in = self.c_hist[self.c_hist_pos] 522 self.set_pos(len(self.c_in)) 523 ## Scrolling 524 elif event.key == K_PAGEUP: 525 if self.c_scroll < len(self.c_out)-1: 526 self.c_scroll += 1 527 elif event.key == K_PAGEDOWN: 528 if self.c_scroll > 0: 529 self.c_scroll -= 1 530 ## Normal character printing 531 elif event.key >= 32: 532 mods = pygame.key.get_mods() 533 if mods & KMOD_CTRL: 534 if event.key in range(256) and chr(event.key) in self.key_calls: 535 self.key_calls[chr(event.key)]() 536 else: 537 char = str(event.unicode) 538 self.c_in = self.str_insert(self.c_in, char) 539 return True
540
541 - def convert_token(self, tok):
542 '''\ 543 Convert a token to its proper type 544 ''' 545 tok = tok.strip("$") 546 try: 547 tmp = eval(tok, self.__dict__, self.user_namespace) 548 except SyntaxError, strerror: 549 self.output("SyntaxError: " + str(strerror)) 550 raise ParseError, tok 551 except TypeError, strerror: 552 self.output("TypeError: " + str(strerror)) 553 raise ParseError, tok 554 except NameError, strerror: 555 self.output("NameError: " + str(strerror)) 556 except: 557 self.output("Error:") 558 raise ParseError, tok 559 else: 560 return tmp
561
562 - def tokenize(self, s):
563 '''\ 564 Tokenize input line, convert tokens to proper types 565 ''' 566 if re_is_comment.match(s): 567 return [s] 568 569 for re in self.user_syntax: 570 group = re.match(s) 571 if group: 572 self.user_syntax[re](self, group) 573 return 574 575 tokens = re_token.findall(s) 576 tokens = [i.strip("\"") for i in tokens] 577 cmd = [] 578 i = 0 579 while i < len(tokens): 580 t_count = 0 581 val = tokens[i] 582 583 if re_is_number.match(val): 584 cmd.append(self.convert_token(val)) 585 elif re_is_var.match(val): 586 cmd.append(self.convert_token(val)) 587 elif val == "True": 588 cmd.append(True) 589 elif val == "False": 590 cmd.append(False) 591 elif re_is_list.match(val): 592 while not balanced(val) and (i + t_count) < len(tokens)-1: 593 t_count += 1 594 val += tokens[i+t_count] 595 else: 596 if (i + t_count) < len(tokens): 597 cmd.append(self.convert_token(val)) 598 else: 599 raise ParseError, val 600 else: 601 cmd.append(val) 602 i += t_count + 1 603 return cmd
604 605 606 ########################## 607 #-Some Builtin functions-#
608 - def clear(self):
609 '''\ 610 Clear the Screen 611 ''' 612 self.c_out = ["[Screen Cleared]"] 613 self.c_scroll = 0
614
615 - def help(self, *args):
616 '''\ 617 Output information about functions 618 Arguments: 619 args -- arbitrary argument list of function names 620 |- No Args - A list of available functions will be displayed 621 |- One or more Args - Docstring of each function will be displayed 622 ''' 623 if args: 624 items = [(i,self.func_calls[i]) for i in args if i in self.func_calls] 625 for i,v in items: 626 out = i + ": Takes %d arguments. " % (v.func_code.co_argcount - (v.func_code.co_varnames[0] is "self")) 627 doc = v.func_doc 628 if doc: 629 out += textwrap.dedent(doc) 630 tmp_indent = self.txt_wrapper.subsequent_indent 631 self.txt_wrapper.subsequent_indent = " "*(len(i)+2) 632 self.output(out) 633 self.txt_wrapper.subsequent_indent = tmp_indent 634 else: 635 out = "Available commands: " + str(self.func_calls.keys()).strip("[]") 636 self.output(out) 637 self.output(r'Type "help command-name" for more information on that command')
638