[tor-commits] [nyx/master] Replace ansi_to_output() with a working function

atagar at torproject.org atagar at torproject.org
Sun Jul 31 23:32:41 UTC 2016


commit bf44422c22507010c9712151b913d8969f1f873d
Author: Damian Johnson <atagar at torproject.org>
Date:   Mon Jul 25 10:43:32 2016 -0700

    Replace ansi_to_output() with a working function
    
    Interpreter panel's ansi_to_output() is horribly overly simplistic. It formats
    the whole line with the attribute it starts with. For instance, /help output
    makes the whole thing bold rather than just the keyword. It also didn't
    recognize the color white.
---
 nyx/curses.py             | 59 +++++++++++++++++++++++++++++++++++++++++++++++
 nyx/panel/interpreter.py  | 29 +++++++----------------
 test/panel/interpreter.py |  9 --------
 test/subwindow.py         | 10 ++++++++
 4 files changed, 77 insertions(+), 30 deletions(-)

diff --git a/nyx/curses.py b/nyx/curses.py
index b33abd3..10ac4f0 100644
--- a/nyx/curses.py
+++ b/nyx/curses.py
@@ -18,6 +18,7 @@ if we want Windows support in the future too.
   curses_attr - curses encoded text attribute
   screen_size - provides the dimensions of our screen
   screenshot - dump of the present on-screen content
+  asci_to_curses - converts terminal formatting to curses
   halt - prevents further curses rendering during shutdown
 
   is_color_supported - checks if terminal supports color output
@@ -88,6 +89,7 @@ import curses.ascii
 import curses.textpad
 import functools
 import os
+import re
 import threading
 
 import stem.util.conf
@@ -112,6 +114,7 @@ RED, GREEN, YELLOW, BLUE, CYAN, MAGENTA, BLACK, WHITE = list(Color)
 
 Attr = stem.util.enum.Enum('NORMAL', 'BOLD', 'UNDERLINE', 'HIGHLIGHT')
 NORMAL, BOLD, UNDERLINE, HIGHLIGHT = list(Attr)
+ANSI_RE = re.compile('\x1B\[([0-9;]+)m')
 
 CURSES_COLORS = {
   Color.RED: curses.COLOR_RED,
@@ -131,6 +134,18 @@ CURSES_ATTRIBUTES = {
   Attr.HIGHLIGHT: curses.A_STANDOUT,
 }
 
+ASCI_TO_CURSES = {
+  '1': BOLD,
+  '30': BLACK,
+  '31': RED,
+  '32': GREEN,
+  '33': YELLOW,
+  '34': BLUE,
+  '35': MAGENTA,
+  '36': CYAN,
+  '37': WHITE,
+}
+
 DEFAULT_COLOR_ATTR = dict([(color, 0) for color in Color])
 COLOR_ATTR = None
 
@@ -460,6 +475,50 @@ def screenshot():
   return '\n'.join(lines).rstrip()
 
 
+def asci_to_curses(msg):
+  """
+  Translates ANSI terminal escape sequences to curses formatting.
+
+  :param str msg: string to be converted
+
+  :returns: **list** series of (text, attr) tuples that's renderable by curses
+  """
+
+  entries, next_attr = [], ()
+  match = ANSI_RE.search(msg)
+
+  while match:
+    if match.start() > 0:
+      entries.append((msg[:match.start()], next_attr))
+
+    curses_attr = match.group(1).split(';')
+    new_attr = [ASCI_TO_CURSES[num] for num in curses_attr if num in ASCI_TO_CURSES]
+
+    if '0' in curses_attr:
+      next_attr = tuple(new_attr)  # includes a 'reset'
+    else:
+      combined_attr = list(next_attr)
+
+      for attr in new_attr:
+        if attr in combined_attr:
+          continue
+        elif attr in Color:
+          # replace previous color with new one
+          combined_attr = filter(lambda attr: attr not in Color, combined_attr)
+
+        combined_attr.append(attr)
+
+      next_attr = tuple(combined_attr)
+
+    msg = msg[match.end():]
+    match = ANSI_RE.search(msg)
+
+  if msg:
+    entries.append((msg, next_attr))
+
+  return entries
+
+
 def halt():
   """
   Prevents further rendering of curses content while python's shutting down.
diff --git a/nyx/panel/interpreter.py b/nyx/panel/interpreter.py
index 7775028..1dcb0e8 100644
--- a/nyx/panel/interpreter.py
+++ b/nyx/panel/interpreter.py
@@ -7,12 +7,11 @@ import code
 import curses
 import nyx.controller
 import nyx.curses
-import re
 import sys
 
 from cStringIO import StringIO
 from mock import patch
-from nyx.curses import BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, BOLD, HIGHLIGHT, NORMAL
+from nyx.curses import GREEN, MAGENTA, CYAN, BOLD, HIGHLIGHT
 from nyx import tor_controller, panel
 
 import stem
@@ -22,25 +21,9 @@ import stem.interpreter.commands
 
 USAGE_INFO = 'to use this panel press enter'
 PROMPT = '>>> '
-ANSI_RE = re.compile('\\x1b\[([0-9;]*)m')
-ATTRS = {'0': NORMAL, '1': BOLD, '30': BLACK, '31': RED, '32': GREEN, '33': YELLOW, '34': BLUE, '35': MAGENTA, '36': CYAN}
 BACKLOG_LIMIT = 100
 
 
-def ansi_to_output(line, attrs):
-  ansi_re = ANSI_RE.findall(line)
-  new_attrs = []
-
-  if line.find('\x1b[') == 0 and ansi_re:
-    for attr in ansi_re[0].split(';'):
-      new_attrs.append(ATTRS[attr])
-    attrs = new_attrs
-
-  line = ANSI_RE.sub('', line)
-
-  return [(line, ) + tuple(attrs)], attrs
-
-
 def format_input(user_input):
   output = [(PROMPT, GREEN, BOLD)]
 
@@ -123,10 +106,14 @@ class InterpreterPanel(panel.Panel):
               sys.stderr = old_stderr
             if response:
               self.prompt_line.insert(len(self.prompt_line) - 1, format_input(user_input))
-              attrs = []
+
               for line in response.split('\n'):
-                line, attrs = ansi_to_output(line, attrs)
-                self.prompt_line.insert(len(self.prompt_line) - 1, line)
+                new_line = []
+
+                for text, attr in nyx.curses.asci_to_curses(line):
+                  new_line.append([text] + list(attr))
+
+                self.prompt_line.insert(len(self.prompt_line) - 1, new_line)
           except stem.SocketClosed:
             is_done = True
 
diff --git a/test/panel/interpreter.py b/test/panel/interpreter.py
index 8283c5e..df42980 100644
--- a/test/panel/interpreter.py
+++ b/test/panel/interpreter.py
@@ -30,15 +30,6 @@ EXPECTED_SCROLLBAR_PANEL = ' |>>> to use this panel press enter'
 
 
 class TestInterpreter(unittest.TestCase):
-  def test_ansi_to_output(self):
-    ansi_text = '\x1b[32;1mthis is some sample text'
-    output_line, attrs = nyx.panel.interpreter.ansi_to_output(ansi_text, [])
-
-    self.assertEqual('this is some sample text', output_line[0][0])
-    self.assertEqual('Green', output_line[0][1])
-    self.assertEqual('Bold', output_line[0][2])
-    self.assertEqual(['Green', 'Bold'], attrs)
-
   def test_format_input(self):
     user_input = 'getinfo'
     output = nyx.panel.interpreter.format_input(user_input)
diff --git a/test/subwindow.py b/test/subwindow.py
index 2df946e..db97970 100644
--- a/test/subwindow.py
+++ b/test/subwindow.py
@@ -14,6 +14,7 @@ import test
 from mock import call, Mock
 
 from test import require_curses
+from nyx.curses import Color, Attr
 
 EXPECTED_ADDSTR_WRAP = """
 0123456789 0123456789
@@ -81,6 +82,15 @@ def _textbox(x = 0, text = ''):
 
 
 class TestCurses(unittest.TestCase):
+  def test_asci_to_curses(self):
+    self.assertEqual([], nyx.curses.asci_to_curses(''))
+    self.assertEqual([('hi!', ())], nyx.curses.asci_to_curses('hi!'))
+    self.assertEqual([('hi!', (Color.RED,))], nyx.curses.asci_to_curses('\x1b[31mhi!\x1b[0m'))
+    self.assertEqual([('boo', ()), ('hi!', (Color.RED, Attr.BOLD))], nyx.curses.asci_to_curses('boo\x1b[31;1mhi!\x1b[0m'))
+    self.assertEqual([('boo', ()), ('hi', (Color.RED,)), (' dami!', (Color.RED, Attr.BOLD))], nyx.curses.asci_to_curses('boo\x1b[31mhi\x1b[1m dami!\x1b[0m'))
+    self.assertEqual([('boo', ()), ('hi', (Color.RED,)), (' dami!', (Color.BLUE,))], nyx.curses.asci_to_curses('boo\x1b[31mhi\x1b[34m dami!\x1b[0m'))
+    self.assertEqual([('boo', ()), ('hi!', (Color.RED, Attr.BOLD)), ('and bye!', ())], nyx.curses.asci_to_curses('boo\x1b[31;1mhi!\x1b[0mand bye!'))
+
   @require_curses
   def test_addstr(self):
     def _draw(subwindow):





More information about the tor-commits mailing list