| # VT100 terminal emulator. | 
 | # This is incomplete and slow, but will do for now... | 
 | # It shouldn't be difficult to extend it to be a more-or-less complete | 
 | # VT100 emulator.  And little bit of profiling could go a long way... | 
 |  | 
 | from array import array | 
 | import regex | 
 | import string | 
 |  | 
 | # Tunable parameters | 
 | DEBUGLEVEL = 1 | 
 |  | 
 | # Symbolic constants | 
 | ESC = '\033' | 
 |  | 
 |  | 
 | # VT100 emulation class | 
 |  | 
 | class VT100: | 
 |  | 
 | 	def __init__(self): | 
 | 		self.debuglevel = DEBUGLEVEL | 
 | 		# Unchangeable parameters (for now) | 
 | 		self.width = 80 | 
 | 		self.height = 24 | 
 | 		self.blankline = array('c', ' '*self.width) | 
 | 		self.blankattr = array('b', '\0'*self.width) | 
 | 		# Set mutable display state | 
 | 		self.reset() | 
 | 		# Set parser state | 
 | 		self.unfinished = '' | 
 | 		# Set screen recognition state | 
 | 		self.reset_recognizer() | 
 |  | 
 | 	def msg(self, msg, *args): | 
 | 		if self.debuglevel > 0: | 
 | 			print 'VT100:', msg%args | 
 |  | 
 | 	def set_debuglevel(self, debuglevel): | 
 | 		self.debuglevel = debuglevel | 
 |  | 
 | 	def reset(self): | 
 | 		self.lines = [] | 
 | 		self.attrs = [] | 
 | 		self.fill_bottom() | 
 | 		self.x = 0 | 
 | 		self.y = 0 | 
 | 		self.curattrs = [] | 
 |  | 
 | 	def show(self): | 
 | 		lineno = 0 | 
 | 		for line in self.lines: | 
 | 			lineno = lineno + 1 | 
 | 			i = len(line) | 
 | 			while i > 0 and line[i-1] == ' ': i = i-1 | 
 | 			print line[:i] | 
 | 			print 'CURSOR:', self.x, self.y | 
 |  | 
 | 	def fill_bottom(self): | 
 | 		while len(self.lines) < self.height: | 
 | 			self.lines.append(self.blankline[:]) | 
 | 			self.attrs.append(self.blankattr[:]) | 
 |  | 
 | 	def fill_top(self): | 
 | 		while len(self.lines) < self.height: | 
 | 			self.lines.insert(0, self.blankline[:]) | 
 | 			self.attrs.insert(0, self.blankattr[:]) | 
 |  | 
 | 	def clear_all(self): | 
 | 		self.lines = [] | 
 | 		self.attrs = [] | 
 | 		self.fill_bottom() | 
 |  | 
 | 	def clear_below(self): | 
 | 		del self.lines[self.y:] | 
 | 		del self.attrs[self.y:] | 
 | 		self.fill_bottom() | 
 |  | 
 | 	def clear_above(self): | 
 | 		del self.lines[:self.y] | 
 | 		del self.attrs[:self.y] | 
 | 		self.fill_top() | 
 |  | 
 | 	def send(self, buffer): | 
 | 		self.msg('send: unfinished=%s, buffer=%s', | 
 | 			  `self.unfinished`, `buffer`) | 
 | 		self.unfinished = self.unfinished + buffer | 
 | 		i = 0 | 
 | 		n = len(self.unfinished) | 
 | 		while i < n: | 
 | 			c = self.unfinished[i] | 
 | 			i = i+1 | 
 | 			if c != ESC: | 
 | 				self.add_char(c) | 
 | 				continue | 
 | 			if i >= n: | 
 | 				i = i-1 | 
 | 				break | 
 | 			c = self.unfinished[i] | 
 | 			i = i+1 | 
 | 			if c == 'c': | 
 | 				self.reset() | 
 | 				continue | 
 | 			if c <> '[': | 
 | 				self.msg('unrecognized: ESC %s', `c`) | 
 | 				continue | 
 | 			argstr = '' | 
 | 			while i < n: | 
 | 				c = self.unfinished[i] | 
 | 				i = i+1 | 
 | 				if c not in '0123456789;': | 
 | 					break | 
 | 				argstr = argstr + c | 
 | 			else: | 
 | 				i = i - len(argstr) - 2 | 
 | 				break | 
 | ##			self.msg('found ESC [ %s %s' % (`argstr`, `c`)) | 
 | 			args = string.splitfields(argstr, ';') | 
 | 			for j in range(len(args)): | 
 | 				s = args[j] | 
 | 				while s[:1] == '0': s = s[1:] | 
 | 				if s: args[j] = eval(s) | 
 | 				else: args[j] = 0 | 
 | 			p1 = p2 = 0 | 
 | 			if args: p1 = args[0] | 
 | 			if args[1:]: p2 = args[1] | 
 | 			if c in '@ABCDH': | 
 | 				if not p1: p1 = 1 | 
 | 			if c in 'H': | 
 | 				if not p2: p2 = 1 | 
 | 			if c == '@': | 
 | 				for j in range(p1): | 
 | 					self.add_char(' ') | 
 | 			elif c == 'A': | 
 | 				self.move_by(0, -p1) | 
 | 			elif c == 'B': | 
 | 				self.move_by(0, p1) | 
 | 			elif c == 'C': | 
 | 				self.move_by(p1, 0) | 
 | 			elif c == 'D': | 
 | 				self.move_by(-p1, 0) | 
 | 			elif c == 'H': | 
 | 				self.move_to(p2-1, p1-1) | 
 | 			elif c == 'J': | 
 | 				if p1 == 0: self.clear_above() | 
 | 				elif p1 == 1: self.clear_below() | 
 | 				elif p1 == 2: self.clear_all() | 
 | 				else: self.msg('weird ESC [ %d J', p1) | 
 | 			elif c == 'K': | 
 | 				if p1 == 0: self.erase_right() | 
 | 				elif p1 == 1: self.erase_left() | 
 | 				elif p1 == 2: self.erase_line() | 
 | 				else: self.msg('weird ESC [ %d K', p1) | 
 | 			elif c == 'm': | 
 | 				if p1 == 0: | 
 | 					self.curattrs = [] | 
 | 				else: | 
 | 					if p1 not in self.curattrs: | 
 | 						self.curattrs.append(p1) | 
 | 						self.curattrs.sort() | 
 | 			else: | 
 | 				self.msg('unrecognized: ESC [ %s', `argstr+c`) | 
 | 		self.unfinished = self.unfinished[i:] | 
 |  | 
 | 	def add_char(self, c): | 
 | 		if c == '\r': | 
 | 			self.move_to(0, self.y) | 
 | 			return | 
 | 		if c in '\n\f\v': | 
 | 			self.move_to(self.x, self.y + 1) | 
 | 			if self.y >= self.height: | 
 | 				self.scroll_up(1) | 
 | 				self.move_to(self.x, self.height - 1) | 
 | 			return | 
 | 		if c == '\b': | 
 | 			self.move_by(-1, 0) | 
 | 			return | 
 | 		if c == '\a': | 
 | 			self.msg('BELL') | 
 | 			return | 
 | 		if c == '\t': | 
 | 			self.move_to((self.x+8)/8*8, self.y) | 
 | 			return | 
 | 		if c == '\0': | 
 | 			return | 
 | 		if c < ' ' or c > '~': | 
 | 			self.msg('ignored control char: %s', `c`) | 
 | 			return | 
 | 		if self.x >= self.width: | 
 | 			self.move_to(0, self.y + 1) | 
 | 		if self.y >= self.height: | 
 | 			self.scroll_up(1) | 
 | 			self.move_to(self.x, self.height - 1) | 
 | 		self.lines[self.y][self.x] = c | 
 | 		if self.curattrs: | 
 | 			self.attrs[self.y][self.x] = max(self.curattrs) | 
 | 		else: | 
 | 			self.attrs[self.y][self.x] = 0 | 
 | 		self.move_by(1, 0) | 
 |  | 
 | 	def move_to(self, x, y): | 
 | 		self.x = min(max(0, x), self.width) | 
 | 		self.y = min(max(0, y), self.height) | 
 |  | 
 | 	def move_by(self, dx, dy): | 
 | 		self.move_to(self.x + dx, self.y + dy) | 
 |  | 
 | 	def scroll_up(self, nlines): | 
 | 		del self.lines[:max(0, nlines)] | 
 | 		del self.attrs[:max(0, nlines)] | 
 | 		self.fill_bottom() | 
 |  | 
 | 	def scroll_down(self, nlines): | 
 | 		del self.lines[-max(0, nlines):] | 
 | 		del self.attrs[-max(0, nlines):] | 
 | 		self.fill_top() | 
 |  | 
 | 	def erase_left(self): | 
 | 		x = min(self.width-1, x) | 
 | 		y = min(self.height-1, y) | 
 | 		self.lines[y][:x] = self.blankline[:x] | 
 | 		self.attrs[y][:x] = self.blankattr[:x] | 
 |  | 
 | 	def erase_right(self): | 
 | 		x = min(self.width-1, x) | 
 | 		y = min(self.height-1, y) | 
 | 		self.lines[y][x:] = self.blankline[x:] | 
 | 		self.attrs[y][x:] = self.blankattr[x:] | 
 |  | 
 | 	def erase_line(self): | 
 | 		self.lines[y][:] = self.blankline | 
 | 		self.attrs[y][:] = self.blankattr | 
 |  | 
 | 	# The following routines help automating the recognition of | 
 | 	# standard screens.  A standard screen is characterized by | 
 | 	# a number of fields.  A field is part of a line, | 
 | 	# characterized by a (lineno, begin, end) tuple; | 
 | 	# e.g. the first 10 characters of the second line are | 
 | 	# specified by the tuple (1, 0, 10).  Fields can be: | 
 | 	# - regex: desired contents given by a regular expression, | 
 | 	# - extract: can be extracted, | 
 | 	# - cursor: screen is only valid if cursor in field, | 
 | 	# - copy: identical to another screen (position is ignored). | 
 | 	# A screen is defined as a dictionary full of fields.  Screens | 
 | 	# also have names and are placed in a dictionary. | 
 |  | 
 | 	def reset_recognizer(self): | 
 | 		self.screens = {} | 
 |  | 
 | 	def define_screen(self, screenname, fields): | 
 | 		fieldscopy = {} | 
 | 		# Check if the fields make sense | 
 | 		for fieldname in fields.keys(): | 
 | 			field = fields[fieldname] | 
 | 			ftype, lineno, begin, end, extra = field | 
 | 			if ftype in ('match', 'search'): | 
 | 				extra = regex.compile(extra) | 
 | 			elif ftype == 'extract': | 
 | 				extra = None | 
 | 			elif ftype == 'cursor': | 
 | 				extra = None | 
 | 			elif ftype == 'copy': | 
 | 				if not self.screens.has_key(extra): | 
 | 					raise ValueError, 'bad copy ref' | 
 | 			else: | 
 | 				raise ValueError, 'bad ftype: %s' % `ftype` | 
 | 			fieldscopy[fieldname] = ( | 
 | 				  ftype, lineno, begin, end, extra) | 
 | 		self.screens[screenname] = fieldscopy | 
 |  | 
 | 	def which_screens(self): | 
 | 		self.busy = [] | 
 | 		self.okay = [] | 
 | 		self.fail = [] | 
 | 		for name in self.screens.keys(): | 
 | 			ok = self.match_screen(name) | 
 | 		return self.okay[:] | 
 |  | 
 | 	def match_screen(self, name): | 
 | 		if name in self.busy: raise RuntimeError, 'recursive match' | 
 | 		if name in self.okay: return 1 | 
 | 		if name in self.fail: return 0 | 
 | 		self.busy.append(name) | 
 | 		fields = self.screens[name] | 
 | 		ok = 0 | 
 | 		for key in fields.keys(): | 
 | 			field = fields[key] | 
 | 			ftype, lineno, begin, end, extra = field | 
 | 			if ftype == 'copy': | 
 | 				if not self.match_screen(extra): break | 
 | 			elif ftype == 'search': | 
 | 				text = self.lines[lineno][begin:end].tostring() | 
 | 				if extra.search(text) < 0: | 
 | 					break | 
 | 			elif ftype == 'match': | 
 | 				text = self.lines[lineno][begin:end].tostring() | 
 | 				if extra.match(text) < 0: | 
 | 					break | 
 | 			elif ftype == 'cursor': | 
 | 				if self.x != lineno or not \ | 
 | 					  begin <= self.y < end: | 
 | 					break | 
 | 		else: | 
 | 			ok = 1 | 
 | 		if ok: | 
 | 			self.okay.append(name) | 
 | 		else: | 
 | 			self.fail.append(name) | 
 | 		self.busy.remove(name) | 
 | 		return ok | 
 |  | 
 | 	def extract_field(self, screenname, fieldname): | 
 | 		ftype, lineno, begin, end, extra = \ | 
 | 			  self.screens[screenname][fieldname] | 
 | 		return stripright(self.lines[lineno][begin:end].tostring()) | 
 |  | 
 | 	def extract_rect(self, left, top, right, bottom): | 
 | 		lines = [] | 
 | 		for i in range(top, bottom): | 
 | 			lines.append(stripright(self.lines[i][left:right]) | 
 | 				  .tostring()) | 
 | 		return lines | 
 |  | 
 |  | 
 | def stripright(line): | 
 | 	i = len(line) | 
 | 	while i > 0 and line[i-1] in string.whitespace: i = i-1 | 
 | 	return line[:i] |