--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/extras/pyrepl/reader.py Tue Feb 16 10:07:05 2010 +0530
@@ -0,0 +1,582 @@
+# Copyright 2000-2004 Michael Hudson mwh@python.net
+#
+# All Rights Reserved
+#
+# Portions Copyright (c) 2005 Nokia Corporation
+#
+# Permission to use, copy, modify, and distribute this software and
+# its documentation for any purpose is hereby granted without fee,
+# provided that the above copyright notice appear in all copies and
+# that both that copyright notice and this permission notice appear in
+# supporting documentation.
+#
+# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
+# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
+# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
+# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
+# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import types
+try:
+ import unicodedata
+except ImportError:
+ import dumbunicodedata as unicodedata
+from pyrepl import commands
+import ascii
+from pyrepl import input
+
+def _make_unctrl_map():
+ uc_map = {}
+ for c in map(unichr, range(256)):
+ if unicodedata.category(c)[0] <> 'C':
+ uc_map[c] = c
+ for i in range(32):
+ c = unichr(i)
+ uc_map[c] = u'^' + unichr(ord('A') + i - 1)
+ uc_map['\177'] = u'^?'
+ for i in range(256):
+ c = unichr(i)
+ if not uc_map.has_key(c):
+ uc_map[c] = u'\\%03o'%i
+ return uc_map
+
+# disp_str proved to be a bottleneck for large inputs, so it's been
+# rewritten in C; it's not required though.
+try:
+ raise ImportError # currently it's borked by the unicode support
+
+ from _pyrepl_utils import disp_str, init_unctrl_map
+
+ init_unctrl_map(_make_unctrl_map())
+
+ del init_unctrl_map
+except ImportError:
+ def _my_unctrl(c, u=_make_unctrl_map()):
+# import unicodedata
+ if c in u:
+ return u[c]
+ else:
+ if unicodedata.category(c).startswith('C'):
+ return '\u%04x'%(ord(c),)
+ else:
+ return c
+
+ def disp_str(buffer, join=''.join, uc=_my_unctrl):
+ """ disp_str(buffer:string) -> (string, [int])
+
+ Return the string that should be the printed represenation of
+ |buffer| and a list detailing where the characters of |buffer|
+ get used up. E.g.:
+
+ >>> disp_str(chr(3))
+ ('^C', [1, 0])
+
+ the list always contains 0s or 1s at present; it could conceivably
+ go higher as and when unicode support happens."""
+ s = map(uc, buffer)
+ return (join(s),
+ map(ord, join(map(lambda x:'\001'+(len(x)-1)*'\000', s))))
+
+ del _my_unctrl
+
+del _make_unctrl_map
+
+# syntax classes:
+
+[SYNTAX_WHITESPACE,
+ SYNTAX_WORD,
+ SYNTAX_SYMBOL] = range(3)
+
+def make_default_syntax_table():
+ # XXX perhaps should use some unicodedata here?
+ st = {}
+ for c in map(unichr, range(256)):
+ st[c] = SYNTAX_SYMBOL
+ for c in [a for a in map(unichr, range(256)) if a.isalpha()]:
+ st[c] = SYNTAX_WORD
+ st[u'\n'] = st[u' '] = SYNTAX_WHITESPACE
+ return st
+
+default_keymap = tuple(
+ [(r'\C-a', 'beginning-of-line'),
+ (r'\C-b', 'left'),
+ (r'\C-c', 'interrupt'),
+ (r'\C-d', 'delete'),
+ (r'\C-e', 'end-of-line'),
+ (r'\C-f', 'right'),
+ (r'\C-g', 'cancel'),
+ (r'\C-h', 'backspace'),
+ (r'\C-j', 'accept'),
+ (r'\<return>', 'accept'),
+ (r'\C-k', 'kill-line'),
+ (r'\C-l', 'clear-screen'),
+ (r'\C-m', 'accept'),
+ (r'\C-q', 'quoted-insert'),
+ (r'\C-t', 'transpose-characters'),
+ (r'\C-u', 'unix-line-discard'),
+ (r'\C-v', 'quoted-insert'),
+ (r'\C-w', 'unix-word-rubout'),
+ (r'\C-x\C-s', 'save-history'),
+ (r'\C-x\C-u', 'upcase-region'),
+ (r'\C-y', 'yank'),
+ (r'\C-z', 'suspend'),
+
+ (r'\M-b', 'backward-word'),
+ (r'\M-c', 'capitalize-word'),
+ (r'\M-d', 'kill-word'),
+ (r'\M-f', 'forward-word'),
+ (r'\M-l', 'downcase-word'),
+ (r'\M-t', 'transpose-words'),
+ (r'\M-u', 'upcase-word'),
+ (r'\M-y', 'yank-pop'),
+ (r'\M--', 'digit-arg'),
+ (r'\M-0', 'digit-arg'),
+ (r'\M-1', 'digit-arg'),
+ (r'\M-2', 'digit-arg'),
+ (r'\M-3', 'digit-arg'),
+ (r'\M-4', 'digit-arg'),
+ (r'\M-5', 'digit-arg'),
+ (r'\M-6', 'digit-arg'),
+ (r'\M-7', 'digit-arg'),
+ (r'\M-8', 'digit-arg'),
+ (r'\M-9', 'digit-arg'),
+ (r'\M-\n', 'insert-nl'),
+ ('\\\\', 'self-insert')] + \
+ [(c, 'self-insert')
+ for c in map(chr, range(32, 127)) if c <> '\\'] + \
+ [(c, 'self-insert')
+ for c in map(chr, range(128, 256)) if c.isalpha()] + \
+ [(r'\<up>', 'up'),
+ (r'\<down>', 'down'),
+ (r'\<left>', 'left'),
+ (r'\<right>', 'right'),
+ (r'\<insert>', 'quoted-insert'),
+ (r'\<delete>', 'delete'),
+ (r'\<backspace>', 'backspace'),
+ (r'\M-\<backspace>', 'backward-kill-word'),
+ (r'\<end>', 'end'),
+ (r'\<home>', 'home'),
+ (r'\<f1>', 'help'),
+ (r'\EOF', 'end'), # the entries in the terminfo database for xterms
+ (r'\EOH', 'home'), # seem to be wrong. this is a less than ideal
+ # workaround
+ ])
+
+del c # from the listcomps
+
+class Reader(object):
+ """The Reader class implements the bare bones of a command reader,
+ handling such details as editing and cursor motion. What it does
+ not support are such things as completion or history support -
+ these are implemented elsewhere.
+
+ Instance variables of note include:
+
+ * buffer:
+ A *list* (*not* a string at the moment :-) containing all the
+ characters that have been entered.
+ * console:
+ Hopefully encapsulates the OS dependent stuff.
+ * pos:
+ A 0-based index into `buffer' for where the insertion point
+ is.
+ * screeninfo:
+ Ahem. This list contains some info needed to move the
+ insertion point around reasonably efficiently. I'd like to
+ get rid of it, because its contents are obtuse (to put it
+ mildly) but I haven't worked out if that is possible yet.
+ * cxy, lxy:
+ the position of the insertion point in screen ... XXX
+ * syntax_table:
+ Dictionary mapping characters to `syntax class'; read the
+ emacs docs to see what this means :-)
+ * commands:
+ Dictionary mapping command names to command classes.
+ * arg:
+ The emacs-style prefix argument. It will be None if no such
+ argument has been provided.
+ * dirty:
+ True if we need to refresh the display.
+ * kill_ring:
+ The emacs-style kill-ring; manipulated with yank & yank-pop
+ * ps1, ps2, ps3, ps4:
+ prompts. ps1 is the prompt for a one-line input; for a
+ multiline input it looks like:
+ ps2> first line of input goes here
+ ps3> second and further
+ ps3> lines get ps3
+ ...
+ ps4> and the last one gets ps4
+ As with the usual top-level, you can set these to instances if
+ you like; str() will be called on them (once) at the beginning
+ of each command. Don't put really long or newline containing
+ strings here, please!
+ This is just the default policy; you can change it freely by
+ overriding get_prompt() (and indeed some standard subclasses
+ do).
+ * finished:
+ handle1 will set this to a true value if a command signals
+ that we're done.
+ """
+
+ help_text = """\
+This is pyrepl. Hear my roar.
+
+Helpful text may appear here at some point in the future when I'm
+feeling more loquacious than I am now."""
+
+ def __init__(self, console):
+ self.buffer = []
+ self.ps1 = "->> "
+ self.ps2 = "/>> "
+ self.ps3 = "|.. "
+ self.ps4 = "\__ "
+ self.kill_ring = []
+ self.arg = None
+ self.finished = 0
+ self.console = console
+ self.commands = {}
+ self.msg = ''
+ for v in vars(commands).values():
+ if ( isinstance(v, type)
+ and issubclass(v, commands.Command)
+ and v.__name__[0].islower() ):
+ self.commands[v.__name__] = v
+ self.commands[v.__name__.replace('_', '-')] = v
+ self.syntax_table = make_default_syntax_table()
+ self.input_trans_stack = []
+ self.keymap = self.collect_keymap()
+ self.input_trans = input.KeymapTranslator(
+ self.keymap,
+ invalid_cls='invalid-key',
+ character_cls='self-insert')
+
+ def collect_keymap(self):
+ return default_keymap
+
+ def calc_screen(self):
+ """The purpose of this method is to translate changes in
+ self.buffer into changes in self.screen. Currently it rips
+ everything down and starts from scratch, which whilst not
+ especially efficient is certainly simple(r).
+ """
+ lines = self.get_unicode().split("\n")
+ screen = []
+ screeninfo = []
+ w = self.console.width - 1
+ p = self.pos
+ for ln, line in zip(range(len(lines)), lines):
+ ll = len(line)
+ if 0 <= p <= ll:
+ if self.msg:
+ for mline in self.msg.split("\n"):
+ screen.append(mline)
+ screeninfo.append((0, []))
+ self.lxy = p, ln
+ prompt = self.get_prompt(ln, ll >= p >= 0)
+ p -= ll + 1
+ lp = len(prompt)
+ l, l2 = disp_str(line)
+ wrapcount = (len(l) + lp) / w
+ if wrapcount == 0:
+ screen.append(prompt + l)
+ screeninfo.append((lp, l2+[1]))
+ else:
+ screen.append(prompt + l[:w-lp] + "\\")
+ screeninfo.append((lp, l2[:w-lp]))
+ for i in range(-lp + w, -lp + wrapcount*w, w):
+ screen.append(l[i:i+w] + "\\")
+ screeninfo.append((0, l2[i:i + w]))
+ screen.append(l[wrapcount*w - lp:])
+ screeninfo.append((0, l2[wrapcount*w - lp:]+[1]))
+ self.screeninfo = screeninfo
+ self.cxy = self.pos2xy(self.pos)
+ return screen
+
+ def bow(self, p=None):
+ """Return the 0-based index of the word break preceding p most
+ immediately.
+
+ p defaults to self.pos; word boundaries are determined using
+ self.syntax_table."""
+ if p is None:
+ p = self.pos
+ st = self.syntax_table
+ b = self.buffer
+ p -= 1
+ while p >= 0 and st.get(b[p], SYNTAX_WORD) <> SYNTAX_WORD:
+ p -= 1
+ while p >= 0 and st.get(b[p], SYNTAX_WORD) == SYNTAX_WORD:
+ p -= 1
+ return p + 1
+
+ def eow(self, p=None):
+ """Return the 0-based index of the word break following p most
+ immediately.
+
+ p defaults to self.pos; word boundaries are determined using
+ self.syntax_table."""
+ if p is None:
+ p = self.pos
+ st = self.syntax_table
+ b = self.buffer
+ while p < len(b) and st.get(b[p], SYNTAX_WORD) <> SYNTAX_WORD:
+ p += 1
+ while p < len(b) and st.get(b[p], SYNTAX_WORD) == SYNTAX_WORD:
+ p += 1
+ return p
+
+ def bol(self, p=None):
+ """Return the 0-based index of the line break preceding p most
+ immediately.
+
+ p defaults to self.pos."""
+ # XXX there are problems here.
+ if p is None:
+ p = self.pos
+ b = self.buffer
+ p -= 1
+ while p >= 0 and b[p] <> '\n':
+ p -= 1
+ return p + 1
+
+ def eol(self, p=None):
+ """Return the 0-based index of the line break following p most
+ immediately.
+
+ p defaults to self.pos."""
+ if p is None:
+ p = self.pos
+ b = self.buffer
+ while p < len(b) and b[p] <> '\n':
+ p += 1
+ return p
+
+ def get_arg(self, default=1):
+ """Return any prefix argument that the user has supplied,
+ returning `default' if there is None. `default' defaults
+ (groan) to 1."""
+ if self.arg is None:
+ return default
+ else:
+ return self.arg
+
+ def get_prompt(self, lineno, cursor_on_line):
+ """Return what should be in the left-hand margin for line
+ `lineno'."""
+ if self.arg is not None and cursor_on_line:
+ return "(arg: %s) "%self.arg
+ if "\n" in self.buffer:
+ if lineno == 0:
+ return self._ps2
+ elif lineno == self.buffer.count("\n"):
+ return self._ps4
+ else:
+ return self._ps3
+ else:
+ return self._ps1
+
+ def push_input_trans(self, itrans):
+ self.input_trans_stack.append(self.input_trans)
+ self.input_trans = itrans
+
+ def pop_input_trans(self):
+ self.input_trans = self.input_trans_stack.pop()
+
+ def pos2xy(self, pos):
+ """Return the x, y coordinates of position 'pos'."""
+ # this *is* incomprehensible, yes.
+ y = 0
+ assert 0 <= pos <= len(self.buffer)
+ if pos == len(self.buffer):
+ y = len(self.screeninfo) - 1
+ p, l2 = self.screeninfo[y]
+ return p + len(l2) - 1, y
+ else:
+ for p, l2 in self.screeninfo:
+ l = l2.count(1)
+ if l > pos:
+ break
+ else:
+ pos -= l
+ y += 1
+ c = 0
+ i = 0
+ while c < pos:
+ c += l2[i]
+ i += 1
+ while l2[i] == 0:
+ i += 1
+ return p + i, y
+
+ def insert(self, text):
+ """Insert 'text' at the insertion point."""
+ self.buffer[self.pos:self.pos] = list(text)
+ self.pos += len(text)
+ self.dirty = 1
+
+ def update_cursor(self):
+ """Move the cursor to reflect changes in self.pos"""
+ self.cxy = self.pos2xy(self.pos)
+ self.console.move_cursor(*self.cxy)
+
+ def after_command(self, cmd):
+ """This function is called to allow post command cleanup."""
+ if getattr(cmd, "kills_digit_arg", 1):
+ if self.arg is not None:
+ self.dirty = 1
+ self.arg = None
+
+ def prepare(self):
+ """Get ready to run. Call restore when finished. You must not
+ write to the console in between the calls to prepare and
+ restore."""
+ try:
+ self.console.prepare()
+ self.arg = None
+ self.screeninfo = []
+ self.finished = 0
+ del self.buffer[:]
+ self.pos = 0
+ self.dirty = 1
+ self.last_command = None
+ self._ps1, self._ps2, self._ps3, self._ps4 = \
+ map(str, [self.ps1, self.ps2, self.ps3, self.ps4])
+ except:
+ self.restore()
+ raise
+
+ def last_command_is(self, klass):
+ if not self.last_command:
+ return 0
+ return issubclass(klass, self.last_command)
+
+ def restore(self):
+ """Clean up after a run."""
+ self.console.restore()
+
+ def finish(self):
+ """Called when a command signals that we're finished."""
+ pass
+
+ def message(self, msg="none"):
+ self.msg = "[ "+msg+" ] "
+ self.dirty = 1
+
+ def error(self, msg="none"):
+ self.msg = "! " + msg + " "
+ self.dirty = 1
+ self.console.beep()
+
+ def refresh(self):
+ """Recalculate and refresh the screen."""
+ if self.console.isbusy():
+ return
+ # this call sets up self.cxy, so call it first.
+ screen = self.calc_screen()
+ self.console.refresh(screen, self.cxy)
+ self.dirty = 0 # forgot this for a while (blush)
+
+ def do_cmd(self, cmd):
+ if isinstance(cmd[0], str):
+ cmd = self.commands.get(cmd[0],
+ commands.invalid_command)(self, cmd)
+ elif isinstance(cmd[0], type):
+ cmd = cmd[0](self, cmd)
+ else:
+ cmd=cmd[0]
+
+ cmd.do()
+
+ self.after_command(cmd)
+
+ if self.dirty:
+ self.refresh()
+ else:
+ self.update_cursor()
+
+ if not isinstance(cmd, commands.digit_arg):
+ self.last_command = cmd.__class__
+
+ self.finished = cmd.finish
+ if self.finished:
+ self.console.finish()
+ self.finish()
+
+ def handle1(self, block=1):
+ """Handle a single event. Wait as long as it takes if block
+ is true (the default), otherwise return None if no event is
+ pending."""
+
+ if self.msg:
+ self.msg = ''
+ self.dirty = 1
+
+ while 1:
+ event = self.console.get_event(block)
+ if not event: # can only happen if we're not blocking
+ return None
+
+ if event.evt == 'key':
+ self.input_trans.push(event)
+ elif event.evt == 'scroll':
+ self.refresh()
+ elif event.evt == 'resize':
+ self.refresh()
+ else:
+ pass
+
+ cmd = self.input_trans.get()
+
+ if cmd is None:
+ if block:
+ continue
+ else:
+ return None
+
+ self.do_cmd(cmd)
+ return 1
+
+ def readline(self):
+ """Read a line. The implementation of this method also shows
+ how to drive Reader if you want more control over the event
+ loop."""
+ self.prepare()
+ try:
+ self.refresh()
+ while not self.finished:
+ self.handle1()
+ return self.get_buffer()
+ finally:
+ self.restore()
+
+ def bind(self, spec, command):
+ self.keymap = self.keymap + ((spec, command),)
+ self.input_trans = input.KeymapTranslator(
+ self.keymap,
+ invalid_cls='invalid-key',
+ character_cls='self-insert')
+
+ def get_buffer(self, encoding=None):
+ if encoding is None:
+ encoding = self.console.encoding
+ return u''.join(self.buffer).encode(self.console.encoding)
+
+ def get_unicode(self):
+ """Return the current buffer as a unicode string."""
+ return u''.join(self.buffer)
+
+def test():
+ from pyrepl.unix_console import UnixConsole
+ reader = Reader(UnixConsole())
+ reader.ps1 = "**> "
+ reader.ps2 = "/*> "
+ reader.ps3 = "|*> "
+ reader.ps4 = "\*> "
+ while reader.readline():
+ pass
+
+if __name__=='__main__':
+ test()