script-publish.py: Allow emulator selection with --term
authorW. Trevor King <wking@tremily.us>
Sun, 13 Jan 2013 12:50:55 +0000 (07:50 -0500)
committerW. Trevor King <wking@tremily.us>
Sun, 13 Jan 2013 12:50:55 +0000 (07:50 -0500)
This means we have to delay ControlCode initialization until the user
has actually selected a terminal.  The curses.setupterm() logic also
assumes that the selected terminal doesn't change after the initial
call.  For our script, this is a safe assumption.  If that assumtion
eventually breaks down, you'll have to cache the last-setup terminal
somewhere, and call setupterm() whenever the current control code's
`.term` doesn't match the cached value.

I also fixed a bug where args.encoding was not being passed to
ScriptServerApp.

posts/script/script-publish.py

index eeb602b09be16844da435b7c04bb60add1503341..4f78ac7525ea8c175da7467f6cfbef45da5acd40 100755 (executable)
@@ -114,9 +114,13 @@ class ControlCode (object):
     >>> caret_escape('^H')
     8
     """
-    def __init__(self, name=None, capname=None, code=None, regexp=None):
+    def __init__(self, name=None, capname=None, term=None, code=None,
+                 regexp=None):
         self.name = name
         self.capname = capname  # terminfo capability name (cf. terminfo(5))
+        if term is None:
+            term = 'ansi'
+        self.term = term
         if code is None:
             code = self._get_code()
         self.code = code
@@ -139,11 +143,13 @@ class ControlCode (object):
         try:
             code = _curses.tigetstr(self.capname)
         except _curses.error:
-            term = _os.environ.get('term', 'xterm')
             with open('/dev/null', 'w') as f:
                 # setupterm() is required for curses.tiget*()
-                _curses.setupterm(term, f.fileno())
+                _curses.setupterm(self.term, f.fileno())
             code = _curses.tigetstr(self.capname)
+        if code is None:
+            _LOG.warning('{} not supported on {} terminals'.format(
+                    self, self.term))
         # code may be None if your terminal doesn't support the capability
         return code
 
@@ -219,9 +225,9 @@ class SelectGraphicRendition (ControlCode):
     colors = [
         'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white']
 
-    def __init__(self):
+    def __init__(self, term=None):
         super(SelectGraphicRendition, self).__init__(
-            'select graphic rendition', 'sgr',
+            name='select graphic rendition', capname='sgr', term=term,
             regexp=_re.compile(b'\x1b[[]([0-9]+)?;?([0-9]+)?m'))
 
     def style(self, match, style=None):
@@ -266,52 +272,6 @@ class SelectGraphicRendition (ControlCode):
         return style
 
 
-CONTROL_CODES = [  # see terminfo(5)
-    # Safe to ignore
-    ControlCode('bell', 'bel'),
-    ControlCode('meta on', 'smm'),  # eight bit mode
-    # Clearing the screen
-    ControlCode('backspace key', 'kbs'),
-    ControlCode('delete one character', 'dch1'),
-    ControlCode('delete characters', 'dch'),
-    ControlCode('clear screen', 'clear'),
-    ControlCode('clear to beginning of line', 'el1'),
-    ControlCode('clear to end of line', 'el'),
-    ControlCode('clear to end of screen', 'ed'),
-    # Cursor motion
-    ControlCode('carriage return', 'cr'),
-    ControlCode('scroll forward', 'ind'),
-    ControlCode('cursor to upper left', 'home'),
-    ControlCode('enter ca mode', 'smcup'),  # start program using 'cup'
-    ControlCode('exit ca mode', 'rmcup'),  # end program using 'cup'
-    ControlCode('cursor address', 'cup'),
-    ControlCode('cursor up', 'cuu1'),
-    ControlCode('cursor down', 'cud1'),
-    ControlCode('cursor left', 'cub1'),
-    ControlCode('cursor right', 'cuf1'),
-    ControlCode('cursor row address', 'vpa'),
-    # Cursor visibility
-    ControlCode('cursor visible', 'cvvis'),
-    ControlCode('cursor invisible', 'civis'),
-    ControlCode('cursor normal visibility', 'cnorm'),
-    # Funky stuff
-    ControlCode('horizontal tab', 'ht'),
-    ControlCode('insert line', 'il1'),
-    ControlCode('insert line', 'il'),
-    ControlCode('erase characters', 'ech'),
-    ControlCode('operating system command', regexp=_re.compile(b'\x1b]')),  # [1]
-    ControlCode('application keypad mode', regexp=_re.compile(b'\x1b=')),  # [1]
-    ControlCode('numeric keypad mode', regexp=_re.compile(b'\x1b>')),  # [1]
-    ControlCode('set application cursor keys', regexp=_re.compile(b'\x1b[[][?]1h')),  # [1]
-    ControlCode('reset application cursor keys', regexp=_re.compile(b'\x1b[[][?]1l')),  # [1]
-    ControlCode('set scrolling region', regexp=_re.compile(b'\x1b[[]([0-9]+);([0-9]+)r')),  # [1]
-    ControlCode('send secondary device attributes', regexp=_re.compile(b'\x1b[[]>0c')),  # [1], i.e. "what kind of device are you?" [2]
-    SelectGraphicRendition(),
-    ]
-# [1]: http://www.gnu.org/software/screen/manual/html_node/Control-Sequences.html
-# [2]: http://vt100.net/docs/vt220-rm/chapter4.html#S4.17.1.2
-
-
 class StyledText (object):
     def __init__(self, text, style=None):
         self.text = text
@@ -408,8 +368,71 @@ class ControlParser (object):
     <span class="outside"><span style="color: cyan">@@ -8,6 +8,10 @@</span> idea</span>
 
     """
-    def __init__(self, encoding='utf-8'):
+    def __init__(self, encoding='utf-8', term=None):
         self._encoding = encoding
+        self._term = term
+        self._control_codes = self._get_control_codes()
+
+    def _get_control_codes(self):
+        # see terminfo(5)
+        return [
+            # Safe to ignore
+            ControlCode('bell', 'bel', term=self._term),
+            ControlCode('meta on', 'smm', term=self._term),  # eight bit mode
+            # Clearing the screen
+            ControlCode('backspace key', 'kbs', term=self._term),
+            ControlCode('delete one character', 'dch1', term=self._term),
+            ControlCode('delete characters', 'dch', term=self._term),
+            ControlCode('clear screen', 'clear', term=self._term),
+            ControlCode('clear to beginning of line', 'el1', term=self._term),
+            ControlCode('clear to end of line', 'el', term=self._term),
+            ControlCode('clear to end of screen', 'ed', term=self._term),
+            # Cursor motion
+            ControlCode('carriage return', 'cr', term=self._term),
+            ControlCode('scroll forward', 'ind', term=self._term),
+            ControlCode('cursor to upper left', 'home', term=self._term),
+            ControlCode('enter ca mode', 'smcup', term=self._term
+                        ),  # start program using 'cup'
+            ControlCode('exit ca mode', 'rmcup', term=self._term
+                        ),  # end program using 'cup'
+            ControlCode('cursor address', 'cup', term=self._term),
+            ControlCode('cursor up', 'cuu1', term=self._term),
+            ControlCode('cursor down', 'cud1', term=self._term),
+            ControlCode('cursor left', 'cub1', term=self._term),
+            ControlCode('cursor right', 'cuf1', term=self._term),
+            ControlCode('cursor row address', 'vpa', term=self._term),
+            # Cursor visibility
+            ControlCode('cursor visible', 'cvvis', term=self._term),
+            ControlCode('cursor invisible', 'civis', term=self._term),
+            ControlCode('cursor normal visibility', 'cnorm', term=self._term),
+            # Funky stuff
+            ControlCode('horizontal tab', 'ht', term=self._term),
+            ControlCode('insert line', 'il1', term=self._term),
+            ControlCode('insert line', 'il', term=self._term),
+            ControlCode('erase characters', 'ech', term=self._term),
+            ControlCode('operating system command',
+                        regexp=_re.compile(b'\x1b]'), term=self._term),  # [1]
+            ControlCode('application keypad mode',
+                        regexp=_re.compile(b'\x1b='), term=self._term),  # [1]
+            ControlCode('numeric keypad mode',
+                        regexp=_re.compile(b'\x1b>'), term=self._term),  # [1]
+            ControlCode('set application cursor keys',
+                        regexp=_re.compile(b'\x1b[[][?]1h'),
+                        term=self._term),  # [1]
+            ControlCode('reset application cursor keys',
+                        regexp=_re.compile(b'\x1b[[][?]1l'),
+                        term=self._term),  # [1]
+            ControlCode('set scrolling region',
+                        regexp=_re.compile(b'\x1b[[]([0-9]+);([0-9]+)r'),
+                        term=self._term),  # [1]
+            ControlCode('send secondary device attributes',
+                        regexp=_re.compile(b'\x1b[[]>0c'),
+                        term=self._term
+                        ),  # [1], i.e. "what kind of device are you?" [2]
+            SelectGraphicRendition(term=self._term),
+            ]
+        # [1]: http://www.gnu.org/software/screen/manual/html_node/Control-Sequences.html
+        # [2]: http://vt100.net/docs/vt220-rm/chapter4.html#S4.17.1.2
 
     def _replace_control_codes(self, text):
         """Split ``text`` into a list of chunks
@@ -418,7 +441,7 @@ class ControlParser (object):
         ``ControlCodeMatch`` instances.
         """
         stack = [text]
-        for cc in CONTROL_CODES:
+        for cc in self._control_codes:
             if cc.regexp is None:
                 continue
             stream = []
@@ -608,8 +631,8 @@ class ControlParser (object):
 
 
 class ScriptParser (object):
-    def __init__(self, encoding='utf-8', css=None):
-        self._control_parser = ControlParser(encoding=encoding)
+    def __init__(self, encoding='utf-8', css=None, term=None):
+        self._control_parser = ControlParser(encoding=encoding, term=term)
         self._encoding = encoding
         if css is None:
             css = self._get_css()
@@ -756,6 +779,9 @@ if __name__ == '__main__':
     parser.add_argument(
         '-e', '--encoding', metavar='ENCODING', default='utf-8',
         help='file content encoding (defaults to utf-8)')
+    parser.add_argument(
+        '--term', metavar='TERM', default=_os.environ.get('TERM', 'ansi'),
+        help='terminal to emulate (defaults to $TERM)')
     parser.add_argument(
         '-p', '--poll', metavar='SECONDS', type=float,
         help='poll frequency in seconds')
@@ -768,10 +794,11 @@ if __name__ == '__main__':
     if args.verbose:
         _LOG.setLevel(max(_logging.DEBUG, _LOG.level - 10*args.verbose))
 
-    sp = ScriptParser(encoding=args.encoding)
+    sp = ScriptParser(encoding=args.encoding, term=args.term)
     if args.serve:
         server = ScriptServerApp(
-            source=args.source, target=args.target, delay=args.poll or 2)
+            source=args.source, target=args.target, delay=args.poll or 2,
+            encoding=args.encoding, term=args.term)
         server.run(port=args.serve)
     elif args.poll:
         while True: