255 lines
10 KiB
Python
255 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
This module implements renderers that play content to the screen.
|
|
"""
|
|
from __future__ import division
|
|
from __future__ import absolute_import
|
|
from __future__ import print_function
|
|
from __future__ import unicode_literals
|
|
from abc import abstractmethod
|
|
from builtins import range
|
|
import json
|
|
|
|
from asciimatics.renderers.base import DynamicRenderer
|
|
from asciimatics.screen import Screen
|
|
from asciimatics.parsers import AnsiTerminalParser, Parser
|
|
|
|
|
|
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()
|
|
|
|
@abstractmethod
|
|
def _render_now(self):
|
|
pass
|
|
|
|
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])
|
|
|
|
|
|
class AnsiArtPlayer(AbstractScreenPlayer):
|
|
"""
|
|
Renderer to play ANSI art text files.
|
|
|
|
In order to tidy up files, this must be used as a context manager (i.e. using `with`).
|
|
"""
|
|
|
|
def __init__(self, filename, height=25, width=80, encoding="cp437", strip=False, rate=2):
|
|
"""
|
|
:param filename: the file containingi the ANSI art.
|
|
:param height: required height of the renderer.
|
|
:param width: required width of the renderer.
|
|
:param encoding: text encoding ofnthe file.
|
|
:param strip: whether to strip CRLF from the file content.
|
|
:param rate: number of lines to render on each update.
|
|
"""
|
|
super(AnsiArtPlayer, self).__init__(height, width)
|
|
self._file = open(filename, "rb")
|
|
self._strip = strip
|
|
self._rate = rate
|
|
self._encoding = encoding
|
|
|
|
def __enter__(self):
|
|
"""
|
|
Create context for use as a context manager.
|
|
"""
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
"""
|
|
Clear up the resources for this context.
|
|
"""
|
|
if self._file:
|
|
self._file.close()
|
|
|
|
def _render_now(self):
|
|
count = 0
|
|
line = None
|
|
while count < self._rate and line != "":
|
|
line = self._file.readline().decode(self._encoding)
|
|
count += 1
|
|
if self._strip:
|
|
line = line.rstrip("\r\n")
|
|
self._play_content(line)
|
|
return self._plain_image, self._colour_map
|
|
|
|
|
|
class AsciinemaPlayer(AbstractScreenPlayer):
|
|
"""
|
|
Renderer to play terminal recordings created by asciinema.
|
|
|
|
This only supports the version 2 file format. Use the max_delay setting to speed up human
|
|
interactions (i.e. to reduce delays from typing).
|
|
|
|
In order to tidy up files, this must be used as a context manager (i.e. using `with`).
|
|
"""
|
|
|
|
def __init__(self, filename, height=None, width=None, max_delay=None):
|
|
"""
|
|
:param filename: the file containingi the ANSI art.
|
|
:param height: required height of the renderer.
|
|
:param width: required width of the renderer.
|
|
:param max_delay: maximum time interval (in secs) to wait between frame updates.
|
|
"""
|
|
# Open the file and check it looks plausibly like a supported format.
|
|
self._file = open(filename)
|
|
header = json.loads(self._file.readline())
|
|
if header["version"] != 2:
|
|
raise RuntimeError("Unsupported file format")
|
|
|
|
# Use file details if not overriden by constructor params.
|
|
height = height if height else header["height"]
|
|
width = width if width else header["width"]
|
|
|
|
# Construct the full player now we have all the details.
|
|
super(AsciinemaPlayer, self).__init__(height, width)
|
|
self._max_delay = max_delay
|
|
|
|
def __enter__(self):
|
|
"""
|
|
Create context for use as a context manager.
|
|
"""
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
"""
|
|
Clear up the resources for this context.
|
|
"""
|
|
if self._file:
|
|
self._file.close()
|
|
|
|
def _render_now(self):
|
|
self._counter += 0.05
|
|
if self._counter >= self._next:
|
|
if self._buffer:
|
|
self._play_content(self._buffer)
|
|
self._buffer = None
|
|
while True:
|
|
try:
|
|
self._next, _, self._buffer = json.loads(self._file.readline())
|
|
if self._next > self._counter:
|
|
# Speed up playback if requested.
|
|
if self._max_delay and self._next - self._counter > self._max_delay:
|
|
self._counter = self._next - self._max_delay
|
|
break
|
|
self._play_content(self._buffer)
|
|
except ValueError:
|
|
# Python 3 raises a subclass of this error, so will also be caught.
|
|
break
|
|
|
|
return self._plain_image, self._colour_map
|