commit bf44422c22507010c9712151b913d8969f1f873d Author: Damian Johnson atagar@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):