def test_ansi_terminal_parser_palette(self): """ Check AnsiTerminalParser colour palettes work as expected. """ parser = AnsiTerminalParser() parser.reset( "\x1B[38;1ma\x1B[38;5;17mb\x1B[48;2;1;2;3mc\x1B[48;5;54md\x1B[999me\x1B[93m\x1B[104m", None) tokens = parser.parse() # Bad colour scheme - ignore self.assertEquals(next(tokens), (0, Parser.DISPLAY_TEXT, "a")) # Standard colour palette self.assertEquals(next(tokens), (8, Parser.CHANGE_COLOURS, (17, None, None))) self.assertEquals(next(tokens), (8, Parser.DISPLAY_TEXT, "b")) # RGB colour scheme - ignore self.assertEquals(next(tokens), (19, Parser.DISPLAY_TEXT, "c")) # Standard colour palette self.assertEquals(next(tokens), (33, Parser.CHANGE_COLOURS, (17, None, 54))) self.assertEquals(next(tokens), (33, Parser.DISPLAY_TEXT, "d")) # Unknown parameter self.assertEquals(next(tokens), (44, Parser.DISPLAY_TEXT, "e")) # Intense colour palette self.assertEquals(next(tokens), (51, Parser.CHANGE_COLOURS, (11, None, 54))) self.assertEquals(next(tokens), (51, Parser.CHANGE_COLOURS, (11, None, 12)))
def test_ansi_terminal_parser_tab(self): """ Check AnsiTerminalParser handles tabs. """ parser = AnsiTerminalParser() parser.reset("\x09", None) tokens = parser.parse() self.assertEquals(next(tokens), (0, Parser.NEXT_TAB, None))
def test_ansi_terminal_parser_clear(self): """ Check AnsiTerminalParser clears screen. """ parser = AnsiTerminalParser() parser.reset("\x1B[2J", None) tokens = parser.parse() self.assertEquals(next(tokens), (0, Parser.CLEAR_SCREEN, None))
def test_ansi_terminal_parser_os_cmd(self): """ Check AnsiTerminalParser removes OS commands. """ parser = AnsiTerminalParser() parser.reset("a\x1B]do something;stuff:to^ignore\x07b", None) tokens = parser.parse() self.assertEquals(next(tokens), (0, Parser.DISPLAY_TEXT, "a")) self.assertEquals(next(tokens), (1, Parser.DISPLAY_TEXT, "b"))
def test_ansi_terminal_parser_bell(self): """ Check AnsiTerminalParser handles bell. """ parser = AnsiTerminalParser() parser.reset("\x07", None) tokens = parser.parse() with self.assertRaises(StopIteration): next(tokens)
def test_ansi_terminal_parser(self): """ Check AnsiTerminalParser works as expected. """ parser = AnsiTerminalParser() tokens = parser.parse( "a\x1B[23ab\x1B[0mc\x1B[1md\x1B[2me\x1B[7mf\x1B[27mg\x1B[31;42mh", None) # Normal text self.assertEquals(next(tokens), ("a", (None, None, None), 0)) # Unknown escape code self.assertEquals(next(tokens), ("b", (None, None, None), 1)) # Reset self.assertEquals(next(tokens), ("c", (7, constants.A_NORMAL, 0), 7)) # Bold self.assertEquals(next(tokens), ("d", (7, constants.A_BOLD, 0), 12)) # Normal self.assertEquals(next(tokens), ("e", (7, constants.A_NORMAL, 0), 17)) # Inverse self.assertEquals(next(tokens), ("f", (7, constants.A_REVERSE, 0), 22)) # Unset inverse self.assertEquals(next(tokens), ("g", (7, constants.A_NORMAL, 0), 27)) # Standard colours, using multiple parameters self.assertEquals(next(tokens), ("h", (constants.COLOUR_RED, constants.A_NORMAL, constants.COLOUR_GREEN), 33)) with self.assertRaises(StopIteration): next(tokens) # Special case colour specifications tokens = parser.parse( "\x1B[38;1ma\x1B[38;5;17mb\x1B[48;2;1;2;3mc\x1B[48;5;54md\x1B[999me", None) # Bad colour scheme - ignore self.assertEquals(next(tokens), ("a", (None, None, None), 0)) # Standard colour palette self.assertEquals(next(tokens), ("b", (17, None, None), 8)) # RGB colour scheme - ignore self.assertEquals(next(tokens), ("c", (17, None, None), 19)) # Standard colour palette self.assertEquals(next(tokens), ("d", (17, None, 54), 33)) # Unknown parameter self.assertEquals(next(tokens), ("e", (17, None, 54), 44))
def test_ansi_terminal_parser_colours(self): """ Check AnsiTerminalParser basic colours work as expected. """ parser = AnsiTerminalParser() parser.reset( "a\x1B[23ab\x1B[0mc\x1B[1md\x1B[2me\x1B[7mf\x1B[27mg\x1B[31;42mh\x1B[m", None) tokens = parser.parse() # Normal text self.assertEquals(next(tokens), (0, Parser.DISPLAY_TEXT, "a")) # Unknown escape code self.assertEquals(next(tokens), (1, Parser.DISPLAY_TEXT, "b")) # Reset self.assertEquals(next(tokens), (7, Parser.CHANGE_COLOURS, (7, constants.A_NORMAL, 0))) self.assertEquals(next(tokens), (7, Parser.DISPLAY_TEXT, "c")) # Bold self.assertEquals(next(tokens), (12, Parser.CHANGE_COLOURS, (7, constants.A_BOLD, 0))) self.assertEquals(next(tokens), (12, Parser.DISPLAY_TEXT, "d")) # Normal self.assertEquals(next(tokens), (17, Parser.CHANGE_COLOURS, (7, constants.A_NORMAL, 0))) self.assertEquals(next(tokens), (17, Parser.DISPLAY_TEXT, "e")) # Inverse self.assertEquals(next(tokens), (22, Parser.CHANGE_COLOURS, (7, constants.A_REVERSE, 0))) self.assertEquals(next(tokens), (22, Parser.DISPLAY_TEXT, "f")) # Unset inverse self.assertEquals(next(tokens), (27, Parser.CHANGE_COLOURS, (7, constants.A_NORMAL, 0))) self.assertEquals(next(tokens), (27, Parser.DISPLAY_TEXT, "g")) # Standard colours, using multiple parameters self.assertEquals(next(tokens), (33, Parser.CHANGE_COLOURS, (constants.COLOUR_RED, constants.A_NORMAL, constants.COLOUR_GREEN))) self.assertEquals(next(tokens), (33, Parser.DISPLAY_TEXT, "h")) # Final escape sequence with no visible text is returned with no text. self.assertEquals(next(tokens), (42, Parser.CHANGE_COLOURS, (constants.COLOUR_WHITE, constants.A_NORMAL, constants.COLOUR_BLACK))) with self.assertRaises(StopIteration): next(tokens)
def test_ansi_terminal_parser_errors(self): """ Check AnsiTerminalParser handles unsupported encodings gracefully. """ parser = AnsiTerminalParser() parser.reset("a\x1BZb\x07c", None) tokens = parser.parse() # Ignore unknown escape and next letter self.assertEquals(next(tokens), ("a", (None, None, None), 0)) self.assertEquals(next(tokens), ("b", (None, None, None), 1)) # Ignore unknown control char self.assertEquals(next(tokens), ("c", (None, None, None), 4))
def __init__(self, height): super(Terminal, self).__init__(height, line_wrap=True, parser=AnsiTerminalParser()) #Key definitions self._map = {} for k, v in [ (Screen.KEY_LEFT, "kcub1"), (Screen.KEY_RIGHT, "kcuf1"), (Screen.KEY_UP, "kcuu1"), (Screen.KEY_DOWN, "kcud1"), (Screen.KEY_HOME, "khome"), (Screen.KEY_END, "kend"), (Screen.KEY_BACK, "kbs"), ]: self._map[k] = curses.tigetstr(v) self._map[Screen.KEY_TAB] = "\t".encode() # Open a pseudo TTY to control the interactive session. Make it non-blocking. self._master, slave = pty.openpty() fl = fcntl.fcntl(self._master, fcntl.F_GETFL) fcntl.fcntl(self._master, fcntl.F_SETFL, fl | os.O_NONBLOCK) # Start the shell and thread to pull data from it. self._shell = subprocess.Popen(["bash", "-i"], preexec_fn=os.setsid, stdin=slave, stdout=slave, stderr=slave) self._lock = threading.Lock() self._thread = threading.Thread(target=self._background) self._thread.daemon = True self._thread.start()
def __init__(self, screen): super(DemoFrame, self).__init__(screen, screen.height, screen.width) # Create the widgets for the demo. layout = Layout([1, 18, 1], fill_frame=True) self.add_layout(layout) self._term_out = TextBox(Widget.FILL_FRAME, line_wrap=True, parser=AnsiTerminalParser()) layout.add_widget(self._term_out, 1) layout.add_widget(Divider(height=2), 1) layout2 = Layout([1, 15, 3, 1]) self.add_layout(layout2) self._term_in = Text() layout2.add_widget(self._term_in, 1) layout2.add_widget(Button("Run", self._run), 2) self.fix() self.set_theme("monochrome") # Open a pseudo TTY to control the interactive session. Make it non-blocking. self._master, slave = pty.openpty() fl = fcntl.fcntl(self._master, fcntl.F_GETFL) fcntl.fcntl(self._master, fcntl.F_SETFL, fl | os.O_NONBLOCK) # Start the shell and thread to pull data from it. self._shell = subprocess.Popen(["bash", "-i"], preexec_fn=os.setsid, stdin=slave, stdout=slave, stderr=slave) self._lock = threading.Lock() self._thread = threading.Thread(target=self._background) self._thread.daemon = True self._thread.start()
def test_ansi_terminal_parser_errors(self): """ Check AnsiTerminalParser handles unsupported encodings gracefully. """ parser = AnsiTerminalParser() parser.reset("a\x1BZb\x01c", None) tokens = parser.parse() # Ignore unknown escape and next letter self.assertEquals(next(tokens), (0, Parser.DISPLAY_TEXT, "a")) self.assertEquals(next(tokens), (1, Parser.DISPLAY_TEXT, "b")) # ANSI art uses control codes for special characters - check we just blank them. self.assertEquals(next(tokens), (4, Parser.DISPLAY_TEXT, " ")) # Back to normal. self.assertEquals(next(tokens), (5, Parser.DISPLAY_TEXT, "c"))
def __init__(self, height, width): """ :param height: required height of the renderer. :param width: required width of the renderer. """ super(AbstractScreenPlayer, self).__init__(height, width, clear=False) self._parser = AnsiTerminalParser() self._current_colours = [Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK] self._show_cursor = False self._cursor_x = 0 self._cursor_y = 0 self._save_cursor_x = 0 self._save_cursor_y = 0 self._counter = 0 self._next = 0 self._buffer = None self._parser.reset("", self._current_colours) self._clear()
def test_ansi_terminal_parser_cursor(self): """ Check AnsiTerminalParser cursor movement work as expected. """ parser = AnsiTerminalParser() parser.reset( "aa\x08b\rc\x1B[Cdd\x1B[De\x1B[A\x1B[B\x1B[1;2H\x1B[?25h\x1B[?25l\r", None) tokens = parser.parse() # Normal text... self.assertEquals(next(tokens), (0, Parser.DISPLAY_TEXT, "a")) self.assertEquals(next(tokens), (1, Parser.DISPLAY_TEXT, "a")) # Backspace and overwrite. self.assertEquals(next(tokens), (2, Parser.MOVE_RELATIVE, (-1, 0))) self.assertEquals(next(tokens), (2, Parser.DISPLAY_TEXT, "b")) # Carriage return and overwrite self.assertEquals(next(tokens), (4, Parser.MOVE_ABSOLUTE, (0, None))) self.assertEquals(next(tokens), (4, Parser.DISPLAY_TEXT, "c")) # Move cursor forwards and append. self.assertEquals(next(tokens), (6, Parser.MOVE_RELATIVE, (1, 0))) self.assertEquals(next(tokens), (6, Parser.DISPLAY_TEXT, "d")) self.assertEquals(next(tokens), (10, Parser.DISPLAY_TEXT, "d")) # Move cursor backwards and overwrite. self.assertEquals(next(tokens), (11, Parser.MOVE_RELATIVE, (-1, 0))) self.assertEquals(next(tokens), (11, Parser.DISPLAY_TEXT, "e")) # Move cursor up and down. self.assertEquals(next(tokens), (15, Parser.MOVE_RELATIVE, (0, -1))) self.assertEquals(next(tokens), (15, Parser.MOVE_RELATIVE, (0, 1))) # Move cursor to location self.assertEquals(next(tokens), (15, Parser.MOVE_ABSOLUTE, (1, 0))) # Show/hide cursor self.assertEquals(next(tokens), (15, Parser.SHOW_CURSOR, True)) self.assertEquals(next(tokens), (15, Parser.SHOW_CURSOR, False)) # Trailing Carriage return self.assertEquals(next(tokens), (15, Parser.MOVE_ABSOLUTE, (0, None)))
def test_ansi_terminal_parser_normalization(self): """ Check AnsiTerminalParser normalization works as expected. """ parser = AnsiTerminalParser() # SGR0 sets black and white normal text. parser.reset("\x1B[ma", None) self.assertEquals(parser.normalize(), "\x1B[38;5;7;2;48;5;0ma") # SGR1 sets bold and SGR7 reverse video. parser.reset("\x1B[1ma\x1B[7mb", None) self.assertEquals(parser.normalize(), "\x1B[1ma\x1B[7mb")
def test_ansi_terminal_parser_palette(self): """ Check AnsiTerminalParser colour palettes work as expected. """ parser = AnsiTerminalParser() parser.reset("\x1B[38;1ma\x1B[38;5;17mb\x1B[48;2;1;2;3mc\x1B[48;5;54md\x1B[999me", None) tokens = parser.parse() # Bad colour scheme - ignore self.assertEquals(next(tokens), ("a", (None, None, None), 0)) # Standard colour palette self.assertEquals(next(tokens), ("b", (17, None, None), 8)) # RGB colour scheme - ignore self.assertEquals(next(tokens), ("c", (17, None, None), 19)) # Standard colour palette self.assertEquals(next(tokens), ("d", (17, None, 54), 33)) # Unknown parameter self.assertEquals(next(tokens), ("e", (17, None, 54), 44))
def __init__(self, name, height): super(Terminal, self).__init__(name) self._required_height = height self._parser = AnsiTerminalParser() self._canvas = None self._current_colours = None self._cursor_x, self._cursor_y = 0, 0 self._show_cursor = True # Supported key mappings self._map = {} for k, v in [ (Screen.KEY_LEFT, "kcub1"), (Screen.KEY_RIGHT, "kcuf1"), (Screen.KEY_UP, "kcuu1"), (Screen.KEY_DOWN, "kcud1"), (Screen.KEY_PAGE_UP, "kpp"), (Screen.KEY_PAGE_DOWN, "knp"), (Screen.KEY_HOME, "khome"), (Screen.KEY_END, "kend"), (Screen.KEY_DELETE, "kdch1"), (Screen.KEY_BACK, "kbs"), ]: self._map[k] = curses.tigetstr(v) self._map[Screen.KEY_TAB] = "\t".encode() # Open a pseudo TTY to control the interactive session. Make it non-blocking. self._master, self._slave = pty.openpty() fl = fcntl.fcntl(self._master, fcntl.F_GETFL) fcntl.fcntl(self._master, fcntl.F_SETFL, fl | os.O_NONBLOCK) # Start the shell and thread to pull data from it. self._shell = subprocess.Popen( ["bash", "-i"], preexec_fn=os.setsid, stdin=self._slave, stdout=self._slave, stderr=self._slave) self._lock = threading.Lock() self._thread = threading.Thread(target=self._background) self._thread.daemon = True self._thread.start()
def test_ansi_terminal_parser_def_colours(self): """ Check AnsiTerminalParser default colours work as expected. """ parser = AnsiTerminalParser() parser.reset("a\x1B[39mb\x1B[49mc", None) tokens = parser.parse() # Normal text self.assertEquals(next(tokens), (0, Parser.DISPLAY_TEXT, "a")) # Default foreground colour self.assertEquals(next(tokens), (1, Parser.CHANGE_COLOURS, (constants.COLOUR_DEFAULT, None, None))) self.assertEquals(next(tokens), (1, Parser.DISPLAY_TEXT, "b")) # Default background colour self.assertEquals( next(tokens), (7, Parser.CHANGE_COLOURS, (constants.COLOUR_DEFAULT, None, constants.COLOUR_DEFAULT))) self.assertEquals(next(tokens), (7, Parser.DISPLAY_TEXT, "c"))
def test_ansi_terminal_parser_colours(self): """ Check AnsiTerminalParser basic colours work as expected. """ parser = AnsiTerminalParser() parser.reset("a\x1B[23ab\x1B[0mc\x1B[1md\x1B[2me\x1B[7mf\x1B[27mg\x1B[31;42mh\x1B[m", None) tokens = parser.parse() # Normal text self.assertEquals(next(tokens), ("a", (None, None, None), 0)) # Unknown escape code self.assertEquals(next(tokens), ("b", (None, None, None), 1)) # Reset self.assertEquals(next(tokens), ("c", (7, constants.A_NORMAL, 0), 7)) # Bold self.assertEquals(next(tokens), ("d", (7, constants.A_BOLD, 0), 12)) # Normal self.assertEquals(next(tokens), ("e", (7, constants.A_NORMAL, 0), 17)) # Inverse self.assertEquals(next(tokens), ("f", (7, constants.A_REVERSE, 0), 22)) # Unset inverse self.assertEquals(next(tokens), ("g", (7, constants.A_NORMAL, 0), 27)) # Standard colours, using multiple parameters self.assertEquals(next(tokens), ("h", (constants.COLOUR_RED, constants.A_NORMAL, constants.COLOUR_GREEN), 33)) # Final escape sequence with no visible text is returned with no text. self.assertEquals(next(tokens), (None, (constants.COLOUR_WHITE, constants.A_NORMAL, constants.COLOUR_BLACK), 42)) with self.assertRaises(StopIteration): next(tokens)
def test_ansi_terminal_parser_cursor(self): """ Check AnsiTerminalParser cursor movement work as expected. """ parser = AnsiTerminalParser() parser.reset("aa\x08b\rc\x1B[Cdd\x1B[De\r", None) tokens = parser.parse() # Carriage return and overwrite self.assertEquals(next(tokens), ("c", (None, None, None), 4)) # Backspace and overwrite. self.assertEquals(next(tokens), ("b", (None, None, None), 2)) # Move cursor forwards and append. self.assertEquals(next(tokens), ("d", (None, None, None), 6)) # Move cursor backwards and overwrite. self.assertEquals(next(tokens), ("e", (None, None, None), 11)) # Normalize returns correct linear form - complete with accurate cursor location. self.assertEqual(parser.normalize(), "cbde\x1B[4D")
class AbstractScreenPlayer(DynamicRenderer): """ Abstract renderer to play terminal text with support for ANSI control codes. """ def __init__(self, height, width): """ :param height: required height of the renderer. :param width: required width of the renderer. """ super(AbstractScreenPlayer, self).__init__(height, width, clear=False) self._parser = AnsiTerminalParser() self._current_colours = [Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK] self._show_cursor = False self._cursor_x = 0 self._cursor_y = 0 self._save_cursor_x = 0 self._save_cursor_y = 0 self._counter = 0 self._next = 0 self._buffer = None self._parser.reset("", self._current_colours) self._clear() def _play_content(self, text): """ Process new raw text. :param text: thebraw text to be processed. """ lines = text.split("\n") for i, line in enumerate(lines): self._parser.append(line) for _, command, params in self._parser.parse(): # logging.debug("Command: {} {}".format(command, params)) if command == Parser.DISPLAY_TEXT: # Just display the text... allowing for line wrapping. if self._cursor_x + len(params) >= self._canvas.width: part_1 = params[:self._canvas.width - self._cursor_x] part_2 = params[self._canvas.width - self._cursor_x:] self._print_at(part_1, self._cursor_x, self._cursor_y) self._print_at(part_2, 0, self._cursor_y + 1) self._cursor_x = len(part_2) self._cursor_y += 1 if self._cursor_y - self._canvas.start_line >= self._canvas.height: self._canvas.scroll() else: self._print_at(params, self._cursor_x, self._cursor_y) self._cursor_x += len(params) elif command == Parser.CHANGE_COLOURS: # Change current text colours. self._current_colours = params elif command == Parser.NEXT_TAB: # Move to next tab stop - hard-coded to default of 8 characters. self._cursor_x = (self._cursor_x // 8) * 8 + 8 elif command == Parser.MOVE_RELATIVE: # Move cursor relative to current position. self._cursor_x += params[0] self._cursor_y += params[1] if self._cursor_y < self._canvas.start_line: self._canvas.scroll(self._cursor_y - self._canvas.start_line) elif command == Parser.MOVE_ABSOLUTE: # Move cursor relative to specified absolute position. if params[0] is not None: self._cursor_x = params[0] if params[1] is not None: self._cursor_y = params[1] + self._canvas.start_line elif command == Parser.DELETE_LINE: # Delete some/all of the current line. if params == 0: self._print_at( " " * (self._canvas.width - self._cursor_x), self._cursor_x, self._cursor_y) elif params == 1: self._print_at(" " * self._cursor_x, 0, self._cursor_y) elif params == 2: self._print_at(" " * self._canvas.width, 0, self._cursor_y) elif command == Parser.DELETE_CHARS: # Delete n characters under the cursor. for x in range(self._cursor_x, self._canvas.width): if x + params < self._canvas.width: cell = self._canvas.get_from(x + params, self._cursor_y) else: cell = (ord(" "), self._current_colours[0], self._current_colours[1], self._current_colours[2]) self._canvas.print_at( chr(cell[0]), x, self._cursor_y, colour=cell[1], attr=cell[2], bg=cell[3]) elif command == Parser.SHOW_CURSOR: # Show/hide the cursor. self._show_cursor = params elif command == Parser.SAVE_CURSOR: # Save the cursor position. self._save_cursor_x = self._cursor_x self._save_cursor_y = self._cursor_y elif command == Parser.RESTORE_CURSOR: # Restore the cursor position. self._cursor_x = self._save_cursor_x self._cursor_y = self._save_cursor_y elif command == Parser.CLEAR_SCREEN: # Clear the screen. self._canvas.clear_buffer( self._current_colours[0], self._current_colours[1], self._current_colours[2]) self._cursor_x = 0 self._cursor_y = self._canvas.start_line # Move to next line, scrolling buffer as needed. if i != len(lines) - 1: self._cursor_x = 0 self._cursor_y += 1 if self._cursor_y - self._canvas.start_line >= self._canvas.height: self._canvas.scroll() def _print_at(self, text, x, y): """ Helper function to simplify use of the renderer. """ self._canvas.print_at( text, x, y, colour=self._current_colours[0], attr=self._current_colours[1], bg=self._current_colours[2])
def test_ansi_terminal_parser_delete(self): """ Check AnsiTerminalParser delete operations work as expected. """ parser = AnsiTerminalParser() # Delete to end of line parser.reset("abcde\x08\x08\x08\x1B[K", None) tokens = parser.parse() self.assertEquals(next(tokens), ("a", (None, None, None), 0)) self.assertEquals(next(tokens), ("b", (None, None, None), 1)) self.assertEquals(next(tokens), (None, (None, None, None), 5)) with self.assertRaises(StopIteration): next(tokens) # Delete to start of line parser.reset("abcde\x08\x08\x08\x1B[1K", None) tokens = parser.parse() self.assertEquals(next(tokens), (" ", (None, None, None), 8)) self.assertEquals(next(tokens), (" ", (None, None, None), 8)) self.assertEquals(next(tokens), ("c", (None, None, None), 2)) self.assertEquals(next(tokens), ("d", (None, None, None), 3)) self.assertEquals(next(tokens), ("e", (None, None, None), 4)) self.assertEquals(next(tokens), (None, (None, None, None), 5)) with self.assertRaises(StopIteration): next(tokens) # Delete line parser.reset("abcde\x08\x08\x08\x1B[2K", None) tokens = parser.parse() self.assertEquals(next(tokens), (" ", (None, None, None), 8)) self.assertEquals(next(tokens), (" ", (None, None, None), 8)) self.assertEquals(next(tokens), (None, (None, None, None), 5)) with self.assertRaises(StopIteration): next(tokens) # Delete char parser.reset("abcde\x08\x08\x08\x1B[P", None) tokens = parser.parse() self.assertEquals(next(tokens), ("a", (None, None, None), 0)) self.assertEquals(next(tokens), ("b", (None, None, None), 1)) self.assertEquals(next(tokens), ("d", (None, None, None), 3)) self.assertEquals(next(tokens), ("e", (None, None, None), 4)) self.assertEquals(next(tokens), (None, (None, None, None), 5)) with self.assertRaises(StopIteration): next(tokens)
def test_ansi_terminal_parser_delete(self): """ Check AnsiTerminalParser delete operations work as expected. """ parser = AnsiTerminalParser() # Delete to end of line parser.reset("abcde\x08\x08\x08\x1B[K", None) tokens = parser.parse() self.assertEquals(next(tokens), (0, Parser.DISPLAY_TEXT, "a")) self.assertEquals(next(tokens), (1, Parser.DISPLAY_TEXT, "b")) self.assertEquals(next(tokens), (2, Parser.DISPLAY_TEXT, "c")) self.assertEquals(next(tokens), (3, Parser.DISPLAY_TEXT, "d")) self.assertEquals(next(tokens), (4, Parser.DISPLAY_TEXT, "e")) self.assertEquals(next(tokens), (5, Parser.MOVE_RELATIVE, (-1, 0))) self.assertEquals(next(tokens), (5, Parser.MOVE_RELATIVE, (-1, 0))) self.assertEquals(next(tokens), (5, Parser.MOVE_RELATIVE, (-1, 0))) self.assertEquals(next(tokens), (5, Parser.DELETE_LINE, 0)) with self.assertRaises(StopIteration): next(tokens) # Delete to start of line parser.reset("abcde\x1B[1K", None) tokens = parser.parse() self.assertEquals(next(tokens), (0, Parser.DISPLAY_TEXT, "a")) self.assertEquals(next(tokens), (1, Parser.DISPLAY_TEXT, "b")) self.assertEquals(next(tokens), (2, Parser.DISPLAY_TEXT, "c")) self.assertEquals(next(tokens), (3, Parser.DISPLAY_TEXT, "d")) self.assertEquals(next(tokens), (4, Parser.DISPLAY_TEXT, "e")) self.assertEquals(next(tokens), (5, Parser.DELETE_LINE, 1)) with self.assertRaises(StopIteration): next(tokens) # Delete line parser.reset("abcde\x1B[2K", None) tokens = parser.parse() self.assertEquals(next(tokens), (0, Parser.DISPLAY_TEXT, "a")) self.assertEquals(next(tokens), (1, Parser.DISPLAY_TEXT, "b")) self.assertEquals(next(tokens), (2, Parser.DISPLAY_TEXT, "c")) self.assertEquals(next(tokens), (3, Parser.DISPLAY_TEXT, "d")) self.assertEquals(next(tokens), (4, Parser.DISPLAY_TEXT, "e")) self.assertEquals(next(tokens), (5, Parser.DELETE_LINE, 2)) with self.assertRaises(StopIteration): next(tokens) # Delete char parser.reset("abcde\x08\x08\x08\x1B[P", None) tokens = parser.parse() self.assertEquals(next(tokens), (0, Parser.DISPLAY_TEXT, "a")) self.assertEquals(next(tokens), (1, Parser.DISPLAY_TEXT, "b")) self.assertEquals(next(tokens), (2, Parser.DISPLAY_TEXT, "c")) self.assertEquals(next(tokens), (3, Parser.DISPLAY_TEXT, "d")) self.assertEquals(next(tokens), (4, Parser.DISPLAY_TEXT, "e")) self.assertEquals(next(tokens), (5, Parser.MOVE_RELATIVE, (-1, 0))) self.assertEquals(next(tokens), (5, Parser.MOVE_RELATIVE, (-1, 0))) self.assertEquals(next(tokens), (5, Parser.MOVE_RELATIVE, (-1, 0))) self.assertEquals(next(tokens), (5, Parser.DELETE_CHARS, 1)) with self.assertRaises(StopIteration): next(tokens)
class Terminal(Widget): """ Widget to handle ansi terminals running a bash shell. The widget will start a bash shell in the background and use a pseudo TTY to control it. It then starts a thread to transfer any data between the two processes (the one running this widget and the bash shell). """ def __init__(self, name, height): super(Terminal, self).__init__(name) self._required_height = height self._parser = AnsiTerminalParser() self._canvas = None self._current_colours = None self._cursor_x, self._cursor_y = 0, 0 self._show_cursor = True # Supported key mappings self._map = {} for k, v in [ (Screen.KEY_LEFT, "kcub1"), (Screen.KEY_RIGHT, "kcuf1"), (Screen.KEY_UP, "kcuu1"), (Screen.KEY_DOWN, "kcud1"), (Screen.KEY_PAGE_UP, "kpp"), (Screen.KEY_PAGE_DOWN, "knp"), (Screen.KEY_HOME, "khome"), (Screen.KEY_END, "kend"), (Screen.KEY_DELETE, "kdch1"), (Screen.KEY_BACK, "kbs"), ]: self._map[k] = curses.tigetstr(v) self._map[Screen.KEY_TAB] = "\t".encode() # Open a pseudo TTY to control the interactive session. Make it non-blocking. self._master, self._slave = pty.openpty() fl = fcntl.fcntl(self._master, fcntl.F_GETFL) fcntl.fcntl(self._master, fcntl.F_SETFL, fl | os.O_NONBLOCK) # Start the shell and thread to pull data from it. self._shell = subprocess.Popen( ["bash", "-i"], preexec_fn=os.setsid, stdin=self._slave, stdout=self._slave, stderr=self._slave) self._lock = threading.Lock() self._thread = threading.Thread(target=self._background) self._thread.daemon = True self._thread.start() def set_layout(self, x, y, offset, w, h): """ Resize the widget (and underlying TTY) to the required size. """ super(Terminal, self).set_layout(x, y, offset, w, h) self._canvas = Canvas(self._frame.canvas, h, w, x=x, y=y) winsize = struct.pack("HHHH", h, w, 0, 0) fcntl.ioctl(self._slave, termios.TIOCSWINSZ, winsize) def update(self, frame_no): """ Draw the current terminal content to screen. """ # Don't allow background thread to update values mid screen refresh. with self._lock: # Push current terminal output to screen. self._canvas.refresh() # Draw cursor if needed. if frame_no % 10 < 5 and self._show_cursor: origin = self._canvas.origin x = self._cursor_x + origin[0] y = self._cursor_y + origin[1] - self._canvas.start_line details = self._canvas.get_from(self._cursor_x, self._cursor_y) if details: char, colour, attr, bg = details attr |= Screen.A_REVERSE self._frame.canvas.print_at(chr(char), x, y, colour, attr, bg) def process_event(self, event): """ Pass any recognised input on to the TTY. """ if isinstance(event, KeyboardEvent): if event.key_code > 0: os.write(self._master, chr(event.key_code).encode()) return elif event.key_code in self._map: os.write(self._master, self._map[event.key_code]) return return event def _add_stream(self, value): """ Process any output from the TTY. """ lines = value.split("\n") for i, line in enumerate(lines): self._parser.reset(line, self._current_colours) for offset, command, params in self._parser.parse(): if command == Parser.DISPLAY_TEXT: # Just display the text... allowing for line wrapping. if self._cursor_x + len(params) > self._w: part_1 = params[:self._w - self._cursor_x] part_2 = params[self._w - self._cursor_x:] self._print_at(part_1, self._cursor_x, self._cursor_y) self._print_at(part_2, 0, self._cursor_y + 1) self._cursor_x = len(part_2) self._cursor_y += 1 if self._cursor_y - self._canvas.start_line >= self._h: self._canvas.scroll() else: self._print_at(params, self._cursor_x, self._cursor_y) self._cursor_x += len(params) elif command == Parser.CHANGE_COLOURS: # Change current text colours. self._current_colours = params elif command == Parser.NEXT_TAB: # Move to next tab stop - hard-coded to default of 8 characters. self._cursor_x = (self._cursor_x // 8) * 8 + 8 elif command == Parser.MOVE_RELATIVE: # Move cursor relative to current position. self._cursor_x += params[0] self._cursor_y += params[1] if self._cursor_y < self._canvas.start_line: self._canvas.scroll(self._cursor_y - self._canvas.start_line) elif command == Parser.MOVE_ABSOLUTE: # Move cursor relative to specified absolute position. if params[0] is not None: self._cursor_x = params[0] if params[1] is not None: self._cursor_y = params[1] + self._canvas.start_line elif command == Parser.DELETE_LINE: # Delete some/all of the current line. if params == 0: self._print_at(" " * (self._w - self._cursor_x), self._cursor_x, self._cursor_y) elif params == 1: self._print_at(" " * self._cursor_x, 0, self._cursor_y) elif params == 2: self._print_at(" " * self._w, 0, self._cursor_y) elif command == Parser.DELETE_CHARS: # Delete n characters under the cursor. for x in range(self._cursor_x, self._w): if x + params < self._w: cell = self._canvas.get_from(x + params, self._cursor_y) else: cell = (ord(" "), self._current_colours[0], self._current_colours[1], self._current_colours[2]) self._canvas.print_at( chr(cell[0]), x, self._cursor_y, colour=cell[1], attr=cell[2], bg=cell[3]) elif command == Parser.SHOW_CURSOR: # Show/hide the cursor. self._show_cursor = params elif command == Parser.CLEAR_SCREEN: # Clear the screen. self._canvas.clear_buffer( self._current_colours[0], self._current_colours[1], self._current_colours[2]) # Move to next line, scrolling buffer as needed. if i != len(lines) - 1: self._cursor_x = 0 self._cursor_y += 1 if self._cursor_y - self._canvas.start_line >= self._h: self._canvas.scroll() def _print_at(self, text, x, y): """ Helper function to simplify use of the canvas. """ self._canvas.print_at( text, x, y, colour=self._current_colours[0], attr=self._current_colours[1], bg=self._current_colours[2]) def _background(self): """ Backround thread running the IO between the widget and the TTY session. """ while True: ready, _, _ = select.select([self._master], [], []) for stream in ready: value = "" while True: try: data = os.read(stream, 102400) data = data.decode("utf8", "replace") value += data # Python 2 and 3 raise different exceptions when they would block except Exception: with self._lock: self._add_stream(value) self._frame.screen.force_update() break def reset(self): """ Reset the widget to a blank screen. """ self._canvas = Canvas(self._frame.canvas, self._h, self._w, x=self._x, y=self._y) self._cursor_x, self._cursor_y = 0, 0 self._current_colours = (Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK) def required_height(self, offset, width): """ Required height for the terminal. """ return self._required_height @property def frame_update_count(self): """ Frame update rate required. """ # Force refresh for cursor. return 5 @property def value(self): """ Terminal value - not needed for demo. """ return @value.setter def value(self, new_value): return