Exemple #1
0
    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)))
Exemple #2
0
 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))
Exemple #3
0
 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))
Exemple #4
0
 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"))
Exemple #5
0
 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)
Exemple #6
0
    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))
Exemple #7
0
    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)
Exemple #8
0
    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))
Exemple #9
0
    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()
Exemple #10
0
    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()
Exemple #11
0
    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"))
Exemple #12
0
 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()
Exemple #13
0
    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)))
Exemple #14
0
    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")
Exemple #15
0
    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))
Exemple #16
0
    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()
Exemple #17
0
    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"))
Exemple #18
0
    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)
Exemple #19
0
    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")
Exemple #20
0
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])
Exemple #21
0
    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)
Exemple #22
0
    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)
Exemple #23
0
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