def test_wipe(self): """ Check that Wipe works. """ # Check that Wipe clears lines going down the screen. screen = MagicMock(spec=Screen, colours=8, unicode_aware=False) canvas = Canvas(screen, 10, 40, 0, 0) effect = Wipe(canvas) effect.reset() self.assert_blank(canvas) my_buffer = [[(32, 7, 0, 0) for _ in range(40)] for _ in range(10)] for x in range(canvas.width): for y in range(canvas.height): canvas.print_at(chr(randint(1, 128)), x, y) my_buffer[y][x] = canvas.get_from(x, y) for i in range(10): effect.update(i) self.assertEqual( self.check_canvas( canvas, my_buffer, lambda value: self.assertLess(value[0], 129)), i % 2 == 0) # Check there is no stop frame by default. self.assertEqual(effect.stop_frame, 0) # This effect should ignore events. event = object() self.assertEqual(event, effect.process_event(event))
def test_mirage(self): """ Check that Mirage works. """ # Check that Mirage randomly updates the Screen every other frame. screen = MagicMock(spec=Screen, colours=8, unicode_aware=False) canvas = Canvas(screen, 10, 40, 0, 0) effect = Mirage(canvas, FigletText("hello"), 3, 1) effect.reset() effect.update(0) self.assert_blank(canvas) effect.update(1) changed = False for x in range(canvas.width): for y in range(canvas.height): if canvas.get_from(x, y) != (32, 7, 0, 0): changed = True self.assertTrue(changed) # Check there is no stop frame by default. self.assertEqual(effect.stop_frame, 0) # This effect should ignore events. event = object() self.assertEqual(event, effect.process_event(event))
def test_wipe(self): """ Check that Wipe works. """ # Check that Wipe clears lines going down the screen. screen = MagicMock(spec=Screen, colours=8, unicode_aware=False) canvas = Canvas(screen, 10, 40, 0, 0) effect = Wipe(canvas) effect.reset() self.assert_blank(canvas) my_buffer = [[(32, 7, 0, 0) for _ in range(40)] for _ in range(10)] for x in range(canvas.width): for y in range(canvas.height): canvas.print_at(chr(randint(1, 128)), x, y) my_buffer[y][x] = canvas.get_from(x, y) for i in range(10): effect.update(i) self.assertEqual(self.check_canvas( canvas, my_buffer, lambda value: self.assertLess(value[0], 129)), i % 2 == 0) # Check there is no stop frame by default. self.assertEqual(effect.stop_frame, 0) # This effect should ignore events. event = object() self.assertEqual(event, effect.process_event(event))
def test_banner(self): """ Check that BannerText works. """ # Check that banner redraws every frame. screen = MagicMock(spec=Screen, colours=8) canvas = Canvas(screen, 10, 100, 0, 0) effect = BannerText(canvas, StaticRenderer(images=["hello"]), 2, 3) effect.reset() effect.update(0) self.assertEqual(canvas.get_from(canvas.width - 1, 2), (ord("h"), 3, 0, 0)) effect.update(1) self.assertEqual(canvas.get_from(canvas.width - 1, 2), (ord("e"), 3, 0, 0)) # Check there is some stop frame - will vary according to screen width self.assertGreater(effect.stop_frame, 0)
def test_banner(self): """ Check that BannerText works. """ # Check that banner redraws every frame. screen = MagicMock(spec=Screen, colours=8, unicode_aware=False) canvas = Canvas(screen, 10, 100, 0, 0) effect = BannerText(canvas, StaticRenderer(images=["hello"]), 2, 3) effect.reset() effect.update(0) self.assertEqual(canvas.get_from(canvas.width - 1, 2), (ord("h"), 3, 0, 0)) effect.update(1) self.assertEqual(canvas.get_from(canvas.width - 1, 2), (ord("e"), 3, 0, 0)) # Check there is some stop frame - will vary according to screen width self.assertGreater(effect.stop_frame, 0) # This effect should ignore events. event = object() self.assertEqual(event, effect.process_event(event))
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
class IrohaTUITestInstance: def __init__(self): self.instance = TestIrohaTUI() self.screen = MagicMock(spec=Screen, colours=8, unicode_aware=False) self.canvas = Canvas(self.screen, 25, 80, 0, 0) self.canvas.clear = lambda: self.canvas.reset() self.instance.screen_manager = ScreenManager.from_frame( SelectorView, ModeSelectorModel, screen=self.canvas, application=self.instance ) def _update(self): for effect in self.instance.screen_manager.scene.effects: effect.update(0) def _send_raw(self, code): self.instance.screen_manager.scene.process_event(KeyboardEvent(code)) def send_codes(self, s): for c in s: self._send_raw(ord(c)) self._update() def send_tab(self, n=1): for _ in range(n): self._send_raw(Screen.KEY_TAB) self._update() def send_backspace(self, n=1): for _ in range(n): self._send_raw(Screen.KEY_BACK) self._update() def send_enter(self, n=1): for _ in range(n): self._send_raw("\r") self._update() def send(self, *sequence): new_sequence = [] for i in sequence: if isinstance(i, tuple): new_sequence.extend([i[0]] * i[1]) else: new_sequence.append(i) for i in new_sequence: if isinstance(i, str): code = getattr(Screen, "KEY_" + i.upper(), None) if code: self._send_raw(code) else: self.send_codes(i) elif isinstance(i, int): self._send_raw(code) else: raise ValueError(f"Wrong type: {i}") def expect(self, s): for y in range(self.canvas.height): chars = [] for x in range(self.canvas.width): char, _, _, _ = self.canvas.get_from(x, y) chars.append(chr(char)) if s in "".join(chars): return x, y self.dump_canvas() raise ValueError def dump_canvas(self): good_colors = {"grey", "red", "green", "yellow", "blue", "magenta", "cyan", "white"} print("-" * 80) for y in range(self.canvas.height): for x in range(self.canvas.width): char, fg, _, bg = self.canvas.get_from(x, y) char = chr(char) fg = COLORS[fg] bg = COLORS[bg] args = {} if fg in good_colors: args["color"] = fg if bg in good_colors: args["on_color"] = f"on_{bg}" print(colored(char, **args), end='') print("\r") print("-" * 80)