From 8dc66e3fa2536028cfb6fc625a272ec7a0db407b Mon Sep 17 00:00:00 2001 From: jstenar <> Date: Sat, 21 Jan 2006 17:54:05 +0000 Subject: [PATCH] Adding Gary Bishops readline v1.12 as is from readline-1.12.zip under the name pyreadline. --- PKG-INFO | 10 + pyreadline/Console.py | 712 ++++++++++++++++++++++++ pyreadline/PyReadline.py | 1122 ++++++++++++++++++++++++++++++++++++++ pyreadline/__init__.py | 19 + pyreadline/keysyms.py | 173 ++++++ setup.py | 10 + 6 files changed, 2046 insertions(+) create mode 100644 PKG-INFO create mode 100644 pyreadline/Console.py create mode 100644 pyreadline/PyReadline.py create mode 100644 pyreadline/__init__.py create mode 100644 pyreadline/keysyms.py create mode 100644 setup.py diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..a0d2109 --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 1.0 +Name: readline +Version: 1.12 +Summary: Python implementation of GNU readline +Home-page: UNKNOWN +Author: Gary Bishop +Author-email: gb@cs.unc.edu +License: UNKNOWN +Description: UNKNOWN +Platform: UNKNOWN diff --git a/pyreadline/Console.py b/pyreadline/Console.py new file mode 100644 index 0000000..b926b60 --- /dev/null +++ b/pyreadline/Console.py @@ -0,0 +1,712 @@ +'''Cursor control and color for the Windows console. + +This was modeled after the C extension of the same name by Fredrik Lundh. +''' + +# primitive debug printing that won't interfere with the screen +if 0: + fp = open('debug.txt', 'w') + def log(s): + print >>fp, s + fp.flush() +else: + def log(s): + pass + +import sys +import traceback +import re + +try: + # I developed this with ctypes 0.6 + from ctypes import * + from _ctypes import call_function +except ImportError: + print 'you need the ctypes module to run this code' + print 'http://starship.python.net/crew/theller/ctypes/' + raise + +# my code +from keysyms import make_keysym, make_keyinfo + +# some constants we need +STD_INPUT_HANDLE = -10 +STD_OUTPUT_HANDLE = -11 +ENABLE_WINDOW_INPUT = 0x0008 +ENABLE_MOUSE_INPUT = 0x0010 +ENABLE_PROCESSED_INPUT = 0x0001 +WHITE = 0x7 +BLACK = 0 +MENU_EVENT = 0x0008 +KEY_EVENT = 0x0001 +MOUSE_MOVED = 0x0001 +MOUSE_EVENT = 0x0002 +WINDOW_BUFFER_SIZE_EVENT = 0x0004 +FOCUS_EVENT = 0x0010 +MENU_EVENT = 0x0008 +VK_SHIFT = 0x10 +VK_CONTROL = 0x11 +VK_MENU = 0x12 +GENERIC_READ = int(0x80000000L) +GENERIC_WRITE = 0x40000000 + +# Windows structures we'll need later +class COORD(Structure): + _fields_ = [("X", c_short), + ("Y", c_short)] + +class SMALL_RECT(Structure): + _fields_ = [("Left", c_short), + ("Top", c_short), + ("Right", c_short), + ("Bottom", c_short)] + +class CONSOLE_SCREEN_BUFFER_INFO(Structure): + _fields_ = [("dwSize", COORD), + ("dwCursorPosition", COORD), + ("wAttributes", c_short), + ("srWindow", SMALL_RECT), + ("dwMaximumWindowSize", COORD)] + +class CHAR_UNION(Union): + _fields_ = [("UnicodeChar", c_short), + ("AsciiChar", c_char)] + +class CHAR_INFO(Structure): + _fields_ = [("Char", CHAR_UNION), + ("Attributes", c_short)] + +class KEY_EVENT_RECORD(Structure): + _fields_ = [("bKeyDown", c_byte), + ("pad2", c_byte), + ('pad1', c_short), + ("wRepeatCount", c_short), + ("wVirtualKeyCode", c_short), + ("wVirtualScanCode", c_short), + ("uChar", CHAR_UNION), + ("dwControlKeyState", c_int)] + +class MOUSE_EVENT_RECORD(Structure): + _fields_ = [("dwMousePosition", COORD), + ("dwButtonState", c_int), + ("dwControlKeyState", c_int), + ("dwEventFlags", c_int)] + +class WINDOW_BUFFER_SIZE_RECORD(Structure): + _fields_ = [("dwSize", COORD)] + +class MENU_EVENT_RECORD(Structure): + _fields_ = [("dwCommandId", c_uint)] + +class FOCUS_EVENT_RECORD(Structure): + _fields_ = [("bSetFocus", c_byte)] + +class INPUT_UNION(Union): + _fields_ = [("KeyEvent", KEY_EVENT_RECORD), + ("MouseEvent", MOUSE_EVENT_RECORD), + ("WindowBufferSizeEvent", WINDOW_BUFFER_SIZE_RECORD), + ("MenuEvent", MENU_EVENT_RECORD), + ("FocusEvent", FOCUS_EVENT_RECORD)] + +class INPUT_RECORD(Structure): + _fields_ = [("EventType", c_short), + ("Event", INPUT_UNION)] + +class CONSOLE_CURSOR_INFO(Structure): + _fields_ = [("dwSize", c_int), + ("bVisible", c_byte)] + +# I didn't want to have to individually import these so I made a list, they are +# added to the Console class later in this file. + +funcs = [ + 'AllocConsole', + 'CreateConsoleScreenBuffer', + 'FillConsoleOutputAttribute', + 'FillConsoleOutputCharacterA', + 'FreeConsole', + 'GetConsoleCursorInfo', + 'GetConsoleMode', + 'GetConsoleScreenBufferInfo', + 'GetConsoleTitleA', + 'GetProcAddress', + 'GetStdHandle', + 'PeekConsoleInputA', + 'ReadConsoleInputA', + 'ScrollConsoleScreenBufferA', + 'SetConsoleActiveScreenBuffer', + 'SetConsoleCursorInfo', + 'SetConsoleCursorPosition', + 'SetConsoleMode', + 'SetConsoleScreenBufferSize', + 'SetConsoleTextAttribute', + 'SetConsoleTitleA', + 'SetConsoleWindowInfo', + 'WriteConsoleA', + 'WriteConsoleOutputCharacterA', + ] + +# I don't want events for these keys, they are just a bother for my application +key_modifiers = { VK_SHIFT:1, + VK_CONTROL:1, + VK_MENU:1, # alt key + 0x5b:1, # windows key + } + +class Console(object): + '''Console driver for Windows. + + ''' + + def __init__(self, newbuffer=0): + '''Initialize the Console object. + + newbuffer=1 will allocate a new buffer so the old content will be restored + on exit. + ''' + #Do I need the following line? It causes a console to be created whenever + #readline is imported into a pythonw application which seems wrong. Things + #seem to work without it... + #self.AllocConsole() + + if newbuffer: + self.hout = self.CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, + 0, None, 1, None) + self.SetConsoleActiveScreenBuffer(self.hout) + else: + self.hout = self.GetStdHandle(STD_OUTPUT_HANDLE) + + self.hin = self.GetStdHandle(STD_INPUT_HANDLE) + self.inmode = c_int(0) + self.GetConsoleMode(self.hin, byref(self.inmode)) + self.SetConsoleMode(self.hin, 0xf) + info = CONSOLE_SCREEN_BUFFER_INFO() + self.GetConsoleScreenBufferInfo(self.hout, byref(info)) + self.attr = info.wAttributes # remember the initial colors + background = self.attr & 0xf0 + for escape in self.escape_to_color: + if self.escape_to_color[escape] is not None: + self.escape_to_color[escape] |= background + log('initial attr=%x' % self.attr) + self.softspace = 0 # this is for using it as a file-like object + self.serial = 0 + + self.pythondll = CDLL('python%s%s' % (sys.version[0], sys.version[2])) + self.inputHookPtr = c_int.from_address(addressof(self.pythondll.PyOS_InputHook)).value + setattr(Console, 'PyMem_Malloc', self.pythondll.PyMem_Malloc) + + def __del__(self): + '''Cleanup the console when finished.''' + # I don't think this ever gets called + self.SetConsoleTextAttribute(self.hout, self.saveattr) + self.SetConsoleMode(self.hin, self.inmode) + self.FreeConsole() + + def fixcoord(self, x, y): + '''Return a long with x and y packed inside, also handle negative x and y.''' + if x < 0 or y < 0: + info = CONSOLE_SCREEN_BUFFER_INFO() + self.GetConsoleScreenBufferInfo(self.hout, byref(info)) + if x < 0: + x = info.srWindow.Right - x + y = info.srWindow.Bottom + y + + # this is a hack! ctypes won't pass structures but COORD is just like a + # long, so this works. + return c_int(y << 16 | x) + + def pos(self, x=None, y=None): + '''Move or query the window cursor.''' + if x is None: + info = CONSOLE_SCREEN_BUFFER_INFO() + self.GetConsoleScreenBufferInfo(self.hout, byref(info)) + return (info.dwCursorPosition.X, info.dwCursorPosition.Y) + else: + return self.SetConsoleCursorPosition(self.hout, self.fixcoord(x, y)) + + def home(self): + '''Move to home.''' + self.pos(0,0) + +# Map ANSI color escape sequences into Windows Console Attributes + + terminal_escape = re.compile('(\001?\033\\[[0-9;]+m\002?)') + escape_parts = re.compile('\001?\033\\[([0-9;]+)m\002?') + escape_to_color = { '0;30': 0x0, #black + '0;31': 0x4, #red + '0;32': 0x2, #green + '0;33': 0x4+0x2, #brown? + '0;34': 0x1, #blue + '0;35': 0x1+0x4, #purple + '0;36': 0x2+0x4, #cyan + '0;37': 0x1+0x2+0x4, #grey + '1;30': 0x1+0x2+0x4, #dark gray + '1;31': 0x4+0x8, #red + '1;32': 0x2+0x8, #light green + '1;33': 0x4+0x2+0x8, #yellow + '1;34': 0x1+0x8, #light blue + '1;35': 0x1+0x4+0x8, #light purple + '1;36': 0x1+0x2+0x8, #light cyan + '1;37': 0x1+0x2+0x4+0x8, #white + '0': None, + } + + # This pattern should match all characters that change the cursor position differently + # than a normal character. + motion_char_re = re.compile('([\n\r\t\010\007])') + + def write_scrolling(self, text, attr=None): + '''write text at current cursor position while watching for scrolling. + + If the window scrolls because you are at the bottom of the screen + buffer, all positions that you are storing will be shifted by the + scroll amount. For example, I remember the cursor position of the + prompt so that I can redraw the line but if the window scrolls, + the remembered position is off. + + This variant of write tries to keep track of the cursor position + so that it will know when the screen buffer is scrolled. It + returns the number of lines that the buffer scrolled. + + ''' + x, y = self.pos() + w, h = self.size() + scroll = 0 # the result + + # split the string into ordinary characters and funny characters + chunks = self.motion_char_re.split(text) + for chunk in chunks: + log('C:'+chunk) + n = self.write_color(chunk, attr) + if len(chunk) == 1: # the funny characters will be alone + if chunk[0] == '\n': # newline + x = 0 + y += 1 + elif chunk[0] == '\r': # carriage return + x = 0 + elif chunk[0] == '\t': # tab + x = 8*(int(x/8)+1) + if x > w: # newline + x -= w + y += 1 + elif chunk[0] == '\007': # bell + pass + elif chunk[0] == '\010': + x -= 1 + if x < 0: + y -= 1 # backed up 1 line + else: # ordinary character + x += 1 + if x == w: # wrap + x = 0 + y += 1 + if y == h: # scroll + scroll += 1 + y = h - 1 + else: # chunk of ordinary characters + x += n + l = int(x / w) # lines we advanced + x = x % w # new x value + y += l + if y >= h: # scroll + scroll += y - h + 1 + y = h - 1 + return scroll + + def write_color(self, text, attr=None): + '''write text at current cursor position and interpret color escapes. + + return the number of characters written. + ''' + log('write_color("%s", %s)' % (text, attr)) + chunks = self.terminal_escape.split(text) + log('chunks=%s' % repr(chunks)) + junk = c_int(0) + n = 0 # count the characters we actually write, omitting the escapes + for chunk in chunks: + m = self.escape_parts.match(chunk) + if m: + attr = self.escape_to_color[m.group(1)] + continue + n += len(chunk) + log('attr=%s' % attr) + if attr is None: + attr = self.attr + self.SetConsoleTextAttribute(self.hout, attr) + self.WriteConsoleA(self.hout, chunk, len(chunk), byref(junk), None) + return n + + def write_plain(self, text, attr=None): + '''write text at current cursor position.''' + log('write("%s", %s)' %(text,attr)) + if attr is None: + attr = self.attr + n = c_int(0) + self.SetConsoleTextAttribute(self.hout, attr) + self.WriteConsoleA(self.hout, text, len(text), byref(n), None) + return len(text) + + # make this class look like a file object + def write(self, text): + log('write("%s")' % text) + return self.write_color(text) + + #write = write_scrolling + + def isatty(self): + return True + + def flush(self): + pass + + def page(self, attr=None, fill=' '): + '''Fill the entire screen.''' + if attr is None: + attr = self.attr + if len(fill) != 1: + raise ValueError + info = CONSOLE_SCREEN_BUFFER_INFO() + self.GetConsoleScreenBufferInfo(self.hout, byref(info)) + if info.dwCursorPosition.X != 0 or info.dwCursorPosition.Y != 0: + self.SetConsoleCursorPosition(self.hout, self.fixcoord(0, 0)) + + w = info.dwSize.X + n = c_int(0) + for y in range(info.dwSize.Y): + self.FillConsoleOutputAttribute(self.hout, attr, w, self.fixcoord(0, y), byref(n)) + self.FillConsoleOutputCharacterA(self.hout, ord(fill[0]), w, self.fixcoord(0, y), byref(n)) + + self.attr = attr + + def text(self, x, y, text, attr=None): + '''Write text at the given position.''' + if attr is None: + attr = self.attr + + pos = self.fixcoord(x, y) + n = c_int(0) + self.WriteConsoleOutputCharacterA(self.hout, text, len(text), pos, byref(n)) + self.FillConsoleOutputAttribute(self.hout, attr, n, pos, byref(n)) + + def rectangle(self, rect, attr=None, fill=' '): + '''Fill Rectangle.''' + x0, y0, x1, y1 = rect + n = c_int(0) + if attr is None: + attr = self.attr + for y in range(y0, y1): + pos = self.fixcoord(x0, y) + self.FillConsoleOutputAttribute(self.hout, attr, x1-x0, pos, byref(n)) + self.FillConsoleOutputCharacterA(self.hout, ord(fill[0]), x1-x0, pos, byref(n)) + + def scroll(self, rect, dx, dy, attr=None, fill=' '): + '''Scroll a rectangle.''' + if attr is None: + attr = self.attr + + x0, y0, x1, y1 = rect + source = SMALL_RECT(x0, y0, x1-1, y1-1) + dest = self.fixcoord(x0+dx, y0+dy) + style = CHAR_INFO() + style.Char.AsciiChar = fill[0] + style.Attributes = attr + + return self.ScrollConsoleScreenBufferA(self.hout, byref(source), byref(source), + dest, byref(style)) + + def scroll_window(self, lines): + '''Scroll the window by the indicated number of lines.''' + info = CONSOLE_SCREEN_BUFFER_INFO() + self.GetConsoleScreenBufferInfo(self.hout, byref(info)) + rect = info.srWindow + log('sw: rtop=%d rbot=%d' % (rect.Top, rect.Bottom)) + top = rect.Top + lines + bot = rect.Bottom + lines + h = bot - top + maxbot = info.dwSize.Y-1 + log('sw: lines=%d mb=%d top=%d bot=%d' % (lines,maxbot,top,bot)) + if top < 0: + top = 0 + bot = h + if bot > maxbot: + bot = maxbot + top = bot - h + + nrect = SMALL_RECT() + nrect.Top = top + nrect.Bottom = bot + nrect.Left = rect.Left + nrect.Right = rect.Right + log('sn: top=%d bot=%d' % (top,bot)) + r=self.SetConsoleWindowInfo(self.hout, True, byref(nrect)) + log('r=%d' % r) + + def get(self): + '''Get next event from queue.''' + inputHookFunc = c_int.from_address(self.inputHookPtr).value + + Cevent = INPUT_RECORD() + count = c_int(0) + while 1: + if inputHookFunc: + call_function(inputHookFunc, ()) + status = self.ReadConsoleInputA(self.hin, byref(Cevent), 1, byref(count)) + if status and count.value == 1: + e = event(self, Cevent) + return e + + def getkeypress(self): + '''Return next key press event from the queue, ignoring others.''' + while 1: + e = self.get() + if e.type == 'KeyPress' and e.keycode not in key_modifiers: + log(e) + if e.keysym == 'Next': + self.scroll_window(12) + elif e.keysym == 'Prior': + self.scroll_window(-12) + else: + return e + + def getchar(self): + '''Get next character from queue.''' + + Cevent = INPUT_RECORD() + count = c_int(0) + while 1: + status = self.ReadConsoleInputA(self.hin, byref(Cevent), 1, byref(count)) + if (status and count.value==1 and Cevent.EventType == 1 and + Cevent.Event.KeyEvent.bKeyDown): + sym = keysym(Cevent.Event.KeyEvent.wVirtualKeyCode) + if len(sym) == 0: + sym = Cevent.Event.KeyEvent.uChar.AsciiChar + return sym + + def peek(self): + '''Check event queue.''' + Cevent = INPUT_RECORD() + count = c_int(0) + status = PeekConsoleInput(self.hin, byref(Cevent), 1, byref(count)) + if status and count == 1: + return event(self, Cevent) + + def title(self, txt=None): + '''Set/get title.''' + if txt: + self.SetConsoleTitleA(txt) + else: + buffer = c_buffer(200) + n = self.GetConsoleTitleA(buffer, 200) + if n > 0: + return buffer.value[:n] + + def size(self, width=None, height=None): + '''Set/get window size.''' + info = CONSOLE_SCREEN_BUFFER_INFO() + status = self.GetConsoleScreenBufferInfo(self.hout, byref(info)) + if not status: + return None + if width is not None and height is not None: + wmin = info.srWindow.Right - info.srWindow.Left + 1 + hmin = info.srWindow.Bottom - info.srWindow.Top + 1 + #print wmin, hmin + width = max(width, wmin) + height = max(height, hmin) + #print width, height + self.SetConsoleScreenBufferSize(self.hout, self.fixcoord(width, height)) + else: + return (info.dwSize.X, info.dwSize.Y) + + def cursor(self, visible): + '''Set cursor on or off.''' + info = CONSOLE_CURSOR_INFO() + if self.GetConsoleCursorInfo(self.hout, byref(info)): + info.bVisible = visible + self.SetConsoleCursorInfo(self.hout, byref(info)) + + def bell(self): + self.write('\007') + + def next_serial(self): + '''Get next event serial number.''' + self.serial += 1 + return self.serial + +# add the functions from the dll to the class +for func in funcs: + setattr(Console, func, getattr(windll.kernel32, func)) + +class event(object): + '''Represent events from the console.''' + def __init__(self, console, input): + '''Initialize an event from the Windows input structure.''' + self.type = '??' + self.serial = console.next_serial() + self.width = 0 + self.height = 0 + self.x = 0 + self.y = 0 + self.char = '' + self.keycode = 0 + self.keysym = '??' + self.keyinfo = '' # a tuple with (control, meta, shift, keycode) for dispatch + self.width = None + + if input.EventType == KEY_EVENT: + if input.Event.KeyEvent.bKeyDown: + self.type = "KeyPress" + else: + self.type = "KeyRelease" + self.char = input.Event.KeyEvent.uChar.AsciiChar + self.keycode = input.Event.KeyEvent.wVirtualKeyCode + self.state = input.Event.KeyEvent.dwControlKeyState + self.keysym = make_keysym(self.keycode) + self.keyinfo = make_keyinfo(self.keycode, self.state) + elif input.EventType == MOUSE_EVENT: + if input.Event.MouseEvent.dwEventFlags & MOUSE_MOVED: + self.type = "Motion" + else: + self.type = "Button" + self.x = input.Event.MouseEvent.dwMousePosition.X + self.y = input.Event.MouseEvent.dwMousePosition.Y + self.state = input.Event.MouseEvent.dwButtonState + elif input.EventType == WINDOW_BUFFER_SIZE_EVENT: + self.type = "Configure" + self.width = input.Event.WindowBufferSizeEvent.dwSize.X + self.height = input.Event.WindowBufferSizeEvent.dwSize.Y + elif input.EventType == FOCUS_EVENT: + if input.Event.FocusEvent.bSetFocus: + self.type = "FocusIn" + else: + self.type = "FocusOut" + elif input.EventType == MENU_EVENT: + self.type = "Menu" + self.state = input.Event.MenuEvent.dwCommandId + + def __repr__(self): + '''Display an event for debugging.''' + if self.type in ['KeyPress', 'KeyRelease']: + s = "%s char='%s'%d keysym='%s' keycode=%d:%x state=%x keyinfo=%s" % \ + (self.type, self.char, ord(self.char), self.keysym, self.keycode, self.keycode, + self.state, self.keyinfo) + elif self.type in ['Motion', 'Button']: + s = '%s x=%d y=%d state=%x' % (self.type, self.x, self.y, self.state) + elif self.type == 'Configure': + s = '%s w=%d h=%d' % (self.type, self.width, self.height) + elif self.type in ['FocusIn', 'FocusOut']: + s = self.type + elif self.type == 'Menu': + s = '%s state=%x' % (self.type, self.state) + else: + s = 'unknown event type' + return s + +def getconsole(buffer=1): + """Get a console handle. + + If buffer is non-zero, a new console buffer is allocated and + installed. Otherwise, this returns a handle to the current + console buffer""" + + c = Console(buffer) + + return c + +# The following code uses ctypes to allow a Python callable to +# substitute for GNU readline within the Python interpreter. Calling +# raw_input or other functions that do input, inside your callable +# might be a bad idea, then again, it might work. + +# The Python callable can raise EOFError or KeyboardInterrupt and +# these will be translated into the appropriate outputs from readline +# so that they will then be translated back! + +# If the Python callable raises any other exception, a traceback will +# be printed and readline will appear to return an empty line. + +# I use ctypes to create a C-callable from a Python wrapper that +# handles the exceptions and gets the result into the right form. + +# the type for our C-callable wrapper +HOOKFUNC22 = CFUNCTYPE(c_char_p, c_char_p) +HOOKFUNC23 = CFUNCTYPE(c_char_p, c_void_p, c_void_p, c_char_p) + +readline_hook = None # the python hook goes here +readline_ref = None # this holds a reference to the c-callable to keep it alive + +def hook_wrapper_23(stdin, stdout, prompt): + '''Wrap a Python readline so it behaves like GNU readline.''' + try: + # call the Python hook + res = readline_hook(prompt) + # make sure it returned the right sort of thing + if res and not isinstance(res, str): + raise TypeError, 'readline must return a string.' + except KeyboardInterrupt: + # GNU readline returns 0 on keyboard interrupt + return 0 + except EOFError: + # It returns an empty string on EOF + res = '' + except: + print >>sys.stderr, 'Readline internal error' + traceback.print_exc() + res = '\n' + # we have to make a copy because the caller expects to free the result + n = len(res) + p = Console.PyMem_Malloc(n+1) + cdll.msvcrt.strncpy(p, res, n+1) + return p + +def hook_wrapper(prompt): + '''Wrap a Python readline so it behaves like GNU readline.''' + try: + # call the Python hook + res = readline_hook(prompt) + # make sure it returned the right sort of thing + if res and not isinstance(res, str): + raise TypeError, 'readline must return a string.' + except KeyboardInterrupt: + # GNU readline returns 0 on keyboard interrupt + return 0 + except EOFError: + # It returns an empty string on EOF + res = '' + except: + print >>sys.stderr, 'Readline internal error' + traceback.print_exc() + res = '\n' + # we have to make a copy because the caller expects to free the result + p = cdll.msvcrt._strdup(res) + return p + +def install_readline(hook): + '''Set up things for the interpreter to call our function like GNU readline.''' + global readline_hook, readline_ref + # save the hook so the wrapper can call it + readline_hook = hook + # get the address of PyOS_ReadlineFunctionPointer so we can update it + PyOS_RFP = c_int.from_address(Console.GetProcAddress(sys.dllhandle, + "PyOS_ReadlineFunctionPointer")) + # save a reference to the generated C-callable so it doesn't go away + if sys.version < '2.3': + readline_ref = HOOKFUNC22(hook_wrapper) + else: + readline_ref = HOOKFUNC23(hook_wrapper_23) + # get the address of the function + func_start = c_int.from_address(addressof(readline_ref)).value + # write the function address into PyOS_ReadlineFunctionPointer + PyOS_RFP.value = func_start + +if __name__ == '__main__': + import time, sys + c = Console(0) + sys.stdout = c + sys.stderr = c + c.page() + c.pos(5, 10) + c.write('hi there') + print 'some printed output' + for i in range(10): + c.getkeypress() + del c diff --git a/pyreadline/PyReadline.py b/pyreadline/PyReadline.py new file mode 100644 index 0000000..212e78d --- /dev/null +++ b/pyreadline/PyReadline.py @@ -0,0 +1,1122 @@ +''' an attempt to implement readline for Python in Python using ctypes''' + +import string +import math +import sys +from glob import glob +import os +import re +import traceback +import operator + +import win32con as c32 + +import Console +from Console import log +from keysyms import key_text_to_keyinfo + +def quote_char(c): + if ' ' <= c <= '~': + return c + else: + return repr(c)[1:-1] + +class Readline: + def __init__(self): + self.startup_hook = None + self.pre_input_hook = None + self.completer = None + self.completer_delims = " \t\n\"\\'`@$><=;|&{(" + self.history_length = -1 + self.history = [] # strings for previous commands + self.history_cursor = 0 + self.undo_stack = [] # each entry is a tuple with cursor_position and line_text + self.line_buffer = [] + self.line_cursor = 0 + self.console = Console.Console() + self.size = self.console.size() + self.prompt_color = None + self.command_color = None + self.key_dispatch = {} + self.previous_func = None + self.first_prompt = True + self.next_meta = False # True to force meta on next character + self.tabstop = 4 + + self.emacs_editing_mode(None) + self.begidx = 0 + self.endidx = 0 + + # variables you can control with parse_and_bind + self.show_all_if_ambiguous = 'off' + self.mark_directories = 'on' + self.bell_style = 'none' + + def _bell(self): + '''ring the bell if requested.''' + if self.bell_style == 'none': + self.console.bell() + + def _quoted_text(self): + quoted = [ quote_char(c) for c in self.line_buffer ] + self.line_char_width = [ len(c) for c in quoted ] + return ''.join(quoted) + + def _line_text(self): + return ''.join(self.line_buffer) + + def _set_line(self, text, cursor=None): + self.line_buffer = [ c for c in str(text) ] + if cursor is None: + self.line_cursor = len(self.line_buffer) + else: + self.line_cursor = cursor + + def _reset_line(self): + self.line_buffer = [] + self.line_cursor = 0 + self.undo_stack = [] + + def _clear_after(self): + c = self.console + x, y = c.pos() + w, h = c.size() + c.rectangle((x, y, w, y+1)) + c.rectangle((0, y+1, w, min(y+3,h))) + + def _set_cursor(self): + c = self.console + xc, yc = self.prompt_end_pos + w, h = c.size() + xc += reduce(operator.add, self.line_char_width[0:self.line_cursor], 0) + while(xc > w): + xc -= w + yc += 1 + c.pos(xc, yc) + + def _print_prompt(self): + c = self.console + log('prompt="%s"' % repr(self.prompt)) + x, y = c.pos() + n = c.write_scrolling(self.prompt, self.prompt_color) + self.prompt_begin_pos = (x, y - n) + self.prompt_end_pos = c.pos() + self.size = c.size() + + def _update_prompt_pos(self, n): + if n != 0: + bx, by = self.prompt_begin_pos + ex, ey = self.prompt_end_pos + self.prompt_begin_pos = (bx, by - n) + self.prompt_end_pos = (ex, ey - n) + + def readline(self, prompt=''): + '''Try to act like GNU readline.''' + + # handle startup_hook + if self.first_prompt: + self.first_prompt = False + if self.startup_hook: + try: + self.startup_hook() + except: + print 'startup hook failed' + traceback.print_exc() + + c = self.console + self._reset_line() + self.prompt = prompt + self._print_prompt() + + if self.pre_input_hook: + try: + self.pre_input_hook() + except: + print 'pre_input_hook failed' + traceback.print_exc() + self.pre_input_hook = None + + while 1: + c.pos(*self.prompt_end_pos) + ltext = self._quoted_text() + n = c.write_scrolling(ltext, self.command_color) + self._update_prompt_pos(n) + self._clear_after() + self._set_cursor() + + event = c.getkeypress() + if self.next_meta: + self.next_meta = False + control, meta, shift, code = event.keyinfo + event.keyinfo = (control, True, shift, code) + + try: + dispatch_func = self.key_dispatch[event.keyinfo] + except KeyError: + c.bell() + continue + r = None + if dispatch_func: + r = dispatch_func(event) + ltext = self._line_text() + if self.undo_stack and ltext == self.undo_stack[-1][1]: + self.undo_stack[-1][0] = self.line_cursor + else: + self.undo_stack.append([self.line_cursor, ltext]) + + self.previous_func = dispatch_func + if r: + break + + c.write('\r\n') + + rtext = self._line_text() + self.add_history(rtext) + + log('returning(%s)' % rtext) + return rtext + '\n' + + def parse_and_bind(self, string): + '''Parse and execute single line of a readline init file.''' + try: + log('parse_and_bind("%s")' % string) + if string.startswith('#'): + return + if string.startswith('set'): + m = re.compile(r'set\s+([-a-zA-Z0-9]+)\s+(.+)\s*$').match(string) + if m: + var_name = m.group(1) + val = m.group(2) + try: + setattr(self, var_name.replace('-','_'), val) + except AttributeError: + log('unknown var="%s" val="%s"' % (var_name, val)) + else: + log('bad set "%s"' % string) + return + log('before') + m = re.compile(r'\s*(.+)\s*:\s*([-a-zA-Z]+)\s*$').match(string) + log('here') + if m: + key = m.group(1) + func_name = m.group(2) + py_name = func_name.replace('-', '_') + try: + func = getattr(self, py_name) + except AttributeError: + log('unknown func key="%s" func="%s"' % (key, func_name)) + print 'unknown function to bind: "%s"' % func_name + self._bind_key(key, func) + except: + log('error') + traceback.print_exc() + raise + log('return') + + def get_line_buffer(self): + '''Return the current contents of the line buffer.''' + return "".join(self.line_buffer) + + def insert_text(self, string): + '''Insert text into the command line.''' + for c in string: + self.line_buffer.insert(self.line_cursor, c) + self.line_cursor += 1 + + def read_init_file(self, filename=None): + '''Parse a readline initialization file. The default filename is the last filename used.''' + log('read_init_file("%s")' % filename) + + def read_history_file(self, filename='~/.history'): + '''Load a readline history file. The default filename is ~/.history.''' + try: + for line in open(filename, 'rt'): + self.add_history(line.rstrip()) + except IOError: + self.history = [] + self.history_cursor = 0 + raise IOError + + def write_history_file(self, filename='~/.history'): + '''Save a readline history file. The default filename is ~/.history.''' + fp = open(filename, 'wb') + for line in self.history: + fp.write(line) + fp.write('\n') + fp.close() + + def get_history_length(self, ): + '''Return the desired length of the history file. + + Negative values imply unlimited history file size.''' + return self.history_length + + def set_history_length(self, length): + '''Set the number of lines to save in the history file. + + write_history_file() uses this value to truncate the history file + when saving. Negative values imply unlimited history file size. + ''' + self.history_length = length + + def set_startup_hook(self, function=None): + '''Set or remove the startup_hook function. + + If function is specified, it will be used as the new startup_hook + function; if omitted or None, any hook function already installed is + removed. The startup_hook function is called with no arguments just + before readline prints the first prompt. + + ''' + self.startup_hook = function + + def set_pre_input_hook(self, function=None): + '''Set or remove the pre_input_hook function. + + If function is specified, it will be used as the new pre_input_hook + function; if omitted or None, any hook function already installed is + removed. The pre_input_hook function is called with no arguments + after the first prompt has been printed and just before readline + starts reading input characters. + + ''' + self.pre_input_hook = function + + def set_completer(self, function=None): + '''Set or remove the completer function. + + If function is specified, it will be used as the new completer + function; if omitted or None, any completer function already + installed is removed. The completer function is called as + function(text, state), for state in 0, 1, 2, ..., until it returns a + non-string value. It should return the next possible completion + starting with text. + ''' + log('set_completer') + self.completer = function + + def get_completer(self): + '''Get the completer function. + ''' + + log('get_completer') + return self.completer + + def get_begidx(self): + '''Get the beginning index of the readline tab-completion scope.''' + return self.begidx + + def get_endidx(self): + '''Get the ending index of the readline tab-completion scope.''' + return self.endidx + + def set_completer_delims(self, string): + '''Set the readline word delimiters for tab-completion.''' + self.completer_delims = string + + def get_completer_delims(self): + '''Get the readline word delimiters for tab-completion.''' + return self.completer_delims + + def add_history(self, line): + '''Append a line to the history buffer, as if it was the last line typed.''' + if not line: + pass + elif len(self.history) > 0 and self.history[-1] == line: + pass + else: + self.history.append(line) + if self.history_length > 0 and len(self.history) > self.history_length: + self.history = self.history[-self.history_length:] + self.history_cursor = len(self.history) + + ### Methods below here are bindable functions + + def beginning_of_line(self, e): # (C-a) + '''Move to the start of the current line. ''' + self.line_cursor = 0 + + def end_of_line(self, e): # (C-e) + '''Move to the end of the line. ''' + self.line_cursor = len(self.line_buffer) + + def forward_char(self, e): # (C-f) + '''Move forward a character. ''' + if self.line_cursor < len(self.line_buffer): + self.line_cursor += 1 + else: + self._bell() + + def backward_char(self, e): # (C-b) + '''Move back a character. ''' + if self.line_cursor > 0: + self.line_cursor -= 1 + else: + self._bell() + + def forward_word(self, e): # (M-f) + '''Move forward to the end of the next word. Words are composed of + letters and digits.''' + L = len(self.line_buffer) + while self.line_cursor < L: + self.line_cursor += 1 + if self.line_cursor == L: + break + if self.line_buffer[self.line_cursor] not in string.letters + string.digits: + break + + def backward_word(self, e): # (M-b) + '''Move back to the start of the current or previous word. Words are + composed of letters and digits.''' + while self.line_cursor > 0: + self.line_cursor -= 1 + if self.line_buffer[self.line_cursor] not in string.letters + string.digits: + break + + def clear_screen(self, e): # (C-l) + '''Clear the screen and redraw the current line, leaving the current + line at the top of the screen.''' + self.console.page() + + def redraw_current_line(self, e): # () + '''Refresh the current line. By default, this is unbound.''' + pass + + def accept_line(self, e): # (Newline or Return) + '''Accept the line regardless of where the cursor is. If this line + is non-empty, it may be added to the history list for future recall + with add_history(). If this line is a modified history line, the + history line is restored to its original state.''' + return True + + def previous_history(self, e): # (C-p) + '''Move back through the history list, fetching the previous command. ''' + if self.history_cursor > 0: + self.history_cursor -= 1 + line = self.history[self.history_cursor] + self._set_line(line) + else: + self._bell() + + def next_history(self, e): # (C-n) + '''Move forward through the history list, fetching the next command. ''' + if self.history_cursor < len(self.history) - 1: + self.history_cursor += 1 + line = self.history[self.history_cursor] + self._set_line(line) + elif self.undo_stack: + cursor, text = self.undo_stack[-1] + self._set_line(text, cursor) + else: + self._bell() + + def beginning_of_history(self, e): # (M-<) + '''Move to the first line in the history.''' + self.history_cursor = 0 + if len(self.history) > 0: + self._set_line(self.history[0]) + else: + self._bell() + + def end_of_history(self, e): # (M->) + '''Move to the end of the input history, i.e., the line currently + being entered.''' + if self.undo_stack: + cursor, text = self.undo_stack[-1] + self._set_line(text, cursor) + else: + self._bell() + + def _i_search(self, direction, init_event): + c = self.console + line = self._line_text() + query = '' + hc_start = self.history_cursor + direction + hc = hc_start + while 1: + x, y = self.prompt_end_pos + c.pos(0, y) + if direction < 0: + prompt = 'reverse-i-search' + else: + prompt = 'forward-i-search' + + scroll = c.write_scrolling("%s`%s': %s" % (prompt, query, line)) + self._update_prompt_pos(scroll) + self._clear_after() + + event = c.getkeypress() + if event.keysym == 'BackSpace': + if len(query) > 0: + query = query[:-1] + hc = hc_start + else: + c.bell() + elif event.char in string.letters + string.digits + string.punctuation + ' ': + query += event.char + hc = hc_start + elif event.keyinfo == init_event.keyinfo: + hc += direction + else: + if event.keysym != 'Return': + c.bell() + break + + while (direction < 0 and hc >= 0) or (direction > 0 and hc < len(self.history)): + if self.history[hc].find(query) >= 0: + break + hc += direction + else: + c.bell() + continue + line = self.history[hc] + + px, py = self.prompt_begin_pos + c.pos(0, py) + self._set_line(line) + self._print_prompt() + + def reverse_search_history(self, e): # (C-r) + '''Search backward starting at the current line and moving up + through the history as necessary. This is an incremental search.''' + self._i_search(-1, e) + + def forward_search_history(self, e): # (C-s) + '''Search forward starting at the current line and moving down + through the the history as necessary. This is an incremental search.''' + self._i_search(1, e) + + def _non_i_search(self, direction): + c = self.console + line = self._line_text() + query = '' + while 1: + c.pos(*self.prompt_end_pos) + scroll = c.write_scrolling(":%s" % query) + self._update_prompt_pos(scroll) + self._clear_after() + + event = c.getkeypress() + if event.keysym == 'BackSpace': + if len(query) > 0: + query = query[:-1] + else: + break + elif event.char in string.letters + string.digits + string.punctuation + ' ': + query += event.char + elif event.keysym == 'Return': + break + else: + c.bell() + + if query: + hc = self.history_cursor - 1 + while (direction < 0 and hc >= 0) or (direction > 0 and hc < len(self.history)): + if self.history[hc].find(query) >= 0: + self._set_line(self.history[hc]) + self.history_cursor = hc + return + hc += direction + else: + c.bell() + + + def non_incremental_reverse_search_history(self, e): # (M-p) + '''Search backward starting at the current line and moving up + through the history as necessary using a non-incremental search for + a string supplied by the user.''' + self._non_i_search(-1) + + def non_incremental_forward_search_history(self, e): # (M-n) + '''Search forward starting at the current line and moving down + through the the history as necessary using a non-incremental search + for a string supplied by the user.''' + self._non_i_search(1) + + def _search(self, direction): + c = self.console + + if (self.previous_func != self.history_search_forward and + self.previous_func != self.history_search_backward): + self.query = ''.join(self.line_buffer[0:self.line_cursor]) + hc = self.history_cursor + direction + while (direction < 0 and hc >= 0) or (direction > 0 and hc < len(self.history)): + h = self.history[hc] + if not self.query: + self._set_line(h) + self.history_cursor = hc + return + elif h.startswith(self.query) and h != self._line_text: + self._set_line(h, len(self.query)) + self.history_cursor = hc + return + hc += direction + else: + self._set_line(self.query) + c.bell() + + def history_search_forward(self, e): # () + '''Search forward through the history for the string of characters + between the start of the current line and the point. This is a + non-incremental search. By default, this command is unbound.''' + self._search(1) + + def history_search_backward(self, e): # () + '''Search backward through the history for the string of characters + between the start of the current line and the point. This is a + non-incremental search. By default, this command is unbound.''' + self._search(-1) + + def yank_nth_arg(self, e): # (M-C-y) + '''Insert the first argument to the previous command (usually the + second word on the previous line) at point. With an argument n, + insert the nth word from the previous command (the words in the + previous command begin with word 0). A negative argument inserts the + nth word from the end of the previous command.''' + pass + + def yank_last_arg(self, e): # (M-. or M-_) + '''Insert last argument to the previous command (the last word of + the previous history entry). With an argument, behave exactly like + yank-nth-arg. Successive calls to yank-last-arg move back through + the history list, inserting the last argument of each line in turn.''' + pass + + def delete_char(self, e): # (C-d) + '''Delete the character at point. If point is at the beginning of + the line, there are no characters in the line, and the last + character typed was not bound to delete-char, then return EOF.''' + if len(self.line_buffer) == 0: + if self.previous_func != self.delete_char: + raise EOFError + self._bell() + if self.line_cursor < len(self.line_buffer): + del self.line_buffer[self.line_cursor] + else: + self._bell() + + def backward_delete_char(self, e): # (Rubout) + '''Delete the character behind the cursor. A numeric argument means + to kill the characters instead of deleting them.''' + if self.line_cursor > 0: + del self.line_buffer[self.line_cursor-1] + self.line_cursor -= 1 + + def forward_backward_delete_char(self, e): # () + '''Delete the character under the cursor, unless the cursor is at + the end of the line, in which case the character behind the cursor + is deleted. By default, this is not bound to a key.''' + pass + + def quoted_insert(self, e): # (C-q or C-v) + '''Add the next character typed to the line verbatim. This is how to + insert key sequences like C-q, for example.''' + e = self.console.getkeypress() + self.line_buffer.insert(self.line_cursor, e.char) + self.line_cursor += 1 + + def tab_insert(self, e): # (M-TAB) + '''Insert a tab character. ''' + ws = ' ' * (self.tabstop - (self.line_cursor%self.tabstop)) + self.insert_text(ws) + + def self_insert(self, e): # (a, b, A, 1, !, ...) + '''Insert yourself. ''' + self.line_buffer.insert(self.line_cursor, e.char) + self.line_cursor += 1 + + def transpose_chars(self, e): # (C-t) + '''Drag the character before the cursor forward over the character + at the cursor, moving the cursor forward as well. If the insertion + point is at the end of the line, then this transposes the last two + characters of the line. Negative arguments have no effect.''' + pass + + def transpose_words(self, e): # (M-t) + '''Drag the word before point past the word after point, moving + point past that word as well. If the insertion point is at the end + of the line, this transposes the last two words on the line.''' + pass + + def upcase_word(self, e): # (M-u) + '''Uppercase the current (or following) word. With a negative + argument, uppercase the previous word, but do not move the cursor.''' + pass + + def downcase_word(self, e): # (M-l) + '''Lowercase the current (or following) word. With a negative + argument, lowercase the previous word, but do not move the cursor.''' + pass + + def capitalize_word(self, e): # (M-c) + '''Capitalize the current (or following) word. With a negative + argument, capitalize the previous word, but do not move the cursor.''' + pass + + def overwrite_mode(self, e): # () + '''Toggle overwrite mode. With an explicit positive numeric + argument, switches to overwrite mode. With an explicit non-positive + numeric argument, switches to insert mode. This command affects only + emacs mode; vi mode does overwrite differently. Each call to + readline() starts in insert mode. In overwrite mode, characters + bound to self-insert replace the text at point rather than pushing + the text to the right. Characters bound to backward-delete-char + replace the character before point with a space.''' + pass + + def kill_line(self, e): # (C-k) + '''Kill the text from point to the end of the line. ''' + self.line_buffer[self.line_cursor:] = [] + + def backward_kill_line(self, e): # (C-x Rubout) + '''Kill backward to the beginning of the line. ''' + self.line_buffer[:self.line_cursor] = [] + self.line_cursor = 0 + + def unix_line_discard(self, e): # (C-u) + '''Kill backward from the cursor to the beginning of the current line. ''' + # how is this different from backward_kill_line? + self.line_buffer[:self.line_cursor] = [] + self.line_cursor = 0 + + def kill_whole_line(self, e): # () + '''Kill all characters on the current line, no matter where point + is. By default, this is unbound.''' + pass + + def kill_word(self, e): # (M-d) + '''Kill from point to the end of the current word, or if between + words, to the end of the next word. Word boundaries are the same as + forward-word.''' + begin = self.line_cursor + self.forward_word(e) + self.line_buffer[begin:self.line_cursor] = [] + self.line_cursor = begin + + def backward_kill_word(self, e): # (M-DEL) + '''Kill the word behind point. Word boundaries are the same as + backward-word. ''' + begin = self.line_cursor + self.backward_word(e) + self.line_buffer[self.line_cursor:begin] = [] + + def unix_word_rubout(self, e): # (C-w) + '''Kill the word behind point, using white space as a word + boundary. The killed text is saved on the kill-ring.''' + begin = self.line_cursor + while self.line_cursor > 0: + self.line_cursor -= 1 + if self.line_buffer[self.line_cursor] == ' ': + break + self.line_buffer[self.line_cursor:begin] = [] + + def delete_horizontal_space(self, e): # () + '''Delete all spaces and tabs around point. By default, this is unbound. ''' + pass + + def kill_region(self, e): # () + '''Kill the text in the current region. By default, this command is unbound. ''' + pass + + def copy_region_as_kill(self, e): # () + '''Copy the text in the region to the kill buffer, so it can be + yanked right away. By default, this command is unbound.''' + pass + + def copy_backward_word(self, e): # () + '''Copy the word before point to the kill buffer. The word + boundaries are the same as backward-word. By default, this command + is unbound.''' + pass + + def copy_forward_word(self, e): # () + '''Copy the word following point to the kill buffer. The word + boundaries are the same as forward-word. By default, this command is + unbound.''' + pass + + def yank(self, e): # (C-y) + '''Yank the top of the kill ring into the buffer at point. ''' + pass + + def yank_pop(self, e): # (M-y) + '''Rotate the kill-ring, and yank the new top. You can only do this + if the prior command is yank or yank-pop.''' + pass + + + def digit_argument(self, e): # (M-0, M-1, ... M--) + '''Add this digit to the argument already accumulating, or start a + new argument. M-- starts a negative argument.''' + pass + + def universal_argument(self, e): # () + '''This is another way to specify an argument. If this command is + followed by one or more digits, optionally with a leading minus + sign, those digits define the argument. If the command is followed + by digits, executing universal-argument again ends the numeric + argument, but is otherwise ignored. As a special case, if this + command is immediately followed by a character that is neither a + digit or minus sign, the argument count for the next command is + multiplied by four. The argument count is initially one, so + executing this function the first time makes the argument count + four, a second time makes the argument count sixteen, and so on. By + default, this is not bound to a key.''' + pass + + + def _get_completions(self): + '''Return a list of possible completions for the string ending at the point. + + Also set begidx and endidx in the process.''' + completions = [] + self.begidx = self.line_cursor + self.endidx = self.line_cursor + if self.completer: + # get the string to complete + while self.begidx > 0: + self.begidx -= 1 + if self.line_buffer[self.begidx] in self.completer_delims: + self.begidx += 1 + break + text = ''.join(self.line_buffer[self.begidx:self.endidx]) + log('complete text="%s"' % text) + i = 0 + while 1: + try: + r = self.completer(text, i) + except: + break + i += 1 + if r and r not in completions: + completions.append(r) + else: + break + log('text completions=%s' % completions) + if not completions: + # get the filename to complete + while self.begidx > 0: + self.begidx -= 1 + if self.line_buffer[self.begidx] in ' \t\n': + self.begidx += 1 + break + text = ''.join(self.line_buffer[self.begidx:self.endidx]) + log('file complete text="%s"' % text) + completions = glob(os.path.expanduser(text) + '*') + if self.mark_directories == 'on': + mc = [] + for f in completions: + if os.path.isdir(f): + mc.append(f + os.sep) + else: + mc.append(f) + completions = mc + log('fnames=%s' % completions) + return completions + + def _display_completions(self, completions): + if not completions: + return + self.console.write('\n') + wmax = max(map(len, completions)) + w, h = self.console.size() + cols = max(1, int((w-1) / (wmax+1))) + rows = int(math.ceil(float(len(completions)) / cols)) + for row in range(rows): + s = '' + for col in range(cols): + i = col*rows + row + if i < len(completions): + self.console.write(completions[i].ljust(wmax+1)) + self.console.write('\n') + self._print_prompt() + + def complete(self, e): # (TAB) + '''Attempt to perform completion on the text before point. The + actual completion performed is application-specific. The default is + filename completion.''' + completions = self._get_completions() + if completions: + cprefix = commonprefix(completions) + rep = [ c for c in cprefix ] + self.line_buffer[self.begidx:self.endidx] = rep + self.line_cursor += len(rep) - (self.endidx - self.begidx) + if len(completions) > 1: + if self.show_all_if_ambiguous == 'on': + self._display_completions(completions) + else: + self._bell() + else: + self._bell() + + def possible_completions(self, e): # (M-?) + '''List the possible completions of the text before point. ''' + completions = self._get_completions() + self._display_completions(completions) + + def insert_completions(self, e): # (M-*) + '''Insert all completions of the text before point that would have + been generated by possible-completions.''' + completions = self._get_completions() + b = self.begidx + e = self.endidx + for comp in completions: + rep = [ c for c in comp ] + rep.append(' ') + self.line_buffer[b:e] = rep + b += len(rep) + e = b + self.line_cursor = b + + def menu_complete(self, e): # () + '''Similar to complete, but replaces the word to be completed with a + single match from the list of possible completions. Repeated + execution of menu-complete steps through the list of possible + completions, inserting each match in turn. At the end of the list of + completions, the bell is rung (subject to the setting of bell-style) + and the original text is restored. An argument of n moves n + positions forward in the list of matches; a negative argument may be + used to move backward through the list. This command is intended to + be bound to TAB, but is unbound by default.''' + pass + + def delete_char_or_list(self, e): # () + '''Deletes the character under the cursor if not at the beginning or + end of the line (like delete-char). If at the end of the line, + behaves identically to possible-completions. This command is unbound + by default.''' + pass + + def start_kbd_macro(self, e): # (C-x () + '''Begin saving the characters typed into the current keyboard macro. ''' + pass + + def end_kbd_macro(self, e): # (C-x )) + '''Stop saving the characters typed into the current keyboard macro + and save the definition.''' + pass + + def call_last_kbd_macro(self, e): # (C-x e) + '''Re-execute the last keyboard macro defined, by making the + characters in the macro appear as if typed at the keyboard.''' + pass + + def re_read_init_file(self, e): # (C-x C-r) + '''Read in the contents of the inputrc file, and incorporate any + bindings or variable assignments found there.''' + pass + + def abort(self, e): # (C-g) + '''Abort the current editing command and ring the terminals bell + (subject to the setting of bell-style).''' + self._bell() + + def do_uppercase_version(self, e): # (M-a, M-b, M-x, ...) + '''If the metafied character x is lowercase, run the command that is + bound to the corresponding uppercase character.''' + pass + + def prefix_meta(self, e): # (ESC) + '''Metafy the next character typed. This is for keyboards without a + meta key. Typing ESC f is equivalent to typing M-f. ''' + self.next_meta = True + + def undo(self, e): # (C-_ or C-x C-u) + '''Incremental undo, separately remembered for each line.''' + log(self.undo_stack) + if len(self.undo_stack) >= 2: + self.undo_stack.pop() + cursor, text = self.undo_stack.pop() + else: + cursor = 0 + text = '' + self.undo_stack = [] + self._set_line(text, cursor) + + def revert_line(self, e): # (M-r) + '''Undo all changes made to this line. This is like executing the + undo command enough times to get back to the beginning.''' + pass + + def tilde_expand(self, e): # (M-~) + '''Perform tilde expansion on the current word.''' + pass + + def set_mark(self, e): # (C-@) + '''Set the mark to the point. If a numeric argument is supplied, the + mark is set to that position.''' + pass + + def exchange_point_and_mark(self, e): # (C-x C-x) + '''Swap the point with the mark. The current cursor position is set + to the saved position, and the old cursor position is saved as the + mark.''' + pass + + def character_search(self, e): # (C-]) + '''A character is read and point is moved to the next occurrence of + that character. A negative count searches for previous occurrences.''' + pass + + def character_search_backward(self, e): # (M-C-]) + '''A character is read and point is moved to the previous occurrence + of that character. A negative count searches for subsequent + occurrences.''' + pass + + def insert_comment(self, e): # (M-#) + '''Without a numeric argument, the value of the comment-begin + variable is inserted at the beginning of the current line. If a + numeric argument is supplied, this command acts as a toggle: if the + characters at the beginning of the line do not match the value of + comment-begin, the value is inserted, otherwise the characters in + comment-begin are deleted from the beginning of the line. In either + case, the line is accepted as if a newline had been typed.''' + pass + + def dump_functions(self, e): # () + '''Print all of the functions and their key bindings to the Readline + output stream. If a numeric argument is supplied, the output is + formatted in such a way that it can be made part of an inputrc + file. This command is unbound by default.''' + pass + + def dump_variables(self, e): # () + '''Print all of the settable variables and their values to the + Readline output stream. If a numeric argument is supplied, the + output is formatted in such a way that it can be made part of an + inputrc file. This command is unbound by default.''' + pass + + def dump_macros(self, e): # () + '''Print all of the Readline key sequences bound to macros and the + strings they output. If a numeric argument is supplied, the output + is formatted in such a way that it can be made part of an inputrc + file. This command is unbound by default.''' + pass + + def _bind_key(self, key, func): + '''setup the mapping from key to call the function.''' + keyinfo = key_text_to_keyinfo(key) + self.key_dispatch[keyinfo] = func + + def emacs_editing_mode(self, e): # (C-e) + '''When in vi command mode, this causes a switch to emacs editing + mode.''' + # make ' ' to ~ self insert + for c in range(ord(' '), 127): + self._bind_key('"%s"' % chr(c), self.self_insert) + # I often accidentally hold the shift or control while typing space + self._bind_key('Shift-space', self.self_insert) + self._bind_key('Control-space', self.self_insert) + self._bind_key('Return', self.accept_line) + self._bind_key('Left', self.backward_char) + self._bind_key('Control-b', self.backward_char) + self._bind_key('Right', self.forward_char) + self._bind_key('Control-f', self.forward_char) + self._bind_key('BackSpace', self.backward_delete_char) + self._bind_key('Home', self.beginning_of_line) + self._bind_key('End', self.end_of_line) + self._bind_key('Delete', self.delete_char) + self._bind_key('Control-d', self.delete_char) + self._bind_key('Clear', self.clear_screen) + self._bind_key('Alt-f', self.forward_word) + self._bind_key('Alt-b', self.backward_word) + self._bind_key('Control-l', self.clear_screen) + self._bind_key('Control-p', self.previous_history) + self._bind_key('Up', self.history_search_backward) + self._bind_key('Control-n', self.next_history) + self._bind_key('Down', self.history_search_forward) + self._bind_key('Control-a', self.beginning_of_line) + self._bind_key('Control-e', self.end_of_line) + self._bind_key('Alt-<', self.beginning_of_history) + self._bind_key('Alt->', self.end_of_history) + self._bind_key('Control-r', self.reverse_search_history) + self._bind_key('Control-s', self.forward_search_history) + self._bind_key('Alt-p', self.non_incremental_reverse_search_history) + self._bind_key('Alt-n', self.non_incremental_forward_search_history) + self._bind_key('Control-z', self.undo) + self._bind_key('Control-_', self.undo) + self._bind_key('Escape', self.prefix_meta) + self._bind_key('Meta-d', self.kill_word) + self._bind_key('Meta-Delete', self.backward_kill_word) + self._bind_key('Control-w', self.unix_word_rubout) + self._bind_key('Control-v', self.quoted_insert) + + # Add keybindings for numpad + # first the number keys + self._bind_key('NUMPAD0', self.self_insert) + self._bind_key('NUMPAD1', self.self_insert) + self._bind_key('NUMPAD2', self.self_insert) + self._bind_key('NUMPAD3', self.self_insert) + self._bind_key('NUMPAD4', self.self_insert) + self._bind_key('NUMPAD5', self.self_insert) + self._bind_key('NUMPAD6', self.self_insert) + self._bind_key('NUMPAD7', self.self_insert) + self._bind_key('NUMPAD8', self.self_insert) + self._bind_key('NUMPAD9', self.self_insert) + # then the others: / * - + + self._bind_key('Divide', self.self_insert) + self._bind_key('Multiply', self.self_insert) + self._bind_key('Add', self.self_insert) + self._bind_key('Subtract', self.self_insert) + # the decimal separator: '.' on US keyboards, ',' on DE one's + self._bind_key('VK_DECIMAL', self.self_insert) + + + def vi_editing_mode(self, e): # (M-C-j) + '''When in emacs editing mode, this causes a switch to vi editing + mode.''' + pass + +def CTRL(c): + '''make a control character''' + assert '@' <= c <= '_' + return chr(ord(c) - ord('@')) + +# make it case insensitive +def commonprefix(m): + "Given a list of pathnames, returns the longest common leading component" + if not m: return '' + prefix = m[0] + for item in m: + for i in range(len(prefix)): + if prefix[:i+1].lower() != item[:i+1].lower(): + prefix = prefix[:i] + if i == 0: return '' + break + return prefix + +# create a Readline object to contain the state +rl = Readline() + +def GetOutputFile(): + '''Return the console object used by readline so that it can be used for printing in color.''' + return rl.console + +# make these available so this looks like the python readline module +parse_and_bind = rl.parse_and_bind +get_line_buffer = rl.get_line_buffer +insert_text = rl.insert_text +read_init_file = rl.read_init_file +read_history_file = rl.read_history_file +write_history_file = rl.write_history_file +get_history_length = rl.get_history_length +set_history_length = rl.set_history_length +set_startup_hook = rl.set_startup_hook +set_pre_input_hook = rl.set_pre_input_hook +set_completer = rl.set_completer +get_completer = rl.get_completer +get_begidx = rl.get_begidx +get_endidx = rl.get_endidx +set_completer_delims = rl.set_completer_delims +get_completer_delims = rl.get_completer_delims +add_history = rl.add_history + +if __name__ == '__main__': + res = [ rl.readline('In[%d] ' % i) for i in range(3) ] + print res +else: + #import wingdbstub + Console.install_readline(rl.readline) + diff --git a/pyreadline/__init__.py b/pyreadline/__init__.py new file mode 100644 index 0000000..f4e260b --- /dev/null +++ b/pyreadline/__init__.py @@ -0,0 +1,19 @@ +from PyReadline import * + +__all__ = [ 'parse_and_bind', + 'get_line_buffer', + 'insert_text', + 'read_init_file', + 'read_history_file', + 'write_history_file', + 'get_history_length', + 'set_history_length', + 'set_startup_hook', + 'set_pre_input_hook', + 'set_completer', + 'get_completer', + 'get_begidx', + 'get_endidx', + 'set_completer_delims', + 'get_completer_delims', + 'add_history' ] diff --git a/pyreadline/keysyms.py b/pyreadline/keysyms.py new file mode 100644 index 0000000..8712c84 --- /dev/null +++ b/pyreadline/keysyms.py @@ -0,0 +1,173 @@ +import win32con as c32 +from ctypes import windll + +# table for translating virtual keys to X windows key symbols +code2sym_map = {c32.VK_CANCEL: 'Cancel', + c32.VK_BACK: 'BackSpace', + c32.VK_TAB: 'Tab', + c32.VK_CLEAR: 'Clear', + c32.VK_RETURN: 'Return', + c32.VK_SHIFT:'Shift_L', + c32.VK_CONTROL: 'Control_L', + c32.VK_MENU: 'Alt_L', + c32.VK_PAUSE: 'Pause', + c32.VK_CAPITAL: 'Caps_Lock', + c32.VK_ESCAPE: 'Escape', + c32.VK_SPACE: 'space', + c32.VK_PRIOR: 'Prior', + c32.VK_NEXT: 'Next', + c32.VK_END: 'End', + c32.VK_HOME: 'Home', + c32.VK_LEFT: 'Left', + c32.VK_UP: 'Up', + c32.VK_RIGHT: 'Right', + c32.VK_DOWN: 'Down', + c32.VK_SELECT: 'Select', + c32.VK_PRINT: 'Print', + c32.VK_EXECUTE: 'Execute', + c32.VK_SNAPSHOT: 'Snapshot', + c32.VK_INSERT: 'Insert', + c32.VK_DELETE: 'Delete', + c32.VK_HELP: 'Help', + c32.VK_F1: 'F1', + c32.VK_F2: 'F2', + c32.VK_F3: 'F3', + c32.VK_F4: 'F4', + c32.VK_F5: 'F5', + c32.VK_F6: 'F6', + c32.VK_F7: 'F7', + c32.VK_F8: 'F8', + c32.VK_F9: 'F9', + c32.VK_F10: 'F10', + c32.VK_F11: 'F11', + c32.VK_F12: 'F12', + c32.VK_F13: 'F13', + c32.VK_F14: 'F14', + c32.VK_F15: 'F15', + c32.VK_F16: 'F16', + c32.VK_F17: 'F17', + c32.VK_F18: 'F18', + c32.VK_F19: 'F19', + c32.VK_F20: 'F20', + c32.VK_F21: 'F21', + c32.VK_F22: 'F22', + c32.VK_F23: 'F23', + c32.VK_F24: 'F24', + c32.VK_NUMLOCK: 'Num_Lock,', + c32.VK_SCROLL: 'Scroll_Lock', + c32.VK_APPS: 'VK_APPS', + c32.VK_PROCESSKEY: 'VK_PROCESSKEY', + c32.VK_ATTN: 'VK_ATTN', + c32.VK_CRSEL: 'VK_CRSEL', + c32.VK_EXSEL: 'VK_EXSEL', + c32.VK_EREOF: 'VK_EREOF', + c32.VK_PLAY: 'VK_PLAY', + c32.VK_ZOOM: 'VK_ZOOM', + c32.VK_NONAME: 'VK_NONAME', + c32.VK_PA1: 'VK_PA1', + c32.VK_OEM_CLEAR: 'VK_OEM_CLEAR', + c32.VK_NUMPAD0: 'NUMPAD0', + c32.VK_NUMPAD1: 'NUMPAD1', + c32.VK_NUMPAD2: 'NUMPAD2', + c32.VK_NUMPAD3: 'NUMPAD3', + c32.VK_NUMPAD4: 'NUMPAD4', + c32.VK_NUMPAD5: 'NUMPAD5', + c32.VK_NUMPAD6: 'NUMPAD6', + c32.VK_NUMPAD7: 'NUMPAD7', + c32.VK_NUMPAD8: 'NUMPAD8', + c32.VK_NUMPAD9: 'NUMPAD9', + c32.VK_DIVIDE: 'Divide', + c32.VK_MULTIPLY: 'Multiply', + c32.VK_ADD: 'Add', + c32.VK_SUBTRACT: 'Subtract', + c32.VK_DECIMAL: 'VK_DECIMAL' + } + +# function to handle the mapping +def make_keysym(keycode): + try: + sym = code2sym_map[keycode] + except KeyError: + sym = '' + return sym + +sym2code_map = {} +for code,sym in code2sym_map.iteritems(): + sym2code_map[sym.lower()] = code + +def key_text_to_keyinfo(keytext): + '''Convert a GNU readline style textual description of a key to keycode with modifiers''' + if keytext.startswith('"'): # " + return keyseq_to_keyinfo(keytext[1:-1]) + else: + return keyname_to_keyinfo(keytext) + +VkKeyScan = windll.user32.VkKeyScanA + +def char_to_keyinfo(char, control=False, meta=False, shift=False): + vk = VkKeyScan(ord(char)) + if vk & 0xffff == 0xffff: + print 'VkKeyScan("%s") = %x' % (char, vk) + raise ValueError, 'bad key' + if vk & 0x100: + shift = True + if vk & 0x200: + control = True + if vk & 0x400: + meta = True + return (control, meta, shift, vk & 0xff) + +def keyname_to_keyinfo(keyname): + control = False + meta = False + shift = False + + while 1: + lkeyname = keyname.lower() + if lkeyname.startswith('control-'): + control = True + keyname = keyname[8:] + elif lkeyname.startswith('meta-'): + meta = True + keyname = keyname[5:] + elif lkeyname.startswith('alt-'): + meta = True + keyname = keyname[4:] + elif lkeyname.startswith('shift-'): + shift = True + keyname = keyname[6:] + else: + if len(keyname) > 1: + return (control, meta, shift, sym2code_map[keyname.lower()]) + else: + return char_to_keyinfo(keyname, control, meta, shift) + +def keyseq_to_keyinfo(keyseq): + res = [] + control = False + meta = False + shift = False + + while 1: + if keyseq.startswith('\\C-'): + control = True + keyseq = keyseq[3:] + elif keyseq.startswith('\\M-'): + meta = True + keyseq = keyseq[3:] + elif keyseq.startswith('\\e'): + res.append(char_to_keyinfo('\033', control, meta, shift)) + control = meta = shift = False + keyseq = keyseq[2:] + elif len(keyseq) >= 1: + res.append(char_to_keyinfo(keyseq[0], control, meta, shift)) + control = meta = shift = False + keyseq = keyseq[1:] + else: + return res[0] + +def make_keyinfo(keycode, state): + control = (state & (4+8)) != 0 + meta = (state & (1+2)) != 0 + shift = (state & 0x10) != 0 + return (control, meta, shift, keycode) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4c75912 --- /dev/null +++ b/setup.py @@ -0,0 +1,10 @@ +from distutils.core import setup + +setup(name="readline", + version="1.12", + description="Python implementation of GNU readline", + author="Gary Bishop", + author_email="gb@cs.unc.edu", + packages=['readline'], + ) +