Permalink
Switch branches/tags
Find file
Fetching contributors…
Cannot retrieve contributors at this time
executable file 2528 lines (2097 sloc) 73.9 KB
#!/usr/bin/env python
"""
NAME
====
vt100.py - Parse a typescript and output text.
SYNOPSIS
========
``vt100.py [OPTIONS] [-f FORMAT] [-g WxH] (filename|-)``
DESCRIPTION
===========
This module implements a VT100-style (ANSI) terminal emulator for the purpose
of parsing the output of script(1) file and printing to a human-readable
format. The intent is to mimic the exact output of xterm(1), as though you
cut and pasted the output from the terminal.
This program can be used to parse any file containing ANSI (ECMA-48) terminal
codes. Usually the input is a typescript file as output from script(1), which
is usually not human-readable. Another potential use of this program to to
parse the output of a program that produces color codes (ESC [ # m) and
produce color HTML.
Output Formats
--------------
A number of output formats are available. Currently, that number is two.
text
The output is a pure ASCII file with unix line endings. All character
attributes are ignored (even 'hidden').
html
The output is a snippet of HTML with one ``pre`` element. Character
attributes, including xterm 256 colors, are supported.
Unimplemented Features
----------------------
This module is designed to mimic the output (and only output) of xterm.
Therefore, there are no plans to implement any sequence that affects input,
causes the terminal to respond, or that xterm does not itself implement.
OPTIONS
=======
-h, --help print help message and exit
--man print manual page and exit
--version print version number and exit
-f FORMAT, --format=FORMAT specify output format (see "Output Formats")
-g WxH, --geometry=WxH specify console geometry (see "Configuration")
--non-script do not ignore "Script (started|done) on" lines
--rc=FILE read configuration from FILE (default ~/.vt100rc)
--no-rc suppress reading of configuration file
-q, --quiet decrease debugging verbosity
-v, --verbose increase debugging verbosity
The following only affect HTML output.
--background=COLOR set the default background color
--foreground=COLOR set the default foreground color
--colorscheme=SCHEME use the given color scheme (see "Configuration")
CONFIGURATION
=============
By default, vt100.py reads ~/.vt100rc for the following 'key = value` pairs.
COLOR is any valid HTML color. The order does not matter, except that all the
settings following ``[SECTION]`` belong to a specific section.
background = COLOR
Default background color.
color0 = COLOR ...through... color255 = COLOR
Color for the 8 ANSI colors (0-7), 8 bright ANSI colors (8-15), and xterm
extended colors (16-255).
colorscheme = SECTION
Import settings from [SECTION] before any in the current section.
format = {text, html}
Default output format. Default is 'text'.
foreground = COLOR
Default foreground color.
geometry = {WxH, detect}
Use W columns and H rows in output. If the value 'detect' is given, the
current terminal's geometry is detected using ``stty size``.
Default is '80x24'.
inverse_bg = COLOR
Background color to use for the "inverse" attribute when neither the
character's foreground color attribute nor the ``foreground`` option is
set. Default is 'black'.
inverse_fg = COLOR
Foreground color to use for the "inverse" attribute when neither the
character's background color attribute nor the ``background`` option is
set. Default is 'white'.
verbosity = INT
Act as those ``-v`` or ``-q`` was given abs(INT) times, if INT positive or
negative, respectively. Default is '0'.
[SECTION]
Start a definition of a color scheme named SECTION.
REQUIREMENTS
============
* Python 2.6+ or 3.0+ (tested on 2.6, 2.7, 3.1, and 3.2)
TODO
====
See TODO for things that are not yet implemented. There are many.
NOTES
=====
For testing how a terminal implements a feature, the included *rawcat* program
may be helpful. It acts like cat(1), except that it outputs the file
literally; it does not perform LF to CRLF translation. Alternatively, one may
replace the LF (0x0a) character with VT (0x0b) or FF (0x0c), which are treated
identically but are not subject to newline translation.
A neat feature of *rawcat* is the ``-w`` option, which causes it to pause
after each output byte so you can observe xterm draw the screen.
SEE ALSO
========
script(1), scriptreplay(1)
AUTHOR
======
Mark Lodato <lodatom@gmail.com>
THANKS
======
Thanks to http://vt100.net for lots of helpful information, especially the
DEC-compatible parser page.
"""
# Requires Python 2.6
from __future__ import print_function
__version__ = "0.4-git"
__author__ = "Mark Lodato"
__license__ = """
Copyright (c) 2010 Mark Lodato
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
import collections
import itertools
import os.path
import re
import subprocess
import sys
from optparse import OptionParser, OptionGroup
try:
from ConfigParser import SafeConfigParser as ConfigParser
except ImportError:
from configparser import ConfigParser
try:
from io import StringIO
except ImportError:
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
if sys.version_info[0] == 2:
__metaclass__ = type
map = itertools.imap
range = xrange
class TextFormatter:
"""Terminal formatter for plain text output."""
def __init__(self, config=None, eol='\n'):
self.eol = eol
self.init()
if config is not None:
self.parse_config(config)
def init(self):
"""Initialize any default instance variables."""
pass
def parse_config(self, config):
"""Parse a SafeConfigParser object."""
pass
def format(self, lines):
"""Return a stringification of the given lines."""
out = []
out.extend(self.begin())
out.extend(self.format_line(line) for line in lines)
out.extend(self.end())
out.append('')
return self.eol.join(out)
def begin(self):
"""Return a list of lines to be output before the formatted lines."""
return []
def format_line(self, line):
"""Return the given line (sequence of Characters) formatted as
a string (without an EOL character)."""
return ''.join(x.char for x in line)
def end(self):
"""Return a list of lines to be output after the formatted lines."""
return []
class HtmlFormatter (TextFormatter):
"""Terminal formatter for HTML output."""
attr_map = {
# 'fg_color' and 'bg_color' set by init()
('weight', 'bold') : 'font-weight: bold',
('weight', 'feint') : 'font-weight: lighter',
('underline', 'single') : 'text-decoration: underline',
('underline', 'double') : ('text-decoration: underline; '
'border-bottom: 1px solid'),
('style', 'italic') : 'font-style: italic',
('blink', 'rapid') : 'text-decoration: blink',
('blink', 'slow') : 'text-decoration: blink', # no fast or slow
('hidden', True) : 'visibility: hidden',
('strikeout', True) : 'text-decoration: line-through',
('overline', True) : 'text-decoration: overline',
# TODO frame
}
escapes = {
'&' : '&amp;',
'<' : '&lt;',
'>' : '&gt;',
}
default_options = {
'foreground' : '',
'background' : '',
'inverse_fg' : 'white',
'inverse_bg' : 'black',
}
# [black, red, green, brown/yellow, blue, magenta, cyan, white]
# Colors used by xterm (before patch #192, blues were #0000cd and #0000ff)
color_16 = ['#000000', '#cd0000', '#00cd00', '#cdcd00',
'#0000e8', '#cd00cd', '#00cdcd', '#e5e5e5',
'#7f7f7f', '#ff0000', '#00ff00', '#ffff00',
'#5c5cff', '#ff00ff', '#00ffff', '#ffffff']
def init(self):
self.init_colors()
self.attr_map = self.__class__.attr_map.copy()
self.options = self.__class__.default_options.copy()
for index, value in enumerate(self.color_256):
self.set_color(index, value)
def init_colors(self):
def create_color_table(color_scale, gray_scale):
table = self.color_16[:16]
for r, g, b in itertools.product(color_scale, repeat=3):
table.append('#%02x%02x%02x' % (r,g,b))
for g in gray_scale:
table.append('#%02x%02x%02x' % (g,g,g))
return table
self.color_256 = create_color_table([0, 95, 135, 175, 215, 255],
[i*10 + 8 for i in range(24)])
self.color_88 = create_color_table([0, 139, 205, 255],
[46, 92, 113, 139, 162, 185, 208, 231])
def set_color(self, index, value):
self.attr_map['fg_color', index] = 'color: %s' % value
self.attr_map['bg_color', index] = 'background-color: %s' % value
def parse_config(self, config):
self._parse_config(config, config.initial_section, set())
if self.options['foreground']:
self.options['inverse_bg'] = self.options['foreground']
if self.options['background']:
self.options['inverse_fg'] = self.options['background']
def _parse_config(self, config, section, seen):
if config.has_option(section, 'colorscheme'):
scheme = config.get(section, 'colorscheme')
if scheme not in seen:
if config.has_section(scheme):
seen.add(scheme)
self._parse_config(config, scheme, seen)
else:
print('warning: colorscheme "%s" not found' % scheme,
file=sys.stderr)
else:
print('warning: recursion in color scheme: [%s] -> %s'
% (section, scheme), file=sys.stderr)
for i in range(256):
key = 'color%d'%i
if config.has_option(section, key):
self.set_color(i, config.get(section, key))
for key in self.options:
if config.has_option(section, key):
value = config.get(section, key)
self.options[key] = value
def _compute_style(self, attr):
# TODO implement inverse
out = []
if attr.pop('inverse', None):
fg = attr.pop('fg_color', None)
bg = attr.pop('bg_color', None)
if fg is not None:
attr['bg_color'] = fg
else:
out.append('background-color: %s' % self.options['inverse_bg'])
if bg is not None:
attr['fg_color'] = bg
else:
out.append('color: %s' % self.options['inverse_fg'])
for key in sorted(attr):
value = attr[key]
try:
out.append( self.attr_map[key, value] )
except KeyError:
# TODO verbose option?
print('unknown attribute: %s:%s' % (key, value),
file=sys.stderr)
return '; '.join(out)
def begin(self):
style = []
if self.options['foreground']:
style.append('color: %s' % self.options['foreground'])
if self.options['background']:
style.append('background-color: %s' % self.options['background'])
if style:
attribute = ' style="%s"' % '; '.join(style)
else:
attribute = ''
return ['<pre%s>' % attribute]
def format_line(self, line):
out = []
last_style = ''
for c in line:
style = self._compute_style(c.attr)
if style != last_style:
if last_style:
out.append('</span>')
if style:
out.append('<span style="%s">' % style)
last_style = style
char = self.escapes.get(c.char, c.char)
out.append(char)
if last_style:
out.append('</span>')
return ''.join(out)
def end(self):
return ['</pre>']
formatters = {
'text' : TextFormatter,
'html' : HtmlFormatter,
}
class Character:
"""A single character along with an associated attribute."""
def __init__(self, char, attr = {}):
self.char = char
self.attr = attr
def __repr__(self):
return "<'%s'>" % (str(self.char))
def __str__(self):
return str(self.char)
class InvalidParameterListError (RuntimeError):
pass
def param_list(s, default, zero_is_default=True, min_length=1):
"""Return the list of integer parameters assuming `s` is a standard
control sequence parameter list. Empty elements are set to `default`."""
def f(token):
if not token:
return default
value = int(token)
if zero_is_default and value == 0:
return default
if value < 0:
raise ValueError
return value
if s is None:
l = []
else:
try:
l = [f(token) for token in s.split(';')]
except ValueError:
raise InvalidParameterListError
l += [default] * (min_length - len(l))
return l
def clip(n, start, stop=None):
"""Return n clipped to range(start,stop)."""
if stop is None:
stop = start
start = 0
if n < start:
return start
if n >= stop:
return stop-1
return n
def new_sequence_decorator(dictionary):
def decorator_generator(key):
assert isinstance(key, (str, int))
def decorator(f, key=key):
dictionary[key] = f.__name__
return f
return decorator
return decorator_generator
class NoNeedToImplement (Exception):
"""A function for which there is no need to implement."""
pass
class Screen:
"""A two-dimensional collection of characters."""
def __init__(self, width, height):
self.width = width
self.height= height
self.clear()
def __iter__(self):
return iter(self.rows)
def __setitem__(self, idx, value):
row, col = idx
self.rows[row][col] = value
def clear(self):
"""Set all elements to None."""
self.rows = [[None] * self.width for i in range(self.height)]
def clear_row(self, row, start=0, stop=None):
"""Set to None all elements on row `row` and columns `start` to
`stop`-1, inclusive."""
if start < 0:
start = 0
if stop is None or stop > self.width:
stop = self.width
row = self.rows[row]
for c in range(start, stop):
row[c] = None
def clear_rows(self, start=0, stop=None):
"""Set to None all elements on rows `start` to `stop`-1, inclusive."""
if start < 0:
start = 0
if stop is None or stop > self.height:
stop = self.height
for r in range(start, stop):
self.rows[r] = [None] * self.width
def shift_row(self, row, col, amount=1, fill=None):
"""Move the elements on row `row` at and to the right of column
`col`, to the right by `amount` places (negative means left).
Elements shifted past either end are discarded. New elements are set
to `fill`."""
row = self.rows[row]
if amount > 0:
amount = min(amount, self.width-col)
row[col+amount:] = row[col:-amount]
row[col:col+amount] = [fill] * amount
else:
amount = min(-amount, self.width-col)
row[col:-amount] = row[col+amount:]
row[-amount:] = [fill] * amount
class Terminal:
# ---------- Decorators for Defining Sequences ----------
commands = {}
escape_sequences = {}
control_sequences = {}
ansi_modes = {}
dec_modes = {}
command = new_sequence_decorator(commands)
escape = new_sequence_decorator(escape_sequences)
control = new_sequence_decorator(control_sequences)
ansi_mode = new_sequence_decorator(ansi_modes)
dec_mode = new_sequence_decorator(dec_modes)
# ---------- Constructor ----------
def __init__(self, height=24, width=80, verbosity=False,
formatter=TextFormatter()):
self.verbosity = verbosity
self.width = width
self.height = height
self.formatter = formatter
self.main_screen = Screen(width, height)
self.alt_screen = Screen(width, height)
self.reset()
# ---------- Utilities ----------
def reset(self):
"""Reset to initial state."""
self.state = 'ground'
self.prev_state = None
self.next_state = None
self.history = []
self.main_screen.clear()
self.alt_screen.clear()
self.screen = self.main_screen
self.row = 0
self.col = 0
self.saved_cursor = [self.default_cursor, self.default_cursor]
self.margin_top = 0
self.margin_bottom = self.height - 1
self.previous = '\0'
self.current = '\0'
self.tabstops = [(i%8)==0 for i in range(self.width)]
self.attr = {}
self.insert_mode = False
self.new_line_mode = False
self.autowrap_mode = True
self.reverse_wrap = False
self.clear()
default_cursor = {
'pos' : (0, 0),
'attr' : {},
'autowrap' : True,
'reverse_wrap' : False,
'origin_mode' : False,
# TODO: pending SS2 or SS3
# TODO: selective erase
}
def _pos_get(self):
"""The cursor position as (row, column)."""
return self.row, self.col
def _pos_set(self, value):
self.row, self.col = value
pos = property(_pos_get, _pos_set)
def is_alt_screen(self):
"""Return True if in alternate screen mode; False otherwise."""
return self.screen is self.alt_screen
def clear(self):
"""Reset internal buffers for switching between states."""
self.collected = ''
def clip_column(self):
"""If the cursor is past the end of the line, move it to the last
position in the line."""
if self.col >= self.width:
self.col = self.width - 1
def output(self, c):
"""Print the character at the current position and increment the
cursor to the next position. If the current position is past the end
of the line, starts a new line."""
if self.col >= self.width:
if self.autowrap_mode:
self.NEL()
else:
self.col = self.width - 1
c = Character(c, self.attr.copy())
if self.insert_mode:
self.screen.shift_row(self.row, self.col)
self.screen[self.pos] = c
self.col += 1
def scroll(self, n, top = None, bottom = None, save = None):
"""Scroll the scrolling region n lines upward (data moves up) between
rows top (inclusive, default 0) and bottom (exclusive, default
height). Any data moved off the top of the screen (if top is 0/None
and save is None, or if save is True) is saved to the history.
If in alternate screen buffer, no history is saved."""
# TODO add option to print instead of adding to history
if top is None:
top = self.margin_top
if bottom is None:
bottom = self.margin_bottom + 1
s = self.screen
if self.is_alt_screen():
save = False
span = bottom-top
if n > 0:
# TODO transform history?
if (save is None and top == 0) or save:
self.history.extend( s.rows[top:top+n] )
if n > span:
extra = n - span
self.history.extend( [[None]*self.width]*extra )
if n > span:
n = span
s.rows[top:bottom-n] = s.rows[top+n:bottom]
s.clear_rows(start=bottom-n, stop=bottom)
elif n < 0:
n = -n
if n > span:
n = span
s.rows[top+n:bottom] = s.rows[top:bottom-n]
s.clear_rows(start=top, stop=top+n)
def ignore(self, c):
"""Ignore the character."""
self.debug(1, 'ignoring character: %s' % repr(c))
def collect(self, c):
"""Record the character as an intermediate."""
self.collected += c
def clear_on_enter(self, old_state):
"""Since most enter_* functions just call self.clear(), this is a
common function so that you can set enter_foo = clear_on_enter."""
self.clear()
def debug(self, level, *args, **kwargs):
if self.verbosity >= level:
kwargs.setdefault('file', sys.stderr)
print(*args, **kwargs)
# ---------- Parsing ----------
def parse(self, s):
"""Parse an entire string."""
for c in s:
self.parse_single(c)
def parse_single(self, c):
"""Parse a single character."""
if isinstance(c, int):
c = chr(c)
try:
f = getattr(self, 'parse_%s' % self.state)
except AttributeError:
raise RuntimeError("internal error: unknown state %s" %
repr(self.state))
self.next_state = self.state
f(c)
self.transition()
def transition(self):
if self.next_state != self.state:
f = getattr(self, 'leave_%s' % self.state, None)
if f is not None:
f(self.next_state)
self.next_state, self.state, self.prev_state = (None,
self.next_state, self.state)
if self.state != self.prev_state:
f = getattr(self, 'enter_%s' % self.state, None)
if f is not None:
f(self.prev_state)
def parse_ground(self, c):
self.previous, self.current = self.current, c
if ord(c) < 0x20:
self.execute(c)
else:
self.output(c)
# ---------- Output ----------
def to_string(self, history=True, screen=True, remove_blank_end=True,
formatter=None):
"""Return a string form of the history and the current screen."""
# Concatenate the history and the screen, and fix each line.
lines = []
if history:
lines.extend(map(self.fixup_line, self.history))
if screen:
lines.extend(map(self.fixup_line, self.main_screen))
if not lines:
return
# Remove blank lines from the end of input.
if remove_blank_end:
lines = self.drop_end(None, list(lines))
if formatter is None:
formatter = self.formatter
return formatter.format(lines)
def print_screen(self, formatter=None):
"""Print the state of the current screen to standard output."""
print(self.to_string(False, True, False, formatter), end='')
def fixup_line(self, line):
"""Remove empty characters from the end of the line and change Nones
to spaces with no attributes."""
def convert_to_blank(x):
if x is not None:
return x
else:
return Character(' ')
def is_None(x):
return x is None
return list(map(convert_to_blank, self.drop_end(is_None, line)))
@staticmethod
def drop_end(predicate, sequence):
"""Simliar as itertools.dropwhile, except operating from the end."""
i = 0
if predicate is None:
for x in reversed(sequence):
if x:
break
i += 1
else:
for x in reversed(sequence):
if not predicate(x):
break
i += 1
if i == 0:
return sequence
else:
return sequence[:-i]
# ---------- Single-character commands (C0) ----------
def execute(self, c):
"""Execute a C0 command."""
name = self.commands.get(c, None)
f = None
if name is not None:
f = getattr(self, name, None)
if f is None:
f = self.ignore
r = f(c)
if r is NotImplemented:
self.debug(0, 'command not implemented: %s' % f.__name__)
elif r is NoNeedToImplement:
self.debug(1, 'ignoring command: %s' % f.__name__)
@command('\x00') # ^@
def NUL(self, c=None):
"""NULl"""
pass
@command('\x07') # ^G
def BEL(self, c=None):
"""Bell"""
pass
@command('\x08') # ^H
def BS(self, c=None):
"""Backspace"""
self.clip_column()
if self.col > 0:
self.col -= 1
elif self.reverse_wrap:
self.col = self.width - 1
if self.row > 0:
self.row -= 1
else:
self.row = self.height - 1
@command('\x09') # ^I
def HT(self, c=None):
"""Horizontal Tab"""
while self.col < self.width-1:
self.col += 1
if self.tabstops[self.col]:
break
@command('\x0a') # ^J
def LF(self, c=None):
"""Line Feed"""
if self.new_line_mode:
self.NEL()
else:
self.IND()
@command('\x0b') # ^K
def VT(self, c=None):
"""Vertical Tab"""
self.LF(c)
@command('\x0c') # ^L
def FF(self, c=None):
"""Form Feed"""
self.LF(c)
@command('\x0d') # ^M
def CR(self, c=None):
"""Carriage Return"""
self.col = 0
@command('\x18') # ^X
def CAN(self, c=None):
"""Cancel"""
self.next_state = 'ground'
@command('\x1a') # ^Z
def SUB(self, c=None):
"""Substitute"""
self.next_state = 'ground'
@command('\x1b') # ^[
def ESC(self, c=None):
"""Escape"""
self.next_state = 'escape'
# ---------- Escape Sequences ----------
enter_escape = clear_on_enter
def parse_escape(self, c):
if ord(c) < 0x20:
self.execute(c)
elif ord(c) < 0x30:
self.collect(c)
elif ord(c) < 0x7f:
self.next_state = 'ground'
self.dispatch_escape(c)
else:
self.ignore(c)
def dispatch_escape(self, c):
command = self.collected + c
name = self.escape_sequences.get(c, None)
f = None
if name is not None:
f = getattr(self, name, None)
if f is None:
f = self.ignore
r = f(command)
if r is NotImplemented:
self.debug(0, 'escape not implemented: %s' % f.__name__)
elif r is NoNeedToImplement:
self.debug(1, 'ignoring escape: %s' % f.__name__)
@escape('7')
def DECSC(self, c=None):
"""Save Cursor"""
self.saved_cursor[int(self.is_alt_screen())] = {
'pos' : self.pos,
'attr' : self.attr.copy(),
'autowrap' : self.DECAWM(None),
'reverse_wrap' : self.reverse_wraparound_mode(None),
'origin_mode' : self.DECOM(None),
}
@escape('8')
def DECRC(self, c=None):
"""Restore Cursor"""
cursor = self.saved_cursor[int(self.is_alt_screen())]
self.pos = cursor['pos']
self.attr = cursor['attr'].copy()
self.DECAWM(cursor['autowrap'])
self.reverse_wraparound_mode(cursor['reverse_wrap'])
self.DECOM(cursor['origin_mode'])
self.clip_column()
@escape('D')
def IND(self, c=None):
"""Index"""
self.clip_column()
if self.row == self.margin_bottom:
self.scroll(1)
elif self.row < self.height - 1:
self.row += 1
@escape('E')
def NEL(self, c=None):
"""Next Line"""
self.IND()
self.col = 0
@escape('H')
def HTS(self, c=None):
"""Horizontal Tab Set"""
if self.col < self.width:
self.tabstops[self.col] = True
@escape('M')
def RI(self, c=None):
"""Reverse Index (reverse line feed)"""
self.clip_column()
if self.row == self.margin_top:
self.scroll(-1)
elif self.row > 0:
self.row -= 1
@escape('P')
def DCS(self, c=None):
"""Device Control String"""
self.next_state = 'dcs'
@escape('X')
def SOS(self, c=None):
"""Start of String"""
self.next_state = 'sos'
@escape('[')
def CSI(self, c=None):
"""Control Sequence Introducer"""
self.next_state = 'control_sequence'
@escape('\\')
def ST(self, c=None):
"""String Terminator"""
pass
@escape(']')
def OSC(self, c=None):
"""Operating System Command"""
self.next_state = 'osc'
@escape('^')
def PM(self, c=None):
"""Privacy Message"""
self.next_state = 'pm'
@escape('_')
def APC(self, c=None):
"""Application Program Command"""
self.next_state = 'apc'
@escape('c')
def RIS(self, command=None, param=None):
"""Reset to Initial State"""
self.reset()
# ---------- Control Sequences ----------
enter_control_sequence = clear_on_enter
def parse_control_sequence(self, c):
if ord(c) < 0x20:
self.execute(c)
elif ord(c) < 0x40:
self.collect(c)
elif ord(c) < 0x7f:
self.next_state = 'ground'
self.dispatch_control_sequence(c)
else:
self.ignore(c)
def dispatch_control_sequence(self, c):
self.collect(c)
m = re.match('^([\x30-\x3f]*)([\x20-\x2f]*[\x40-\x7f])$',
self.collected)
if not m:
return self.invalid_control_sequence()
param, command = m.groups()
if param and param[0] in '<=>?':
command = param[0] + command
param = param[1:]
name = self.control_sequences.get(command, None)
f = None
if name is not None:
f = getattr(self, name, None)
if f is None:
f = self.ignore_control_sequence
try:
r = f(command, param)
if r is NotImplemented:
self.debug(0, 'control sequence not implemented: %s'
% f.__name__)
elif r is NoNeedToImplement:
self.debug(1, 'ignoring control sequence: %s'
% f.__name__)
except InvalidParameterListError:
self.invalid_control_sequence()
def invalid_control_sequence(self):
"""Called when the control sequence is invalid."""
self.debug(0, 'invalid control sequence: %s'
% (repr(self.collected)))
def ignore_control_sequence(self, command, param):
"""Called when the control sequence is ignored."""
self.debug(1, 'ignoring control sequence: %s, %s'
% (repr(command), repr(param)))
@control('@')
def ICH(self, command=None, param=None):
"""Insert (blank) Characters"""
n = param_list(param, 1)[0]
self.clip_column()
r = self.row
c = self.col
self.screen.shift_row(r, c, amount=n, fill=Character(' '))
@control('A')
def CUU(self, command=None, param=None):
"""Cursor Up"""
n = param_list(param, 1)[0]
self.clip_column()
if self.row >= self.margin_top:
self.row = clip(self.row-n, self.margin_top, self.margin_bottom+1)
else:
self.row = clip(self.row-n, self.height)
@control('B')
def CUD(self, command=None, param=None):
"""Cursor Down"""
n = param_list(param, 1)[0]
self.clip_column()
if self.row <= self.margin_bottom:
self.row = clip(self.row+n, self.margin_top, self.margin_bottom+1)
else:
self.row = clip(self.row+n, self.height)
@control('C')
def CUF(self, command=None, param=None):
"""Cursor Forward"""
n = param_list(param, 1)[0]
self.col = clip(self.col+n, self.width)
@control('D')
def CUB(self, command=None, param=None):
"""Cursor Backward"""
n = param_list(param, 1)[0]
self.clip_column()
self.col = clip(self.col-n, self.width)
@control('E')
def CNL(self, command=None, param=None):
"""Cursor Next Line"""
self.CUD(command, param)
self.col = 0
@control('F')
def CPL(self, command=None, param=None):
"""Cursor Previous Line"""
self.CUU(command, param)
self.col = 0
@control('G')
def CHA(self, command=None, param=None):
"""Character Position Absolute"""
n = param_list(param, 1)[0]
self.col = clip(n-1, self.width)
@control('H')
def CUP(self, command=None, param=None):
"""Cursor Position [row;column]"""
n,m = param_list(param, 1, min_length=2)[:2]
self.row = clip(n-1, self.height)
self.col = clip(m-1, self.width)
@control('I')
def CHT(self, command=None, param=None):
"""Cursor Forward Tabulation"""
n = param_list(param, 1)[0]
for i in range(n):
self.HT()
@control('J')
def ED(self, command=None, param=None):
"""Erase in Display
Ps = 0 -> Erase Below (default)
Ps = 1 -> Erase Above
Ps = 2 -> Erase All
Ps = 3 -> Erase Saved Lines (xterm)
"""
n = param_list(param, 0)[0]
if n == 0:
self.screen.clear_row(self.row, start=self.col)
self.screen.clear_rows(start=self.row+1)
elif n == 1:
self.screen.clear_rows(stop=self.row)
self.screen.clear_row(self.row, stop=self.col+1)
elif n == 2:
self.screen.clear()
elif n == 3:
# Note: xterm's interpetation of this is a little funky. It does
# not erase the entire history, but saves a number of lines
# dependent, in an odd way, on the number of rows in the window.
# I see no point in emulating this behavior.
self.history[:] = []
@control('?J')
def DECSED(self, command=None, param=None):
"""Selective Erase in Display
Ps = 0 -> Selective Erase Below (default)
Ps = 1 -> Selective Erase Above
Ps = 2 -> Selective Erase All
"""
return NotImplemented
@control('K')
def EL(self, command=None, param=None):
"""Erase in Line
Ps = 0 -> Erase to Right (default)
Ps = 1 -> Erase to Left
Ps = 2 -> Erase All
"""
n = param_list(param, 0)[0]
self.clip_column()
if n == 0:
self.screen.clear_row(self.row, start=self.col)
elif n == 1:
self.screen.clear_row(self.row, stop=self.col+1)
elif n == 2:
self.screen.clear_row(self.row)
@control('?J')
def DECSEL(self, command=None, param=None):
"""Selective Erase in Line
Ps = 0 -> Selective Erase to Right (default)
Ps = 1 -> Selective Erase to Left
Ps = 2 -> Selective Erase All
"""
return NotImplemented
@control('L')
def IL(self, command=None, param=None):
"""Insert Line(s)"""
n = param_list(param, 1)[0]
self.clip_column()
if self.margin_top <= self.row <= self.margin_bottom:
self.scroll(-n, top=self.row, save=False)
@control('M')
def DL(self, command=None, param=None):
"""Delete Line(s)"""
n = param_list(param, 1)[0]
self.clip_column()
if self.margin_top <= self.row <= self.margin_bottom:
self.scroll(n, top=self.row, save=False)
@control('P')
def DCH(self, command=None, param=None):
"""Delete Character(s)"""
n = param_list(param, 1)[0]
r = self.row
c = self.col
self.screen.shift_row(r, c, amount=-n, fill=None)
@control('S')
def SU(self, command=None, param=None):
"""Scroll Up"""
n = param_list(param, 1)[0]
self.scroll(n)
@control('T')
def SD(self, command=None, param=None):
"""Scroll Down / Mouse Tracking"""
# TODO mouse tracking
n = param_list(param, 1)[0]
self.scroll(-n)
@control('X')
def ECH(self, command=None, param=None):
"""Erase Character"""
n = param_list(param, 1)[0]
self.screen.clear_row(self.row, start=self.col, stop=self.col+n)
@control('Z')
def CBT(self, command=None, param=None):
"""Cursor Backward Tabulation"""
n = param_list(param, 1)[0]
for i in range(n):
while self.col > 0:
self.col -= 1
if self.tabstops[self.col]:
break
@control('`')
def HPA(self, command=None, param=None):
"""Character Position Absolute"""
self.CHA(command, param)
@control('a')
def HPR(self, command=None, param=None):
"""Character Position Forward (Horizontal Position Right)"""
self.CUF(command, param)
@control('b')
def REP(self, command=None, param=None):
"""Repeat"""
n = param_list(param, 1)[0]
if ord(self.previous) >= 0x20:
for i in range(n):
self.output(self.previous)
@control('d')
def VPA(self, command=None, param=None):
"""Line Position Absolute"""
n = param_list(param, 1)[0]
self.row = clip(n-1, self.height)
@control('e')
def VPR(self, command=None, param=None):
"""Line Position Forward"""
self.CUD(command, param)
@control('f')
def HVP(self, command=None, param=None):
"""Horizontal and Vertical Position"""
self.CUP(command, param)
@control('g')
def TBC(self, command=None, param=None):
"""Tab Clear"""
n = param_list(param, 0)[0]
if n == 0:
if self.col < self.width:
self.tabstops[self.col] = False
elif n == 3:
self.tabstops[:] = [False] * self.width
@control('h')
def SM(self, command=None, param=None):
"""Set Mode"""
return self.dispatch_modes('ANSI', param, True)
@control('?h')
def DECSM(self, command=None, param=None):
"""Set DEC Private Mode"""
return self.dispatch_modes('DEC', param, True)
@control('j')
def HPB(self, command=None, param=None):
"""Character Position Backward"""
self.CUB(command, param)
@control('k')
def VPB(self, command=None, param=None):
"""Line Position Backward"""
self.CUU(command, param)
@control('l')
def RM(self, command=None, param=None):
"""Reset Mode"""
return self.dispatch_modes('ANSI', param, False)
@control('?l')
def DECRM(self, command=None, param=None):
"""Reset DEC Private Mode"""
return self.dispatch_modes('DEC', param, False)
@control('m')
def SGR(self, command=None, param=None):
"""Set Graphics Attributes"""
l = param_list(param, 0)
l_iter = iter(l)
for n in l_iter:
if n == 0:
self.attr.clear()
elif 30 <= n <= 38 or 40 <= n <= 48:
if n in (38, 48):
try:
m = next(l_iter)
o = next(l_iter)
except StopIteration:
break
if m != 5:
# xterm stops parsing if this happens
self.debug(0, 'invalid 256-color attribute: %s %s %s' %
(m,n,o))
break
value = o
else:
value = n % 10
key = 'fg_color' if n < 40 else 'bg_color'
self.attr[key] = value
else:
try:
key, value = self.sgr_table[n]
except KeyError:
self.debug(0, 'unknown attribute: %s' % n)
pass
else:
if value is None:
self.attr.pop(key, None)
else:
self.attr[key] = value
sgr_table = {
# 0 clear all attributes
1 : ('weight', 'bold'),
2 : ('weight', 'faint'),
3 : ('style', 'italic'),
4 : ('underline', 'single'),
5 : ('blink', 'slow'),
6 : ('blink', 'rapid'),
7 : ('inverse', True),
8 : ('hidden', True),
9 : ('strikeout', True),
# 10-19 font stuff
20 : ('style', 'fraktur'),
21 : ('underline', 'double'),
22 : ('weight', None),
23 : ('style', None),
24 : ('underline', None),
25 : ('blink', None),
# 26 reserved
27 : ('inverse', None),
28 : ('hidden', None),
29 : ('strikeout', None),
# 30-37 foreground color
# 38 foreground color (88- or 256-color extension)
39 : ('fg_color', None),
# 30-37 background color
# 38 background color (88- or 256-color extension)
49 : ('bg_color', None),
# 50 reserved
51 : ('frame', 'box'),
52 : ('frame', 'circle'),
53 : ('overline', True),
54 : ('frame', None),
55 : ('overline', None),
# 56-59 reserved
# 60-65 ideogram stuff
# 90-107 xterm 16-color support enabled (light colors)
# 100 xterm 16-color support disabled
}
@control('!p')
def DECSTR(self, command=None, param=None):
"""Soft Terminal Reset"""
self.DECTCEM(True)
self.IRM(True)
self.DECOM(False)
self.DECAWM(True)
self.reverse_wraparound_mode(False)
self.KAM(False)
self.DECCKM(False)
self.DECNKM(False)
self.DECSTBM()
# TODO Set all character sets to ASCII
self.SGR()
self.DECSCA(False)
self.saved_cursor = [self.default_cursor, self.default_cursor]
@control('r')
def DECSTBM(self, command=None, param=None):
"""Set Top and Bottom Margins (Scrolling Region)"""
self.pos = (0,0)
top, bottom = param_list(param, None, min_length=2)[:2]
if top is None:
top = 1
if bottom is None or bottom > self.height:
bottom = self.height
if bottom > top:
self.margin_top = top - 1
self.margin_bottom = bottom - 1
@control('?r')
def restore_dec_private_mode(self, command=None, param=None):
"""Restore DEC Private Mode Values"""
return NotImplemented
@control('$r')
def DECCARA(self, command=None, param=None):
"""Change Attributes in Rectangular Area"""
return NotImplemented
@control('s')
def save_cursor(self, command=None, param=None):
"""Save cursor"""
self.DECSC()
@control('?s')
def save_dec_private_mode(self, command=None, param=None):
"""Save DEC Private Mode Values"""
return NotImplemented
@control('$t')
def DECRARA(self, command=None, param=None):
"""Reverse Attributes in Rectangular Area"""
return NotImplemented
@control('u')
def restore_cursor(self, command=None, param=None):
"""Restore cursor"""
self.DECRC()
# ---------- Control Strings ----------
enter_osc = clear_on_enter
enter_dcs = clear_on_enter
enter_sos = clear_on_enter
enter_apc = clear_on_enter
enter_pm = clear_on_enter
# TODO OSC to set text parameters
def parse_osc(self, c): self.parse_control_string(c)
def parse_dcs(self, c): self.parse_control_string(c)
def parse_sos(self, c): self.parse_control_string(c)
def parse_pm (self, c): self.parse_control_string(c)
def parse_apc(self, c): self.parse_control_string(c)
finish_osc = None
finish_dcs = None
finish_sos = None
finish_apc = None
finish_pm = None
def parse_control_string(self, c):
# Consume the whole string and pass it to the process function.
if c in '\x18\x1a':
# CAN and SUB quit the string
self.cancel_control_string()
# should we self.execute(c) ?
elif c == '\x07' and self.state == 'osc':
# NOTE: xterm ends OSC with BEL, in addition to ESC \
self.finish_control_string()
elif self.collected and self.collected[-1] == '\x1b':
# NOTE: xterm consumes the character after the ESC always, but
# only process it if it is '\'. Not sure about VTxxx.
self.collected = self.collected[:-1]
if c == '\x5c':
self.finish_control_string()
else:
self.cancel_control_string()
else:
self.collect(c)
def finish_control_string(self):
name = 'finish_%s' % self.state
f = getattr(self, name, None)
if f is None:
f = self.ignore_control_string
f(self.collected)
self.next_state = 'ground'
def cancel_control_string(self):
self.next_state = 'ground'
def ignore_control_string(self, *args):
"""Called when a control string is ignored."""
self.debug(1, 'ignoring %s control string: %s' % (self.state,
repr(args)))
# ---------- Modes ----------
def dispatch_modes(self, mode_type, param, value):
if not param:
return
if mode_type == 'DEC':
modes = self.dec_modes
elif mode_type == 'ANSI':
modes = self.ansi_modes
else:
self.debug(0, 'unknown mode type: %s' % mode_type)
return
for n in param_list(param, 0):
name = modes.get(n, None)
f = None
if name is not None:
f = getattr(self, name, None)
if f is None:
self.debug(0, 'unrecognized %s mode: %s' % (mode_type, n))
else:
r = f(value)
if r is NotImplemented:
self.debug(0, 'mode not implemented: %s' % f.__name__)
if r is NoNeedToImplement:
self.debug(1, 'ignoring mode: %s' % f.__name__)
@ansi_mode(4)
def IRM(self, value):
"""Insertion Replacement Mode"""
if value is None:
return self.insert_mode
else:
self.insert_mode = value
@ansi_mode(20)
def LNM(self, value):
"""Line Feed/New Line Mode"""
if value is None:
return self.new_line_mode
else:
self.new_line_mode = value
@dec_mode(3)
def DECCOLM(self, value):
"""Column Mode"""
return NotImplemented
@dec_mode(5)
def DECSCNM(self, value):
"""Screen Mode"""
return NotImplemented
@dec_mode(6)
def DECOM(self, value):
"""Origin Mode"""
return NotImplemented
@dec_mode(7)
def DECAWM(self, value):
"""Auto Wrap Mode"""
if value is None:
return self.autowrap_mode
else:
self.autowrap_mode = value
@dec_mode(45)
def reverse_wraparound_mode(self, value):
"""Reverse-wraparound mode"""
if value is None:
return self.reverse_wrap
else:
self.reverse_wrap = value
@dec_mode(47)
@dec_mode(1047)
def alternate_screen_buffer_mode(self, value):
"""Alternate Screen Buffer"""
if value is None:
return self.screen is self.alt_screen
if value:
self.screen = self.alt_screen
else:
self.screen = self.main_screen
@dec_mode(1048)
def save_cursor_mode(self, value):
"""Save cursor"""
if value is None:
return None
if value:
return self.DECSC()
else:
return self.DECRC()
@dec_mode(1049)
def alternate_screen_buffer_clearing_mode(self, value):
"""Save cursor, switch to alternate screen buffer, and clear the
screen."""
if value is None:
return None
if value:
self.DECSC()
self.alternate_screen_buffer_mode(True)
self.ED(self, param='2')
else:
self.alternate_screen_buffer_mode(False)
self.DECRC()
# ================================================================
# Things implemented by xterm but not here.
# ================================================================
@command('\x05') # ^E
def ENQ(self, c=None):
"""Enquiry"""
return NoNeedToImplement
@command('\x0e') # ^N
def SO(self, c=None):
"""Shift Out (LS1)"""
return NotImplemented
@command('\x0f') # ^O
def SI(self, c=None):
"""Shift In (LS0)"""
return NotImplemented
# --------------------
@escape('=')
def DECPAM(self, command=None, param=None):
"""Application Keypad"""
return NoNeedToImplement
@escape('>')
def DECPNM(self, command=None, param=None):
"""Normal Keypad"""
return NoNeedToImplement
@escape('N')
def SS2(self, c=None):
"""Single Shift 2"""
return NotImplemented
@escape('O')
def SS3(self, c=None):
"""Single Shift 3"""
return NotImplemented
@escape(' F')
def S7C1T(self, c=None):
"""7-bit controls"""
return NotImplemented
@escape(' G')
def S8C1T(self, c=None):
"""8-bit controls"""
return NotImplemented
@escape(' L')
def set_ansi_level_1(self, c=None):
"""Set ANSI conformance level 1"""
return NotImplemented
@escape(' M')
def set_ansi_level_2(self, c=None):
"""Set ANSI conformance level 2"""
return NotImplemented
@escape(' N')
def set_ansi_level_3(self, c=None):
"""Set ANSI conformance level 3"""
return NotImplemented
# ESC # 3 DEC double-height line, top half (DECDHL)
# ESC # 4 DEC double-height line, bottom half (DECDHL)
# ESC # 5 DEC single-width line (DECSWL)
# ESC # 6 DEC double-width line (DECDWL)
# ESC # 8 DEC Screen Alignment Test (DECALN)
# ESC % @ Select default character set, ISO 8859-1 (ISO 2022)
# ESC % G Select UTF-8 character set (ISO 2022)
# ESC ( C Designate G0 Character Set (ISO 2022)
# ESC ) C Designate G1 Character Set (ISO 2022)
# ESC * C Designate G2 Character Set (ISO 2022)
# ESC + C Designate G3 Character Set (ISO 2022)
# ESC - C Designate G1 Character Set (VT300)
# ESC . C Designate G2 Character Set (VT300)
# ESC / C Designate G3 Character Set (VT300)
# ESC F Cursor to lower left corner of screen (if enabled by the
# hpLowerleftBugCompat resource).
# ESC l Memory Lock (per HP terminals). Locks memory above the cur-
# sor.
# ESC m Memory Unlock (per HP terminals)
# ESC n Invoke the G2 Character Set as GL (LS2).
# ESC o Invoke the G3 Character Set as GL (LS3).
# ESC | Invoke the G3 Character Set as GR (LS3R).
# ESC } Invoke the G2 Character Set as GR (LS2R).
# ESC ~ Invoke the G1 Character Set as GR (LS1R).
# --------------------
@control('>T')
def xterm_title_mode_reset(self, command=None, param=None):
"""Xterm reset title mode features"""
return NotImplemented
@control('>c') # Secondary DA
@control('c') # Primary DA
def DA(self, command=None, param=None):
"""Send Device Attributes"""
return NoNeedToImplement
@control('?i')
@control('i')
def MC(self, command=None, param=None):
"""Media Copy"""
return NotImplemented
@control('>m')
def xterm_resource_value_modifiers(self, command=None, param=None):
return NoNeedToImplement
@control('>n')
def xterm_disable_modifiers(self, command=None, param=None):
return NoNeedToImplement
@control('?n')
@control('n')
def DSR(self, command=None, param=None):
"""Device Status Report"""
return NoNeedToImplement
@control('>p')
def xterm_pointer_mode(self, command=None, param=None):
"""Set resource value pointerMode"""
return NoNeedToImplement
@control('"p')
def DECSCL(self, command=None, param=None):
"""Set Conformance Level"""
return NotImplemented
@control(' q')
def DECSCUSR(self, command=None, param=None):
"""Set cursor style"""
return NoNeedToImplement
@control('"q')
def DECSCA(self, command=None, param=None):
"""Set Character protection Attribute"""
return NotImplemented
@control('t')
def window_manipulation(self, command=None, param=None):
"""Window manipulation"""
return NoNeedToImplement
@control('>t')
def xterm_title_mode_feature(self, command=None, param=None):
"""Set features of the title modes"""
return NoNeedToImplement
@control(' t')
def DECSWBV(self, command=None, param=None):
"""Set Warning-Bell Volume"""
return NoNeedToImplement
@control(' u')
def DECSMBV(self, command=None, param=None):
"""Set Margin-Bell Volume"""
return NoNeedToImplement
# --------------------
@ansi_mode(2)
def KAM(self, value):
"""Keyboard Action Mode"""
return NoNeedToImplement
@ansi_mode(12)
def SRM(self, value):
"""Send/Receive Mode"""
return NotImplemented
@dec_mode(1)
def DECCKM(self, value):
"""Cursor Key Mode"""
return NoNeedToImplement
@dec_mode(2)
def DECANM(self, value):
"""ANSI/VT52 Mode"""
return NotImplemented
@dec_mode(4)
def DECSCLM(self, value):
"""(Smooth) Scrolling Mode"""
return NoNeedToImplement
@dec_mode(8)
def DECARM(self, value):
"""Auto Repeat"""
return NoNeedToImplement
@dec_mode(9)
def send_mouse_xy_on_press(self, value):
"""Send Mouse X & Y on button press."""
return NoNeedToImplement
@dec_mode(10)
def show_toolbar(self, value):
"""Show toolbar"""
return NoNeedToImplement
@dec_mode(12)
def blinking_cursor(self, value):
"""Blinking Cursor"""
return NoNeedToImplement
@dec_mode(18)
def DECPFF(self, value):
"""Print Form Feed Mode"""
return NotImplemented
@dec_mode(19)
def DECPEX(self, value):
"""Print Extent Mode"""
return NotImplemented
@dec_mode(25)
def DECTCEM(self, value):
"""Text Cursor Enable Mode"""
return NoNeedToImplement
@dec_mode(30)
def show_scrollbar(self, value):
"""Show scrollbar"""
return NoNeedToImplement
@dec_mode(35)
def font_shifting_mode(self, value):
"""Enable font-shifting functions"""
return NotImplemented
@dec_mode(38)
def DECTEK(self, value):
"""Tektronix Mode"""
return NotImplemented
@dec_mode(40)
def allow_80_to_132_mode(self, value):
"""Allow 80 -> 132 Mode"""
return NotImplemented
@dec_mode(41)
def more_fix(self, value):
"""more(1) fix"""
return NotImplemented
@dec_mode(42)
def DECNRCM(self, value):
"""Character Set Mode (National Replacement Character Sets)"""
return NotImplemented
@dec_mode(44)
def margin_bell(self, value):
"""Margin bell"""
return NoNeedToImplement
@dec_mode(46)
def logging_mode(self, value):
"""Logging mode"""
return NotImplemented
@dec_mode(66)
def DECNKM(self, value):
"""Numeric Keypad Mode"""
return NoNeedToImplement
@dec_mode(67)
def DECBKM(self, value):
"""Backarrow Key Mode"""
return NoNeedToImplement
@dec_mode(1000)
def send_mouse_xy_on_press_and_release(self, value):
"""Send Mouse X & Y on button press and release."""
return NoNeedToImplement
@dec_mode(1001)
def hilite_mouse_tracking(self, value):
"""Hilite Mouse Tracking"""
return NoNeedToImplement
@dec_mode(1002)
def cell_motion_mouse_tracking(self, value):
"""Cell Motion Mouse Tracking"""
return NoNeedToImplement
@dec_mode(1003)
def all_motion_mouse_tracking(self, value):
"""All Motion Mouse Tracking"""
return NoNeedToImplement
@dec_mode(1004)
def send_focus_events(self, value):
"""Send FocusIn/FocusOut Events"""
return NoNeedToImplement
@dec_mode(1034)
def eight_bit_input(self, value):
"""Interpret "meta" key, sets eighth bit."""
return NoNeedToImplement
@dec_mode(1035)
def num_lock_modifier(self, value):
"""Enable special modifiers for Alt and Num-Lock keys."""
return NoNeedToImplement
@dec_mode(1036)
def meta_sends_escape(self, value):
"""Send ESC when Meta modifies a key."""
return NoNeedToImplement
@dec_mode(1037)
def send_del_for_delete(self, value):
"""Send DEL from the editing-keypad Delete key."""
return NoNeedToImplement
@dec_mode(1039)
def alt_sends_escape(self, value):
"""Send ESC when Alt modifies a key."""
return NoNeedToImplement
@dec_mode(1040)
def keep_selection(self, value):
"""Keep selection even if not highlighted."""
return NoNeedToImplement
@dec_mode(1041)
def select_to_clipboard(self, value):
"""Use the Clipboard selection."""
return NoNeedToImplement
@dec_mode(1042)
def bell_is_urgent(self, value):
"""Enable Urgency window manager hint when Control-G is received."""
return NoNeedToImplement
@dec_mode(1043)
def pop_on_bell(self, value):
"""Enable raising of the window when Control-G is received."""
return NoNeedToImplement
@dec_mode(1050)
def terminfo_function_key_mode(self, value):
"""Set terminfo/termcap function-key mode."""
return NoNeedToImplement
@dec_mode(1051)
def sun_function_key_mode(self, value):
"""Set Sun function-key mode."""
return NoNeedToImplement
@dec_mode(1052)
def hp_function_key_mode(self, value):
"""Set HP function-key mode."""
return NoNeedToImplement
@dec_mode(1053)
def sco_function_key_mode(self, value):
"""Set SCO function-key mode."""
return NoNeedToImplement
@dec_mode(1060)
def legacy_keyboard_emulation(self, value):
"""Set legacy keyboard emulation (X11R6)."""
return NoNeedToImplement
@dec_mode(1061)
def vt220_keyboard_emulation(self, value):
"""Set VT220 keyboard emulation."""
return NoNeedToImplement
@dec_mode(2004)
def bracketed_paste(self, value):
"""Set bracketed paste mode."""
return NoNeedToImplement
# ================================================================
# Things not implemented by xterm.
# ================================================================
@command('\x01') # ^A
def SOH(self, c=None):
"""Start Of Heading"""
return NotImplemented
@command('\x02') # ^B
def STX(self, c=None):
"""Start of TeXt"""
return NotImplemented
@command('\x03') # ^C
def ETX(self, c=None):
"""End of TeXt"""
return NotImplemented
@command('\x04') # ^D
def EOT(self, c=None):
"""End Of Transmission"""
return NotImplemented
@command('\x06') # ^F
def ACK(self, c=None):
"""ACKnowledge"""
return NotImplemented
@command('\x10') # ^P
def DLE(self, c=None):
"""Data Link Escape"""
return NotImplemented
@command('\x11') # ^Q
def DC1(self, c=None):
"""Device Control 1"""
return NotImplemented
@command('\x12') # ^R
def DC2(self, c=None):
"""Device Control 2"""
return NotImplemented
@command('\x13') # ^S
def DC3(self, c=None):
"""Device Control 3"""
return NotImplemented
@command('\x14') # ^T
def DC4(self, c=None):
"""Device Control 4"""
return NotImplemented
@command('\x15') # ^U
def NAK(self, c=None):
"""Negative AcKnowledge"""
return NotImplemented
@command('\x16') # ^V
def SYN(self, c=None):
"""SYNchronous idle"""
return NotImplemented
@command('\x17') # ^W
def ETB(self, c=None):
"""End of Transmission Block"""
return NotImplemented
@command('\x19') # ^Y
def EM(self, c=None):
"""End of Medium"""
return NotImplemented
@command('\x1c') # ^\
def FS(self, c=None):
"""File Separator (IS4)"""
return NotImplemented
@command('\x1d') # ^]
def GS(self, c=None):
"""Group Separator (IS3)"""
return NotImplemented
@command('\x1e') # ^^
def RS(self, c=None):
"""Record Separator (IS2)"""
return NotImplemented
@command('\x1f') # ^_
def US(self, c=None):
"""Unit Separator (IS1)"""
return NotImplemented
# --------------------
# no @escape('0')
# no @escape('1')
# no @escape('2')
# no @escape('3')
# no @escape('4')
# no @escape('5')
# no @escape('6')
# no @escape('9')
# no @escape(':')
# no @escape(';')
# no @escape('<')
# no @escape('?')
# no @escape('@')
# no @escape('A')
@escape('B')
def BPH(self, command=None, param=None):
"""Break Permitted Here"""
return NotImplemented
@escape('C')
def NBH(self, command=None, param=None):
"""No Break Here"""
return NotImplemented
@escape('F')
def SSA(self, command=None, param=None):
"""Start of Selected Area"""
return NotImplemented
@escape('G')
def ESA(self, command=None, param=None):
"""End of Selected Area"""
return NotImplemented
@escape('I')
def HTJ(self, command=None, param=None):
"""Character Tabulation with Justification"""
return NotImplemented
@escape('J')
def VTS(self, command=None, param=None):
"""Veritical Tab Set"""
return NotImplemented
@escape('K')
def PLD(self, command=None, param=None):
"""Partial Line Forward (Down)"""
return NotImplemented
@escape('L')
def PLU(self, command=None, param=None):
"""Partial Line Backward (Up)"""
return NotImplemented
@escape('Q')
def PU1(self, command=None, param=None):
"""Private Use 1"""
return NotImplemented
@escape('R')
def PU2(self, command=None, param=None):
"""Private Use 2"""
return NotImplemented
@escape('S')
def STS(self, command=None, param=None):
"""Set Transmit State"""
return NotImplemented
@escape('T')
def CCH(self, command=None, param=None):
"""Cancel Character"""
return NotImplemented
@escape('U')
def MW(self, command=None, param=None):
"""Message Waiting"""
return NotImplemented
@escape('V')
def SPA(self, c=None):
"""Start of Guarded (Protected) Area"""
return NotImplemented
@escape('W')
def EPA(self, c=None):
"""End of Guarded (Protected) Area"""
return NotImplemented
# no @escape('Y')
@escape('Z')
def SCI(self, c=None):
"""Single Character Introducer"""
return NotImplemented
@escape('a')
def INT(self, command=None, param=None):
"""INTerrupt"""
return NotImplemented
@escape('b')
def EMI(self, command=None, param=None):
"""Enable Manual Input"""
return NotImplemented
@escape('d')
def CMD(self, command=None, param=None):
"""Coding Method Delimiter"""
return NotImplemented
# --------------------
@control('N')
def EF(self, command=None, param=None):
"""Erase in Field"""
return NotImplemented
@control('O')
def EA(self, command=None, param=None):
"""Erase in Area"""
return NotImplemented
@control('Q')
def SSE(self, command=None, param=None):
return NotImplemented
pass
@control('R')
def CPR(self, command=None, param=None):
"""Active Position Report"""
return NotImplemented
@control('U')
def NP(self, command=None, param=None):
"""Next Page"""
return NotImplemented
@control('V')
def PP(self, command=None, param=None):
"""Previous Page"""
return NotImplemented
@control('W')
def CTC(self, command=None, param=None):
"""Cursor Tabulation Control"""
return NotImplemented
@control('Y')
def CVT(self, command=None, param=None):
"""Cursor Line Tabulation"""
return NotImplemented
@control('[')
def SRS(self, command=None, param=None):
"""Start Reversed String"""
return NotImplemented
@control('\\')
def PTX(self, command=None, param=None):
"""Parallel Texts"""
return NotImplemented
@control(']')
def SDS(self, command=None, param=None):
"""Start Directed String"""
return NotImplemented
@control('^')
def SIMD(self, command=None, param=None):
"""Select Implicit Movement Direction"""
return NotImplemented
# no @control('_')
@control('o')
def DAQ(self, command=None, param=None):
"""Define Area Qualification"""
return NotImplemented
# --------------------
@ansi_mode(1)
def GATM(self, value):
"""Guarded Area Transfer Mode"""
return NotImplemented
@ansi_mode(3)
def CRM(self, value):
"""Control Representation Mode"""
return NotImplemented
@ansi_mode(5)
def SRTM(self, value):
"""Status Report Transfer Mode"""
return NotImplemented
@ansi_mode(6)
def ERM(self, value):
"""Erasure Mode"""
return NotImplemented
@ansi_mode(7)
def VEM(self, value):
"""Line Editing Mode"""
return NotImplemented
@ansi_mode(8)
def BDSM(self, value):
"""Bi-Directional Support Mode"""
return NotImplemented
@ansi_mode(9)
def DCSM(self, value):
"""Device Component Select Mode"""
return NotImplemented
@ansi_mode(10)
def HEM(self, value):
"""Character Editing Mode"""
return NotImplemented
@ansi_mode(11)
def PUM(self, value):
"""Positioning Unit Mode"""
return NotImplemented
@ansi_mode(13)
def FEAM(self, value):
"""Format Effector Action Mode"""
return NotImplemented
@ansi_mode(14)
def FETM(self, value):
"""Format Effector Transfer Mode"""
return NotImplemented
@ansi_mode(15)
def MATM(self, value):
"""Multiple Area Transfer Mode"""
return NotImplemented
@ansi_mode(16)
def TTM(self, value):
"""Transfer Termination Mode"""
return NotImplemented
@ansi_mode(17)
def SATM(self, value):
"""Selected Area Transfer Mode"""
return NotImplemented
@ansi_mode(18)
def TSM(self, value):
"""Tabulation Stop Mode"""
return NotImplemented
@ansi_mode(19)
def EBM(self, value):
"""Editing Boundary Mode"""
return NotImplemented
@ansi_mode(21)
def GRCM(self, value):
"""Graphic Rendition Combination"""
return NotImplemented
@ansi_mode(22)
def ZDM(self, value):
"""Zero Default Mode"""
return NotImplemented
def remove_script_lines(text):
"""Remove the starting and ending lines produced by script(1)."""
script_re = re.compile(r'^Script (started|done) on \w+ \d+ \w+ \d{4} '
r'\d\d:\d\d:\d\d \w+ \w+$')
try:
first_newline = text.index(b'\n')
first_line = text[:first_newline].decode('ascii')
except (ValueError, UnicodeDecodeError):
pass
else:
if script_re.match(first_line):
text = text[first_newline+1:]
try:
last_newline = text.rstrip().rindex(b'\n')
last_line = text[last_newline+1:].decode('ascii')
except (ValueError, UnicodeDecodeError):
pass
else:
if script_re.match(last_line):
text = text[:last_newline]
return text
def detect_geometry():
"""Determine the console geometry from the current console."""
# This is not very portable, but works on Linux and is easy!
p = subprocess.Popen(['stty', 'size'], stdout=subprocess.PIPE)
stdout = p.communicate()[0]
rows, cols = map(int, stdout.split())
return rows, cols
def parse_geometry(s):
"""Parse a WxH geometry string."""
cols, rows = s.split('x')
cols = int(cols.strip())
rows = int(rows.strip())
return rows, cols
class FileInserter:
"""Helper for SimpleConfigParser"""
def __init__(self, fp, line):
self.fp = fp
self.line = line
def readline(self):
self.readline = self.fp.readline
return self.line
def __iter__(self):
return itertools.chain([self.line], self.fp)
class SimpleConfigParser (ConfigParser):
"""Configuration parser that allows a default section if none is specified
in the configuration file.
Based on SimpleConfigParser, copyright 2010 Philippe Lagadec.
"""
def __init__(self, *args, **kwargs):
self.initial_section = kwargs.pop('initial_section', 'NOSECTION')
ConfigParser.__init__(self, *args, **kwargs)
self.add_section(self.initial_section)
def _read(self, fp, fpname):
firstline = '[%s]\n' % self.initial_section
fp = FileInserter(fp, firstline)
return ConfigParser._read(self, fp, fpname)
def get(self, section, *args, **kwargs):
if section is None:
section = self.initial_section
return ConfigParser.get(self, section, *args, **kwargs)
def set(self, section, *args, **kwargs):
if section is None:
section = self.initial_section
return ConfigParser.set(self, section, *args, **kwargs)
def main():
usage = "%prog [OPTIONS] [-f FORMAT] [-g WxH] (filename|-)"
version = "%%prog %s" % __version__
parser = OptionParser(usage=usage, version=version)
parser.add_option('--man', action='store_true', default=False,
help='show the manual page and exit')
parser.add_option('-f', '--format', choices=('text','html'),
help='output format. Choices: text, html')
parser.add_option('-g', '--geometry', metavar='WxH',
help='use W columns and H rows in output, or "detect"')
parser.add_option('--non-script', action='store_true', default=False,
help='do not ignore "Script (started|done) on <date>" lines')
parser.add_option('--rc', metavar='FILE', default='~/.vt100rc',
help='read configuration from FILE (default %default)')
parser.add_option('--no-rc', action='store_true', default=False,
help='suppress reading of configuration file')
parser.add_option('-q', '--quiet', action='count', default=0,
help='decrease debugging verbosity')
parser.add_option('-v', '--verbose', action='count', default=0,
help='increase debugging verbosity')
html_group = OptionGroup(parser, "HTML Options")
html_group.add_option('--background', metavar='COLOR',
help="set the default foreground color")
html_group.add_option('--foreground', metavar='COLOR',
help="set the default background color")
html_group.add_option('--colorscheme', metavar='SCHEME',
help='use the given color scheme')
parser.add_option_group(html_group)
options, args = parser.parse_args()
if options.man:
print(globals()['__doc__'])
return 0
defaults = {
'format' : 'text',
'geometry' : '80x24',
'verbosity' : '0',
}
config = SimpleConfigParser(defaults)
if not options.no_rc:
configfile = os.path.expanduser(options.rc)
config.read(configfile)
for opt in html_group.option_list:
name = opt.dest
value = getattr(options, name)
if value is not None:
config.set(None, name, value)
options.verbose -= options.quiet
options.verbose += config.getint(None, 'verbosity')
del options.quiet
if len(args) != 1:
parser.error('missing required filename argument')
filename, = args
if filename == '-':
text = sys.stdin.read()
else:
with open(filename, 'rb') as f:
text = f.read()
if options.format is None:
options.format = config.get(None, 'format')
formatter = formatters[options.format](config=config)
if options.geometry is None:
options.geometry = config.get(None, 'geometry')
if options.geometry == 'detect':
rows, cols = detect_geometry()
else:
try:
rows, cols = parse_geometry(options.geometry)
except:
parser.error('invalid format for --geometry: %s' % options.geometry)
t = Terminal(verbosity=options.verbose, formatter=formatter,
width=cols, height=rows)
if not options.non_script:
text = remove_script_lines(text)
t.parse(text)
print(t.to_string(), end='')
if __name__ == "__main__":
sys.exit(main())