mirror of
https://github.com/cyberboy666/r_e_c_u_r.git
synced 2025-12-12 19:30:11 +01:00
2122 lines
77 KiB
Python
2122 lines
77 KiB
Python
# -*- coding: utf-8 -*-
|
|
from __future__ import division
|
|
from __future__ import absolute_import
|
|
from __future__ import print_function
|
|
from __future__ import unicode_literals
|
|
from past.builtins import basestring
|
|
from locale import getlocale, getdefaultlocale
|
|
import struct
|
|
from builtins import object
|
|
from builtins import range
|
|
from builtins import str
|
|
from builtins import ord
|
|
from builtins import chr
|
|
from math import sqrt
|
|
from future.utils import with_metaclass
|
|
import time
|
|
from abc import ABCMeta, abstractmethod
|
|
import json
|
|
import sys
|
|
import signal
|
|
from asciimatics.event import KeyboardEvent, MouseEvent
|
|
from asciimatics.exceptions import ResizeScreenError, StopApplication, NextScene
|
|
from wcwidth import wcwidth, wcswidth
|
|
|
|
# Logging
|
|
from logging import getLogger
|
|
logger = getLogger(__name__)
|
|
|
|
# Looks like pywin32 is missing some Windows constants
|
|
ENABLE_EXTENDED_FLAGS = 0x0080
|
|
ENABLE_QUICK_EDIT_MODE = 0x0040
|
|
|
|
|
|
class _AbstractCanvas(with_metaclass(ABCMeta, object)):
|
|
"""
|
|
Abstract class to handle screen buffering.
|
|
"""
|
|
|
|
# Characters for anti-aliasing line drawing.
|
|
_line_chars = " ''^.|/7.\\|Ywbd#"
|
|
_uni_line_chars = " ▘▝▀▖▌▞▛▗▚▐▜▄▙▟█"
|
|
|
|
# Colour palette for 8/16 colour terminals
|
|
_8_palette = [
|
|
0x00, 0x00, 0x00,
|
|
0x80, 0x00, 0x00,
|
|
0x00, 0x80, 0x00,
|
|
0x80, 0x80, 0x00,
|
|
0x00, 0x00, 0x80,
|
|
0x80, 0x00, 0x80,
|
|
0x00, 0x80, 0x80,
|
|
0xc0, 0xc0, 0xc0,
|
|
] + [0x00 for _ in range(248 * 3)]
|
|
|
|
# Colour palette for 256 colour terminals
|
|
_256_palette = [
|
|
0x00, 0x00, 0x00,
|
|
0x80, 0x00, 0x00,
|
|
0x00, 0x80, 0x00,
|
|
0x80, 0x80, 0x00,
|
|
0x00, 0x00, 0x80,
|
|
0x80, 0x00, 0x80,
|
|
0x00, 0x80, 0x80,
|
|
0xc0, 0xc0, 0xc0,
|
|
0x80, 0x80, 0x80,
|
|
0xff, 0x00, 0x00,
|
|
0x00, 0xff, 0x00,
|
|
0xff, 0xff, 0x00,
|
|
0x00, 0x00, 0xff,
|
|
0xff, 0x00, 0xff,
|
|
0x00, 0xff, 0xff,
|
|
0xff, 0xff, 0xff,
|
|
0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x5f,
|
|
0x00, 0x00, 0x87,
|
|
0x00, 0x00, 0xaf,
|
|
0x00, 0x00, 0xd7,
|
|
0x00, 0x00, 0xff,
|
|
0x00, 0x5f, 0x00,
|
|
0x00, 0x5f, 0x5f,
|
|
0x00, 0x5f, 0x87,
|
|
0x00, 0x5f, 0xaf,
|
|
0x00, 0x5f, 0xd7,
|
|
0x00, 0x5f, 0xff,
|
|
0x00, 0x87, 0x00,
|
|
0x00, 0x87, 0x5f,
|
|
0x00, 0x87, 0x87,
|
|
0x00, 0x87, 0xaf,
|
|
0x00, 0x87, 0xd7,
|
|
0x00, 0x87, 0xff,
|
|
0x00, 0xaf, 0x00,
|
|
0x00, 0xaf, 0x5f,
|
|
0x00, 0xaf, 0x87,
|
|
0x00, 0xaf, 0xaf,
|
|
0x00, 0xaf, 0xd7,
|
|
0x00, 0xaf, 0xff,
|
|
0x00, 0xd7, 0x00,
|
|
0x00, 0xd7, 0x5f,
|
|
0x00, 0xd7, 0x87,
|
|
0x00, 0xd7, 0xaf,
|
|
0x00, 0xd7, 0xd7,
|
|
0x00, 0xd7, 0xff,
|
|
0x00, 0xff, 0x00,
|
|
0x00, 0xff, 0x5f,
|
|
0x00, 0xff, 0x87,
|
|
0x00, 0xff, 0xaf,
|
|
0x00, 0xff, 0xd7,
|
|
0x00, 0xff, 0xff,
|
|
0x5f, 0x00, 0x00,
|
|
0x5f, 0x00, 0x5f,
|
|
0x5f, 0x00, 0x87,
|
|
0x5f, 0x00, 0xaf,
|
|
0x5f, 0x00, 0xd7,
|
|
0x5f, 0x00, 0xff,
|
|
0x5f, 0x5f, 0x00,
|
|
0x5f, 0x5f, 0x5f,
|
|
0x5f, 0x5f, 0x87,
|
|
0x5f, 0x5f, 0xaf,
|
|
0x5f, 0x5f, 0xd7,
|
|
0x5f, 0x5f, 0xff,
|
|
0x5f, 0x87, 0x00,
|
|
0x5f, 0x87, 0x5f,
|
|
0x5f, 0x87, 0x87,
|
|
0x5f, 0x87, 0xaf,
|
|
0x5f, 0x87, 0xd7,
|
|
0x5f, 0x87, 0xff,
|
|
0x5f, 0xaf, 0x00,
|
|
0x5f, 0xaf, 0x5f,
|
|
0x5f, 0xaf, 0x87,
|
|
0x5f, 0xaf, 0xaf,
|
|
0x5f, 0xaf, 0xd7,
|
|
0x5f, 0xaf, 0xff,
|
|
0x5f, 0xd7, 0x00,
|
|
0x5f, 0xd7, 0x5f,
|
|
0x5f, 0xd7, 0x87,
|
|
0x5f, 0xd7, 0xaf,
|
|
0x5f, 0xd7, 0xd7,
|
|
0x5f, 0xd7, 0xff,
|
|
0x5f, 0xff, 0x00,
|
|
0x5f, 0xff, 0x5f,
|
|
0x5f, 0xff, 0x87,
|
|
0x5f, 0xff, 0xaf,
|
|
0x5f, 0xff, 0xd7,
|
|
0x5f, 0xff, 0xff,
|
|
0x87, 0x00, 0x00,
|
|
0x87, 0x00, 0x5f,
|
|
0x87, 0x00, 0x87,
|
|
0x87, 0x00, 0xaf,
|
|
0x87, 0x00, 0xd7,
|
|
0x87, 0x00, 0xff,
|
|
0x87, 0x5f, 0x00,
|
|
0x87, 0x5f, 0x5f,
|
|
0x87, 0x5f, 0x87,
|
|
0x87, 0x5f, 0xaf,
|
|
0x87, 0x5f, 0xd7,
|
|
0x87, 0x5f, 0xff,
|
|
0x87, 0x87, 0x00,
|
|
0x87, 0x87, 0x5f,
|
|
0x87, 0x87, 0x87,
|
|
0x87, 0x87, 0xaf,
|
|
0x87, 0x87, 0xd7,
|
|
0x87, 0x87, 0xff,
|
|
0x87, 0xaf, 0x00,
|
|
0x87, 0xaf, 0x5f,
|
|
0x87, 0xaf, 0x87,
|
|
0x87, 0xaf, 0xaf,
|
|
0x87, 0xaf, 0xd7,
|
|
0x87, 0xaf, 0xff,
|
|
0x87, 0xd7, 0x00,
|
|
0x87, 0xd7, 0x5f,
|
|
0x87, 0xd7, 0x87,
|
|
0x87, 0xd7, 0xaf,
|
|
0x87, 0xd7, 0xd7,
|
|
0x87, 0xd7, 0xff,
|
|
0x87, 0xff, 0x00,
|
|
0x87, 0xff, 0x5f,
|
|
0x87, 0xff, 0x87,
|
|
0x87, 0xff, 0xaf,
|
|
0x87, 0xff, 0xd7,
|
|
0x87, 0xff, 0xff,
|
|
0xaf, 0x00, 0x00,
|
|
0xaf, 0x00, 0x5f,
|
|
0xaf, 0x00, 0x87,
|
|
0xaf, 0x00, 0xaf,
|
|
0xaf, 0x00, 0xd7,
|
|
0xaf, 0x00, 0xff,
|
|
0xaf, 0x5f, 0x00,
|
|
0xaf, 0x5f, 0x5f,
|
|
0xaf, 0x5f, 0x87,
|
|
0xaf, 0x5f, 0xaf,
|
|
0xaf, 0x5f, 0xd7,
|
|
0xaf, 0x5f, 0xff,
|
|
0xaf, 0x87, 0x00,
|
|
0xaf, 0x87, 0x5f,
|
|
0xaf, 0x87, 0x87,
|
|
0xaf, 0x87, 0xaf,
|
|
0xaf, 0x87, 0xd7,
|
|
0xaf, 0x87, 0xff,
|
|
0xaf, 0xaf, 0x00,
|
|
0xaf, 0xaf, 0x5f,
|
|
0xaf, 0xaf, 0x87,
|
|
0xaf, 0xaf, 0xaf,
|
|
0xaf, 0xaf, 0xd7,
|
|
0xaf, 0xaf, 0xff,
|
|
0xaf, 0xd7, 0x00,
|
|
0xaf, 0xd7, 0x5f,
|
|
0xaf, 0xd7, 0x87,
|
|
0xaf, 0xd7, 0xaf,
|
|
0xaf, 0xd7, 0xd7,
|
|
0xaf, 0xd7, 0xff,
|
|
0xaf, 0xff, 0x00,
|
|
0xaf, 0xff, 0x5f,
|
|
0xaf, 0xff, 0x87,
|
|
0xaf, 0xff, 0xaf,
|
|
0xaf, 0xff, 0xd7,
|
|
0xaf, 0xff, 0xff,
|
|
0xd7, 0x00, 0x00,
|
|
0xd7, 0x00, 0x5f,
|
|
0xd7, 0x00, 0x87,
|
|
0xd7, 0x00, 0xaf,
|
|
0xd7, 0x00, 0xd7,
|
|
0xd7, 0x00, 0xff,
|
|
0xd7, 0x5f, 0x00,
|
|
0xd7, 0x5f, 0x5f,
|
|
0xd7, 0x5f, 0x87,
|
|
0xd7, 0x5f, 0xaf,
|
|
0xd7, 0x5f, 0xd7,
|
|
0xd7, 0x5f, 0xff,
|
|
0xd7, 0x87, 0x00,
|
|
0xd7, 0x87, 0x5f,
|
|
0xd7, 0x87, 0x87,
|
|
0xd7, 0x87, 0xaf,
|
|
0xd7, 0x87, 0xd7,
|
|
0xd7, 0x87, 0xff,
|
|
0xd7, 0xaf, 0x00,
|
|
0xd7, 0xaf, 0x5f,
|
|
0xd7, 0xaf, 0x87,
|
|
0xd7, 0xaf, 0xaf,
|
|
0xd7, 0xaf, 0xd7,
|
|
0xd7, 0xaf, 0xff,
|
|
0xd7, 0xd7, 0x00,
|
|
0xd7, 0xd7, 0x5f,
|
|
0xd7, 0xd7, 0x87,
|
|
0xd7, 0xd7, 0xaf,
|
|
0xd7, 0xd7, 0xd7,
|
|
0xd7, 0xd7, 0xff,
|
|
0xd7, 0xff, 0x00,
|
|
0xd7, 0xff, 0x5f,
|
|
0xd7, 0xff, 0x87,
|
|
0xd7, 0xff, 0xaf,
|
|
0xd7, 0xff, 0xd7,
|
|
0xd7, 0xff, 0xff,
|
|
0xff, 0x00, 0x00,
|
|
0xff, 0x00, 0x5f,
|
|
0xff, 0x00, 0x87,
|
|
0xff, 0x00, 0xaf,
|
|
0xff, 0x00, 0xd7,
|
|
0xff, 0x00, 0xff,
|
|
0xff, 0x5f, 0x00,
|
|
0xff, 0x5f, 0x5f,
|
|
0xff, 0x5f, 0x87,
|
|
0xff, 0x5f, 0xaf,
|
|
0xff, 0x5f, 0xd7,
|
|
0xff, 0x5f, 0xff,
|
|
0xff, 0x87, 0x00,
|
|
0xff, 0x87, 0x5f,
|
|
0xff, 0x87, 0x87,
|
|
0xff, 0x87, 0xaf,
|
|
0xff, 0x87, 0xd7,
|
|
0xff, 0x87, 0xff,
|
|
0xff, 0xaf, 0x00,
|
|
0xff, 0xaf, 0x5f,
|
|
0xff, 0xaf, 0x87,
|
|
0xff, 0xaf, 0xaf,
|
|
0xff, 0xaf, 0xd7,
|
|
0xff, 0xaf, 0xff,
|
|
0xff, 0xd7, 0x00,
|
|
0xff, 0xd7, 0x5f,
|
|
0xff, 0xd7, 0x87,
|
|
0xff, 0xd7, 0xaf,
|
|
0xff, 0xd7, 0xd7,
|
|
0xff, 0xd7, 0xff,
|
|
0xff, 0xff, 0x00,
|
|
0xff, 0xff, 0x5f,
|
|
0xff, 0xff, 0x87,
|
|
0xff, 0xff, 0xaf,
|
|
0xff, 0xff, 0xd7,
|
|
0xff, 0xff, 0xff,
|
|
0x08, 0x08, 0x08,
|
|
0x12, 0x12, 0x12,
|
|
0x1c, 0x1c, 0x1c,
|
|
0x26, 0x26, 0x26,
|
|
0x30, 0x30, 0x30,
|
|
0x3a, 0x3a, 0x3a,
|
|
0x44, 0x44, 0x44,
|
|
0x4e, 0x4e, 0x4e,
|
|
0x58, 0x58, 0x58,
|
|
0x62, 0x62, 0x62,
|
|
0x6c, 0x6c, 0x6c,
|
|
0x76, 0x76, 0x76,
|
|
0x80, 0x80, 0x80,
|
|
0x8a, 0x8a, 0x8a,
|
|
0x94, 0x94, 0x94,
|
|
0x9e, 0x9e, 0x9e,
|
|
0xa8, 0xa8, 0xa8,
|
|
0xb2, 0xb2, 0xb2,
|
|
0xbc, 0xbc, 0xbc,
|
|
0xc6, 0xc6, 0xc6,
|
|
0xd0, 0xd0, 0xd0,
|
|
0xda, 0xda, 0xda,
|
|
0xe4, 0xe4, 0xe4,
|
|
0xee, 0xee, 0xee,
|
|
]
|
|
|
|
def __init__(self, height, width, buffer_height, colours, unicode_aware):
|
|
"""
|
|
:param height: The buffer height for this object.
|
|
:param width: The buffer width for this object.
|
|
:param buffer_height: The buffer height for this object.
|
|
:param colours: Number of colours for this object.
|
|
:param unicode_aware: Force use of unicode options for this object.
|
|
"""
|
|
super(_AbstractCanvas, self).__init__()
|
|
|
|
# Can we handle unicode environments?
|
|
self._unicode_aware = unicode_aware
|
|
|
|
# Create screen buffers.
|
|
self.height = height
|
|
self.width = width
|
|
self.colours = colours
|
|
self._buffer_height = buffer_height
|
|
self._screen_buffer = None
|
|
self._double_buffer = None
|
|
self._start_line = 0
|
|
self._x = 0
|
|
self._y = 0
|
|
|
|
# dictionary cache for colour blending
|
|
self._blends = {}
|
|
|
|
# Reset the screen ready to go...
|
|
self.reset()
|
|
|
|
def reset(self):
|
|
"""
|
|
Reset the internal buffers for the abstract canvas.
|
|
"""
|
|
# Reset our screen buffer
|
|
self._start_line = 0
|
|
self._x = self._y = None
|
|
line = [(u" ", Screen.COLOUR_WHITE, 0, 0, 1) for _ in range(self.width)]
|
|
|
|
# Note that we use json to duplicate the data as copy.deepcopy is an
|
|
# order of magnitude slower.
|
|
self._screen_buffer = [
|
|
json.loads(json.dumps(line)) for _ in range(self._buffer_height)]
|
|
self._double_buffer = json.loads(json.dumps(self._screen_buffer))
|
|
self._reset()
|
|
|
|
def scroll(self):
|
|
"""
|
|
Scroll the abstract canvas up one line.
|
|
"""
|
|
self._start_line += 1
|
|
|
|
def scroll_to(self, line):
|
|
"""
|
|
Scroll the abstract canvas to make a specific line.
|
|
|
|
:param line: The line to scroll to.
|
|
"""
|
|
self._start_line = line
|
|
|
|
@abstractmethod
|
|
def _reset(self):
|
|
"""
|
|
Internal implementation required to reset underlying drawing
|
|
interface.
|
|
"""
|
|
|
|
@abstractmethod
|
|
def refresh(self):
|
|
"""
|
|
Refresh this object - this will draw to the underlying display
|
|
interface.
|
|
"""
|
|
|
|
def get_from(self, x, y):
|
|
"""
|
|
Get the character at the specified location.
|
|
|
|
:param x: The column (x coord) of the character.
|
|
:param y: The row (y coord) of the character.
|
|
|
|
:return: A 4-tuple of (ascii code, foreground, attributes, background)
|
|
for the character at the location.
|
|
"""
|
|
if y < 0 or y >= self._buffer_height or x < 0 or x >= self.width:
|
|
return None
|
|
cell = self._double_buffer[y][x]
|
|
return ord(cell[0]), cell[1], cell[2], cell[3]
|
|
|
|
def print_at(self, text, x, y, colour=7, attr=0, bg=0, transparent=False):
|
|
"""
|
|
Print the text at the specified location using the
|
|
specified colour and attributes.
|
|
|
|
:param text: The (single line) text to be printed.
|
|
:param x: The column (x coord) for the start of the text.
|
|
:param y: The line (y coord) for the start of the text.
|
|
:param colour: The colour of the text to be displayed.
|
|
:param attr: The cell attribute of the text to be displayed.
|
|
:param bg: The background colour of the text to be displayed.
|
|
:param transparent: Whether to print spaces or not, thus giving a
|
|
transparent effect.
|
|
|
|
The colours and attributes are the COLOUR_xxx and A_yyy constants
|
|
defined in the Screen class.
|
|
"""
|
|
# Trim text to the buffer vertically. Don't trim horizontally as we don't know whether any
|
|
# of these characters are dual-width yet. Handle it on the fly below...
|
|
if y < 0 or y >= self._buffer_height or x > self.width:
|
|
return
|
|
|
|
if len(text) > 0:
|
|
j = 0
|
|
for i, c in enumerate(text):
|
|
# Handle under-run and overrun of double-width glyphs now.
|
|
width = wcwidth(c)
|
|
if x + i + j < 0:
|
|
x += (width - 1)
|
|
continue
|
|
if x + i + j + width > self.width:
|
|
return
|
|
|
|
# Now handle the update.
|
|
if c != " " or not transparent:
|
|
# Make sure that we populate the second character correctly for double-width
|
|
# glyphs. This ensures that if the glyph gets overwritten with a normal width
|
|
# it will clear both cells in the refresh.
|
|
self._double_buffer[y][x + i + j] = (str(c), colour, attr, bg, width)
|
|
if self._double_buffer[y][x + i + j][4] == 2:
|
|
j += 1
|
|
if x + i + j < self.width:
|
|
self._double_buffer[y][x + i + j] = (str(c), colour, attr, bg, 0)
|
|
|
|
@property
|
|
def start_line(self):
|
|
"""
|
|
:return: The start line of the top of the canvas.
|
|
"""
|
|
return self._start_line
|
|
|
|
@property
|
|
def unicode_aware(self):
|
|
"""
|
|
:return: Whether unicode input/output is supported or not.
|
|
"""
|
|
return self._unicode_aware
|
|
|
|
@property
|
|
def dimensions(self):
|
|
"""
|
|
:return: The full dimensions of the canvas as a (height, width) tuple.
|
|
"""
|
|
return self.height, self.width
|
|
|
|
@property
|
|
def palette(self):
|
|
"""
|
|
:return: A palette compatible with the PIL.
|
|
"""
|
|
if self.colours < 256:
|
|
# Use the ANSI colour set.
|
|
return self._8_palette
|
|
else:
|
|
return self._256_palette
|
|
|
|
def centre(self, text, y, colour=7, attr=0, colour_map=None):
|
|
"""
|
|
Centre the text on the specified line (y) using the optional
|
|
colour and attributes.
|
|
|
|
:param text: The (single line) text to be printed.
|
|
:param y: The line (y coord) for the start of the text.
|
|
:param colour: The colour of the text to be displayed.
|
|
:param attr: The cell attribute of the text to be displayed.
|
|
:param colour_map: Colour/attribute list for multi-colour text.
|
|
|
|
The colours and attributes are the COLOUR_xxx and A_yyy constants
|
|
defined in the Screen class.
|
|
"""
|
|
x = (self.width - wcswidth(text)) // 2
|
|
self.paint(text, x, y, colour, attr, colour_map=colour_map)
|
|
|
|
def paint(self, text, x, y, colour=7, attr=0, bg=0, transparent=False,
|
|
colour_map=None):
|
|
"""
|
|
Paint multi-colour text at the defined location.
|
|
|
|
:param text: The (single line) text to be printed.
|
|
:param x: The column (x coord) for the start of the text.
|
|
:param y: The line (y coord) for the start of the text.
|
|
:param colour: The default colour of the text to be displayed.
|
|
:param attr: The default cell attribute of the text to be displayed.
|
|
:param bg: The default background colour of the text to be displayed.
|
|
:param transparent: Whether to print spaces or not, thus giving a
|
|
transparent effect.
|
|
:param colour_map: Colour/attribute list for multi-colour text.
|
|
|
|
The colours and attributes are the COLOUR_xxx and A_yyy constants
|
|
defined in the Screen class.
|
|
colour_map is a list of tuples (foreground, attribute, background) that
|
|
must be the same length as the passed in text (or None if no mapping is
|
|
required).
|
|
"""
|
|
if colour_map is None:
|
|
self.print_at(text, x, y, colour, attr, bg, transparent)
|
|
else:
|
|
for i, c in enumerate(text):
|
|
if len(colour_map[i]) > 0 and colour_map[i][0] is not None:
|
|
colour = colour_map[i][0]
|
|
if len(colour_map[i]) > 1 and colour_map[i][1] is not None:
|
|
attr = colour_map[i][1]
|
|
if len(colour_map[i]) > 2 and colour_map[i][2] is not None:
|
|
bg = colour_map[i][2]
|
|
self.print_at(c, x + i, y, colour, attr, bg, transparent)
|
|
|
|
def _blend(self, new, old, ratio):
|
|
"""
|
|
Blend the new colour with the old according to the ratio.
|
|
|
|
:param new: The new colour (or None if not required).
|
|
:param old: The old colour.
|
|
:param ratio: The ratio to blend new and old
|
|
:returns: the new colour index to use for the required blend.
|
|
"""
|
|
# Don't bother blending if none is required.
|
|
if new is None:
|
|
return old
|
|
|
|
# Check colour blend cache for a quick answer.
|
|
key = (min(new, old), max(new, old))
|
|
if key in self._blends:
|
|
return self._blends[key]
|
|
|
|
# No quick answer - do it the long way... First lookup the RGB values
|
|
# for both colours and blend.
|
|
(r1, g1, b1) = self.palette[new * 3:new * 3 + 3]
|
|
(r2, g2, b2) = self.palette[old * 3:old * 3 + 3]
|
|
|
|
# Helper function to blend RGB values.
|
|
def f(c1, c2):
|
|
return ((c1 * ratio) + (c2 * (100 - ratio))) // 100
|
|
|
|
r = f(r1, r2)
|
|
g = f(g1, g2)
|
|
b = f(b1, b2)
|
|
|
|
# Now do the reverse lookup...
|
|
nearest = (256 ** 2) * 3
|
|
match = 0
|
|
for c in range(self.colours):
|
|
(rc, gc, bc) = self.palette[c * 3:c * 3 + 3]
|
|
diff = sqrt(((rc - r) * 0.3) ** 2 + ((gc - g) * 0.59) ** 2 +
|
|
((bc - b) * 0.11) ** 2)
|
|
if diff < nearest:
|
|
nearest = diff
|
|
match = c
|
|
|
|
# Save off the answer and return it
|
|
self._blends[key] = match
|
|
return match
|
|
|
|
def highlight(self, x, y, w, h, fg=None, bg=None, blend=100):
|
|
"""
|
|
Highlight a specified section of the screen.
|
|
|
|
:param x: The column (x coord) for the start of the highlight.
|
|
:param y: The line (y coord) for the start of the highlight.
|
|
:param w: The width of the highlight (in characters).
|
|
:param h: The height of the highlight (in characters).
|
|
:param fg: The foreground colour of the highlight.
|
|
:param bg: The background colour of the highlight.
|
|
:param blend: How much (as a percentage) to take of the new colour
|
|
when blending.
|
|
|
|
The colours and attributes are the COLOUR_xxx and A_yyy constants
|
|
defined in the Screen class. If fg or bg are None that means don't
|
|
change the foreground/background as appropriate.
|
|
"""
|
|
for i in range(w):
|
|
if x + i >= self.width or x + i < 0:
|
|
continue
|
|
|
|
for j in range(h):
|
|
if y + j >= self._buffer_height or y + j < 0:
|
|
continue
|
|
|
|
old = self._double_buffer[y + j][x + i]
|
|
new_bg = self._blend(bg, old[3], blend)
|
|
new_fg = self._blend(fg, old[1], blend)
|
|
self._double_buffer[y + j][x + i] = \
|
|
(old[0], new_fg, old[2], new_bg, old[4])
|
|
|
|
def is_visible(self, x, y):
|
|
"""
|
|
Return whether the specified location is on the visible screen.
|
|
|
|
:param x: The column (x coord) for the location to check.
|
|
:param y: The line (y coord) for the location to check.
|
|
"""
|
|
return ((x >= 0) and
|
|
(x <= self.width) and
|
|
(y >= self._start_line) and
|
|
(y < self._start_line + self.height))
|
|
|
|
def move(self, x, y):
|
|
"""
|
|
Move the drawing cursor to the specified position.
|
|
|
|
:param x: The column (x coord) for the location to check.
|
|
:param y: The line (y coord) for the location to check.
|
|
"""
|
|
self._x = int(round(x, 1)) * 2
|
|
self._y = int(round(y, 1)) * 2
|
|
|
|
def draw(self, x, y, char=None, colour=7, bg=0, thin=False):
|
|
"""
|
|
Draw a line from drawing cursor to the specified position. This uses a
|
|
modified Bressenham algorithm, interpolating twice as many points to
|
|
render down to anti-aliased characters when no character is specified,
|
|
or uses standard algorithm plotting with the specified character.
|
|
|
|
:param x: The column (x coord) for the location to check.
|
|
:param y: The line (y coord) for the location to check.
|
|
:param char: Optional character to use to draw the line.
|
|
:param colour: Optional colour for plotting the line.
|
|
:param bg: Optional background colour for plotting the line.
|
|
:param thin: Optional width of anti-aliased line.
|
|
"""
|
|
# Decide what type of line drawing to use.
|
|
line_chars = (self._uni_line_chars if self._unicode_aware else
|
|
self._line_chars)
|
|
|
|
# Define line end points.
|
|
x0 = self._x
|
|
y0 = self._y
|
|
x1 = int(round(x * 2, 0))
|
|
y1 = int(round(y * 2, 0))
|
|
|
|
# Remember last point for next line.
|
|
self._x = x1
|
|
self._y = y1
|
|
|
|
dx = abs(x1 - x0)
|
|
dy = abs(y1 - y0)
|
|
|
|
sx = -1 if x0 > x1 else 1
|
|
sy = -1 if y0 > y1 else 1
|
|
|
|
def _get_start_char(cx, cy):
|
|
needle = self.get_from(cx, cy)
|
|
if needle is not None:
|
|
letter, cfg, _, cbg = needle
|
|
if colour == cfg and bg == cbg and chr(letter) in line_chars:
|
|
return line_chars.find(chr(letter))
|
|
return 0
|
|
|
|
def _draw_on_x(ix, iy):
|
|
err = dx
|
|
px = ix - 2
|
|
py = iy - 2
|
|
next_char = 0
|
|
while ix != x1:
|
|
if ix < px or ix - px >= 2 or iy < py or iy - py >= 2:
|
|
px = ix & ~1
|
|
py = iy & ~1
|
|
next_char = _get_start_char(px // 2, py // 2)
|
|
next_char |= 2 ** abs(ix % 2) * 4 ** (iy % 2)
|
|
err -= 2 * dy
|
|
if err < 0:
|
|
iy += sy
|
|
err += 2 * dx
|
|
ix += sx
|
|
|
|
if char is None:
|
|
self.print_at(line_chars[next_char],
|
|
px // 2, py // 2, colour, bg=bg)
|
|
else:
|
|
self.print_at(char, px // 2, py // 2, colour, bg=bg)
|
|
|
|
def _draw_on_y(ix, iy):
|
|
err = dy
|
|
px = ix - 2
|
|
py = iy - 2
|
|
next_char = 0
|
|
while iy != y1:
|
|
if ix < px or ix - px >= 2 or iy < py or iy - py >= 2:
|
|
px = ix & ~1
|
|
py = iy & ~1
|
|
next_char = _get_start_char(px // 2, py // 2)
|
|
next_char |= 2 ** abs(ix % 2) * 4 ** (iy % 2)
|
|
err -= 2 * dx
|
|
if err < 0:
|
|
ix += sx
|
|
err += 2 * dy
|
|
iy += sy
|
|
|
|
if char is None:
|
|
self.print_at(line_chars[next_char],
|
|
px // 2, py // 2, colour, bg=bg)
|
|
else:
|
|
self.print_at(char, px // 2, py // 2, colour, bg=bg)
|
|
if dx > dy:
|
|
_draw_on_x(x0, y0)
|
|
if not thin:
|
|
_draw_on_x(x0, y0 + 1)
|
|
else:
|
|
_draw_on_y(x0, y0)
|
|
if not thin:
|
|
_draw_on_y(x0 + 1, y0)
|
|
|
|
|
|
class Canvas(_AbstractCanvas):
|
|
"""
|
|
A Canvas is an object that can be used to draw to the screen. It maintains
|
|
its own buffer that will be flushed to the screen when `refresh()` is
|
|
called.
|
|
"""
|
|
|
|
def __init__(self, screen, height, width, x=None, y=None):
|
|
"""
|
|
:param screen: The underlying Screen that will be drawn to on refresh.
|
|
:param height: The height of the screen buffer to be used.
|
|
:param width: The width of the screen buffer to be used.
|
|
:param x: The x position for the top left corner of the Canvas.
|
|
:param y: The y position for the top left corner of the Canvas.
|
|
|
|
If either of the x or y positions is not set, the Canvas will default
|
|
to centring within the current Screen for that location.
|
|
"""
|
|
# Save off the screen details.
|
|
# TODO: Fix up buffer logic once and for all!
|
|
super(Canvas, self).__init__(
|
|
height, width, 200, screen.colours, screen.unicode_aware)
|
|
self._screen = screen
|
|
self._dx = (screen.width - width) // 2 if x is None else x
|
|
self._dy = (screen.height - height) // 2 if y is None else y
|
|
|
|
def refresh(self):
|
|
"""
|
|
Flush the canvas content to the underlying screen.
|
|
"""
|
|
for y in range(self.height):
|
|
for x in range(self.width):
|
|
c = self._double_buffer[y + self._start_line][x]
|
|
if c[4] != 0:
|
|
self._screen.print_at(c[0], x + self._dx, y + self._dy, c[1], c[2], c[3])
|
|
|
|
def _reset(self):
|
|
# Nothing needed for a Canvas
|
|
pass
|
|
|
|
@property
|
|
def origin(self):
|
|
"""
|
|
The location of top left corner of the canvas on the Screen.
|
|
|
|
:returns: A tuple (x, y) of the location
|
|
"""
|
|
return self._dx, self._dy
|
|
|
|
|
|
class Screen(with_metaclass(ABCMeta, _AbstractCanvas)):
|
|
"""
|
|
Class to track basic state of the screen. This constructs the necessary
|
|
resources to allow us to do the ASCII animations.
|
|
|
|
This is an abstract class that will build the correct concrete class for
|
|
you when you call :py:meth:`.wrapper`. If needed, you can use the
|
|
:py:meth:`~.Screen.open` and :py:meth:`~.Screen.close` methods for finer
|
|
grained control of the construction and tidy up.
|
|
|
|
Note that you need to define the required height for your screen buffer.
|
|
This is important if you plan on using any Effects that will scroll the
|
|
screen vertically (e.g. Scroll). It must be big enough to handle the
|
|
full scrolling of your selected Effect.
|
|
"""
|
|
|
|
# Text attributes for use when printing to the Screen.
|
|
A_BOLD = 1
|
|
A_NORMAL = 2
|
|
A_REVERSE = 3
|
|
A_UNDERLINE = 4
|
|
|
|
# Text colours for use when printing to the Screen.
|
|
COLOUR_BLACK = 0
|
|
COLOUR_RED = 1
|
|
COLOUR_GREEN = 2
|
|
COLOUR_YELLOW = 3
|
|
COLOUR_BLUE = 4
|
|
COLOUR_MAGENTA = 5
|
|
COLOUR_CYAN = 6
|
|
COLOUR_WHITE = 7
|
|
|
|
# Standard extended key codes.
|
|
KEY_ESCAPE = -1
|
|
KEY_F1 = -2
|
|
KEY_F2 = -3
|
|
KEY_F3 = -4
|
|
KEY_F4 = -5
|
|
KEY_F5 = -6
|
|
KEY_F6 = -7
|
|
KEY_F7 = -8
|
|
KEY_F8 = -9
|
|
KEY_F9 = -10
|
|
KEY_F10 = -11
|
|
KEY_F11 = -12
|
|
KEY_F12 = -13
|
|
KEY_F13 = -14
|
|
KEY_F14 = -15
|
|
KEY_F15 = -16
|
|
KEY_F16 = -17
|
|
KEY_F17 = -18
|
|
KEY_F18 = -19
|
|
KEY_F19 = -20
|
|
KEY_F20 = -21
|
|
KEY_F21 = -22
|
|
KEY_F22 = -23
|
|
KEY_F23 = -24
|
|
KEY_F24 = -25
|
|
KEY_PRINT_SCREEN = -100
|
|
KEY_INSERT = -101
|
|
KEY_DELETE = -102
|
|
KEY_HOME = -200
|
|
KEY_END = -201
|
|
KEY_LEFT = -203
|
|
KEY_UP = -204
|
|
KEY_RIGHT = -205
|
|
KEY_DOWN = -206
|
|
KEY_PAGE_UP = -207
|
|
KEY_PAGE_DOWN = -208
|
|
KEY_BACK = -300
|
|
KEY_TAB = -301
|
|
KEY_BACK_TAB = -302
|
|
KEY_NUMPAD0 = -400
|
|
KEY_NUMPAD1 = -401
|
|
KEY_NUMPAD2 = -402
|
|
KEY_NUMPAD3 = -403
|
|
KEY_NUMPAD4 = -404
|
|
KEY_NUMPAD5 = -405
|
|
KEY_NUMPAD6 = -406
|
|
KEY_NUMPAD7 = -407
|
|
KEY_NUMPAD8 = -408
|
|
KEY_NUMPAD9 = -409
|
|
KEY_MULTIPLY = -410
|
|
KEY_ADD = -411
|
|
KEY_SUBTRACT = -412
|
|
KEY_DECIMAL = -413
|
|
KEY_DIVIDE = -414
|
|
KEY_CAPS_LOCK = -500
|
|
KEY_NUM_LOCK = -501
|
|
KEY_SCROLL_LOCK = -502
|
|
KEY_SHIFT = -600
|
|
KEY_CONTROL = -601
|
|
KEY_MENU = -602
|
|
|
|
def __init__(self, height, width, buffer_height, unicode_aware):
|
|
"""
|
|
Don't call this constructor directly.
|
|
"""
|
|
super(Screen, self).__init__(
|
|
height, width, buffer_height, 0, unicode_aware)
|
|
|
|
# Initialize base class variables - e.g. those used for drawing.
|
|
self.height = height
|
|
self.width = width
|
|
self._last_start_line = 0
|
|
|
|
# Set up internal state for colours - used by children to determine
|
|
# changes to text colour when refreshing the screen.
|
|
self._colour = 0
|
|
self._attr = 0
|
|
self._bg = 0
|
|
|
|
# tracking of current cursor position - used in screen refresh.
|
|
self._cur_x = 0
|
|
self._cur_y = 0
|
|
|
|
# Control variables for playing out a set of Scenes.
|
|
self._scenes = []
|
|
self._scene_index = 0
|
|
self._frame = 0
|
|
self._idle_frame_count = 0
|
|
self._forced_update = False
|
|
self._unhandled_input = self._unhandled_event_default
|
|
|
|
@classmethod
|
|
def open(cls, height=200, catch_interrupt=False, unicode_aware=None):
|
|
"""
|
|
Construct a new Screen for any platform. This will just create the
|
|
correct Screen object for your environment. See :py:meth:`.wrapper` for
|
|
a function to create and tidy up once you've finished with the Screen.
|
|
|
|
:param height: The buffer height for this window (if using scrolling).
|
|
:param catch_interrupt: Whether to catch and prevent keyboard
|
|
interrupts. Defaults to False to maintain backwards compatibility.
|
|
:param unicode_aware: Whether the application can use unicode or not.
|
|
If None, try to detect from the environment if UTF-8 is enabled.
|
|
"""
|
|
if sys.platform == "win32":
|
|
# Clone the standard output buffer so that we can do whatever we
|
|
# need for the application, but restore the buffer at the end.
|
|
# Note that we need to resize the clone to ensure that it is the
|
|
# same size as the original in some versions of Windows.
|
|
old_out = win32console.PyConsoleScreenBufferType(
|
|
win32file.CreateFile("CONOUT$",
|
|
GENERIC_READ | GENERIC_WRITE,
|
|
FILE_SHARE_WRITE,
|
|
None,
|
|
OPEN_ALWAYS,
|
|
0,
|
|
None))
|
|
try:
|
|
info = old_out.GetConsoleScreenBufferInfo()
|
|
except pywintypes.error:
|
|
info = None
|
|
win_out = win32console.CreateConsoleScreenBuffer()
|
|
if info:
|
|
win_out.SetConsoleScreenBufferSize(info['Size'])
|
|
else:
|
|
win_out.SetStdHandle(STD_OUTPUT_HANDLE)
|
|
win_out.SetConsoleActiveScreenBuffer()
|
|
|
|
# Get the standard input buffer.
|
|
win_in = win32console.PyConsoleScreenBufferType(
|
|
win32file.CreateFile("CONIN$",
|
|
GENERIC_READ | GENERIC_WRITE,
|
|
FILE_SHARE_READ,
|
|
None,
|
|
OPEN_ALWAYS,
|
|
0,
|
|
None))
|
|
win_in.SetStdHandle(STD_INPUT_HANDLE)
|
|
|
|
# Hide the cursor.
|
|
win_out.SetConsoleCursorInfo(1, 0)
|
|
|
|
# Disable scrolling
|
|
out_mode = win_out.GetConsoleMode()
|
|
win_out.SetConsoleMode(
|
|
out_mode & ~ win32console.ENABLE_WRAP_AT_EOL_OUTPUT)
|
|
|
|
# Enable mouse input, disable quick-edit mode and disable ctrl-c
|
|
# if needed.
|
|
in_mode = win_in.GetConsoleMode()
|
|
new_mode = (in_mode | win32console.ENABLE_MOUSE_INPUT |
|
|
ENABLE_EXTENDED_FLAGS)
|
|
new_mode &= ~ENABLE_QUICK_EDIT_MODE
|
|
if catch_interrupt:
|
|
# Ignore ctrl-c handlers if specified.
|
|
new_mode &= ~win32console.ENABLE_PROCESSED_INPUT
|
|
win_in.SetConsoleMode(new_mode)
|
|
|
|
screen = _WindowsScreen(win_out, win_in, height, old_out, in_mode,
|
|
unicode_aware=unicode_aware)
|
|
else:
|
|
# Reproduce curses.wrapper()
|
|
stdscr = curses.initscr()
|
|
curses.noecho()
|
|
curses.cbreak()
|
|
stdscr.keypad(1)
|
|
|
|
# noinspection PyBroadException
|
|
# pylint: disable=bare-except
|
|
# - This code deliberately duplicates the (bad) curses module code.
|
|
try:
|
|
curses.start_color()
|
|
except:
|
|
pass
|
|
screen = _CursesScreen(stdscr, height,
|
|
catch_interrupt=catch_interrupt,
|
|
unicode_aware=unicode_aware)
|
|
|
|
return screen
|
|
|
|
@abstractmethod
|
|
def close(self, restore=True):
|
|
"""
|
|
Close down this Screen and tidy up the environment as required.
|
|
|
|
:param restore: whether to restore the environment or not.
|
|
"""
|
|
|
|
@classmethod
|
|
def wrapper(cls, func, height=200, catch_interrupt=False, arguments=None,
|
|
unicode_aware=None):
|
|
"""
|
|
Construct a new Screen for any platform. This will initialize the
|
|
Screen, call the specified function and then tidy up the system as
|
|
required when the function exits.
|
|
|
|
:param func: The function to call once the Screen has been created.
|
|
:param height: The buffer height for this Screen (if using scrolling).
|
|
:param catch_interrupt: Whether to catch and prevent keyboard
|
|
interrupts. Defaults to False to maintain backwards compatibility.
|
|
:param arguments: Optional arguments list to pass to func (after the
|
|
Screen object).
|
|
:param unicode_aware: Whether the application can use unicode or not.
|
|
If None, try to detect from the environment if UTF-8 is enabled.
|
|
"""
|
|
screen = Screen.open(height,
|
|
catch_interrupt=catch_interrupt,
|
|
unicode_aware=unicode_aware)
|
|
restore = True
|
|
try:
|
|
try:
|
|
if arguments:
|
|
func(screen, *arguments)
|
|
else:
|
|
func(screen)
|
|
except ResizeScreenError:
|
|
restore = False
|
|
raise
|
|
finally:
|
|
screen.close(restore)
|
|
|
|
def _reset(self):
|
|
"""
|
|
Reset the Screen.
|
|
"""
|
|
self._last_start_line = 0
|
|
self._colour = None
|
|
self._attr = None
|
|
self._bg = None
|
|
self._cur_x = None
|
|
self._cur_y = None
|
|
|
|
def refresh(self):
|
|
"""
|
|
Refresh the screen.
|
|
"""
|
|
# Scroll the screen as required to minimize redrawing.
|
|
if self._last_start_line != self._start_line:
|
|
self._scroll(self._start_line - self._last_start_line)
|
|
self._last_start_line = self._start_line
|
|
|
|
# Now draw any deltas to the scrolled screen. Note that CJK character sets sometimes
|
|
# use double-width characters, so don't try to draw the next character if we hit one.
|
|
for y in range(min(self.height, self._buffer_height)):
|
|
skip_next = False
|
|
for x in range(self.width):
|
|
old_cell = self._screen_buffer[y + self._start_line][x]
|
|
new_cell = self._double_buffer[y + self._start_line][x]
|
|
|
|
if skip_next:
|
|
skip_next = False
|
|
else:
|
|
# Check for orphaned half-characters from dual width glyphs (which occurs when
|
|
# a new glyph is drawn over the top of part of such a glpyh).
|
|
if (new_cell[4] == 0 or
|
|
(new_cell[4] == 2 and x < self.width - 1 and
|
|
self._double_buffer[y + self._start_line][x + 1][4] == 2)):
|
|
new_cell = ("x", new_cell[1], new_cell[2], new_cell[3], 1)
|
|
|
|
# Now check for any required updates.
|
|
if old_cell != new_cell:
|
|
self._change_colours(new_cell[1], new_cell[2], new_cell[3])
|
|
self._print_at(new_cell[0], x, y, new_cell[4])
|
|
|
|
# Skip the next character if the new cell was double-width.
|
|
skip_next = new_cell[4] == 2
|
|
|
|
# Finally update the screen buffer to reflect reality.
|
|
self._screen_buffer[y + self._start_line][x] = new_cell
|
|
|
|
|
|
def clear(self):
|
|
"""
|
|
Clear the Screen of all content.
|
|
"""
|
|
# Clear the actual terminal
|
|
self.reset()
|
|
self._change_colours(Screen.COLOUR_WHITE, 0, 0)
|
|
self._clear()
|
|
|
|
def get_key(self):
|
|
"""
|
|
Check for a key without waiting. This method is deprecated. Use
|
|
:py:meth:`.get_event` instead.
|
|
"""
|
|
event = self.get_event()
|
|
if event and isinstance(event, KeyboardEvent):
|
|
return event.key_code
|
|
return None
|
|
|
|
@abstractmethod
|
|
def get_event(self):
|
|
"""
|
|
Check for any events (e.g. key-press or mouse movement) without waiting.
|
|
|
|
:returns: A :py:obj:`.Event` object if anything was detected, otherwise
|
|
it returns None.
|
|
"""
|
|
|
|
@staticmethod
|
|
def ctrl(char):
|
|
"""
|
|
Calculate the control code for a given key. For example, this converts
|
|
"a" to 1 (which is the code for ctrl-a).
|
|
|
|
:param char: The key to convert to a control code.
|
|
:return: The control code as an integer or None if unknown.
|
|
"""
|
|
# Convert string to int if needed.
|
|
if isinstance(char, basestring):
|
|
char = ord(char.upper())
|
|
|
|
# Only deal with the characters between '@' and '_'
|
|
return char & 0x1f if 64 <= char <= 95 else None
|
|
|
|
@abstractmethod
|
|
def has_resized(self):
|
|
"""
|
|
Check whether the screen has been re-sized.
|
|
|
|
:returns: True when the screen has been re-sized since the last check.
|
|
"""
|
|
|
|
def getch(self, x, y):
|
|
"""
|
|
Get the character at a specified location. This method is deprecated.
|
|
Use :py:meth:`.get_from` instead.
|
|
|
|
:param x: The x coordinate.
|
|
:param y: The y coordinate.
|
|
"""
|
|
return self.get_from(x, y)
|
|
|
|
def putch(self, text, x, y, colour=7, attr=0, bg=0, transparent=False):
|
|
"""
|
|
Print text at the specified location. This method is deprecated. Use
|
|
:py:meth:`.print_at` instead.
|
|
|
|
:param text: The (single line) text to be printed.
|
|
:param x: The column (x coord) for the start of the text.
|
|
:param y: The line (y coord) for the start of the text.
|
|
:param colour: The colour of the text to be displayed.
|
|
:param attr: The cell attribute of the text to be displayed.
|
|
:param bg: The background colour of the text to be displayed.
|
|
:param transparent: Whether to print spaces or not, thus giving a
|
|
transparent effect.
|
|
"""
|
|
self.print_at(text, x, y, colour, attr, bg, transparent)
|
|
|
|
@staticmethod
|
|
def _unhandled_event_default(event):
|
|
"""
|
|
Default unhandled event handler for handling simple scene navigation.
|
|
"""
|
|
if isinstance(event, KeyboardEvent):
|
|
c = event.key_code
|
|
if c in (ord("X"), ord("x"), ord("Q"), ord("q")):
|
|
raise StopApplication("User terminated app")
|
|
if c in (ord(" "), ord("\n"), ord("\r")):
|
|
raise NextScene()
|
|
|
|
def play(self, scenes,tk=None, stop_on_resize=False, unhandled_input=None,
|
|
start_scene=None, repeat=True):
|
|
"""
|
|
Play a set of scenes. This is effectively a helper function to wrap
|
|
:py:meth:`.set_scenes` and :py:meth:`.draw_next_frame` to simplify
|
|
animation for most applications.
|
|
|
|
:param scenes: a list of :py:obj:`.Scene` objects to play.
|
|
:param stop_on_resize: Whether to stop when the screen is resized.
|
|
Default is to carry on regardless - which will typically result
|
|
in an error. This is largely done for back-compatibility.
|
|
:param unhandled_input: Function to call for any input not handled
|
|
by the Scenes/Effects being played. Defaults to a function that
|
|
closes the application on "Q" or "X" being pressed.
|
|
:param start_scene: The old Scene to start from. This must have name
|
|
that matches the name of one of the Scenes passed in.
|
|
:param repeat: Whether to repeat the Scenes once it has reached the end.
|
|
Defaults to True.
|
|
|
|
:raises ResizeScreenError: if the screen is resized (and allowed by
|
|
stop_on_resize).
|
|
|
|
The unhandled input function just takes one parameter - the input
|
|
event that was not handled.
|
|
"""
|
|
# Initialise the Screen for animation.
|
|
self.set_scenes(
|
|
scenes, unhandled_input=unhandled_input, start_scene=start_scene)
|
|
logger.info('its the right screen !')
|
|
self.tk = tk
|
|
|
|
# Mainline loop for animations
|
|
try:
|
|
while True:
|
|
self.tk.update()
|
|
|
|
a = time.time()
|
|
self.draw_next_frame(repeat=repeat)
|
|
if self.has_resized():
|
|
if stop_on_resize:
|
|
self._scenes[self._scene_index].exit()
|
|
raise ResizeScreenError("Screen resized",
|
|
self._scenes[self._scene_index])
|
|
b = time.time()
|
|
if b - a < 0.05:
|
|
time.sleep(a + 0.05 - b)
|
|
except StopApplication:
|
|
# Time to stop - just exit the function.
|
|
return
|
|
|
|
def set_scenes(self, scenes, unhandled_input=None, start_scene=None):
|
|
"""
|
|
Remember a set of scenes to be played. This must be called before
|
|
using :py:meth:`.draw_next_frame`.
|
|
|
|
:param scenes: a list of :py:obj:`.Scene` objects to play.
|
|
:param unhandled_input: Function to call for any input not handled
|
|
by the Scenes/Effects being played. Defaults to a function that
|
|
closes the application on "Q" or "X" being pressed.
|
|
:param start_scene: The old Scene to start from. This must have name
|
|
that matches the name of one of the Scenes passed in.
|
|
|
|
:raises ResizeScreenError: if the screen is resized (and allowed by
|
|
stop_on_resize).
|
|
|
|
The unhandled input function just takes one parameter - the input
|
|
event that was not handled.
|
|
"""
|
|
# Save off the scenes now.
|
|
self._scenes = scenes
|
|
|
|
# Set up default unhandled input handler if needed.
|
|
if unhandled_input is None:
|
|
# Check that none of the Effects is incompatible with the default
|
|
# handler.
|
|
safe = True
|
|
for scene in self._scenes:
|
|
for effect in scene.effects:
|
|
safe &= effect.safe_to_default_unhandled_input
|
|
if safe:
|
|
unhandled_input = self._unhandled_event_default
|
|
self._unhandled_input = unhandled_input
|
|
|
|
# Find the starting scene. Default to first if no match.
|
|
self._scene_index = 0
|
|
if start_scene is not None:
|
|
for i, scene in enumerate(scenes):
|
|
if scene.name == start_scene.name:
|
|
self._scene_index = i
|
|
break
|
|
|
|
# Reset the Scene - this allows the original scene to pick up old
|
|
# values on resizing.
|
|
self._scenes[self._scene_index].reset(
|
|
old_scene=start_scene, screen=self)
|
|
|
|
# Reset other internal state for the animation
|
|
self._frame = 0
|
|
self._idle_frame_count = 0
|
|
self._forced_update = False
|
|
self.clear()
|
|
|
|
def draw_next_frame(self, repeat=True):
|
|
"""
|
|
Draw the next frame in the currently configured Scenes. You must call
|
|
:py:meth:`.set_scenes` before using this for the first time.
|
|
|
|
:param repeat: Whether to repeat the Scenes once it has reached the end.
|
|
Defaults to True.
|
|
|
|
:raises StopApplication: if the application should be terminated.
|
|
"""
|
|
scene = self._scenes[self._scene_index]
|
|
try:
|
|
# Check for an event now and remember for refresh reasons.
|
|
event = self.get_event()
|
|
got_event = event is not None
|
|
|
|
# Now process all the input events
|
|
while event is not None:
|
|
event = scene.process_event(event)
|
|
if event is not None and self._unhandled_input is not None:
|
|
self._unhandled_input(event)
|
|
event = self.get_event()
|
|
|
|
# Only bother with a refresh if there was an event to process or
|
|
# we have to refresh due to the refresh limit required for an
|
|
# Effect.
|
|
self._frame += 1
|
|
self._idle_frame_count -= 1
|
|
if got_event or self._idle_frame_count <= 0 or self._forced_update:
|
|
self._forced_update = False
|
|
self._idle_frame_count = 1000000
|
|
for effect in scene.effects:
|
|
# Update the effect and delete if needed.
|
|
effect.update(self._frame)
|
|
if effect.delete_count is not None:
|
|
effect.delete_count -= 1
|
|
if effect.delete_count <= 0:
|
|
scene.remove_effect(effect)
|
|
|
|
# Sort out when we next _need_ to do a refresh.
|
|
if effect.frame_update_count > 0:
|
|
self._idle_frame_count = min(self._idle_frame_count,
|
|
effect.frame_update_count)
|
|
self.refresh()
|
|
|
|
if 0 < scene.duration <= self._frame:
|
|
raise NextScene()
|
|
except NextScene as e:
|
|
# Tidy up the current scene.
|
|
scene.exit()
|
|
|
|
# Find the specified next Scene
|
|
if e.name is None:
|
|
# Just allow next iteration of loop
|
|
self._scene_index += 1
|
|
if self._scene_index >= len(self._scenes):
|
|
if repeat:
|
|
self._scene_index = 0
|
|
else:
|
|
raise StopApplication("Repeat disabled")
|
|
else:
|
|
# Find the required scene.
|
|
for i, scene in enumerate(self._scenes):
|
|
if scene.name == e.name:
|
|
self._scene_index = i
|
|
break
|
|
else:
|
|
raise RuntimeError(
|
|
"Could not find Scene: '{}'".format(e.name))
|
|
|
|
# Reset the screen if needed.
|
|
scene = self._scenes[self._scene_index]
|
|
scene.reset()
|
|
self._frame = 0
|
|
self._idle_frame_count = 0
|
|
if scene.clear:
|
|
self.clear()
|
|
|
|
def force_update(self):
|
|
"""
|
|
Force the Screen to redraw the current Scene on the next call to
|
|
draw_next_frame, overriding the frame_update_count value for all the
|
|
Effects.
|
|
"""
|
|
self._forced_update = True
|
|
|
|
@abstractmethod
|
|
def _change_colours(self, colour, attr, bg):
|
|
"""
|
|
Change current colour if required.
|
|
|
|
:param colour: New colour to use.
|
|
:param attr: New attributes to use.
|
|
:param bg: New background colour to use.
|
|
"""
|
|
|
|
@abstractmethod
|
|
def _print_at(self, text, x, y, width):
|
|
"""
|
|
Print string at the required location.
|
|
|
|
:param text: The text string to print.
|
|
:param x: The x coordinate
|
|
:param y: The Y coordinate
|
|
:param width: The width of the character (for dual-width glyphs in CJK languages).
|
|
"""
|
|
|
|
@abstractmethod
|
|
def _clear(self):
|
|
"""
|
|
Clear the window.
|
|
"""
|
|
|
|
@abstractmethod
|
|
def _scroll(self, lines):
|
|
"""
|
|
Scroll the window up or down.
|
|
|
|
:param lines: Number of lines to scroll. Negative numbers scroll down.
|
|
"""
|
|
|
|
@abstractmethod
|
|
def set_title(self, title):
|
|
"""
|
|
Set the title for this terminal/console session. This will typically
|
|
change the text displayed in the window title bar.
|
|
|
|
:param title: The title to be set.
|
|
"""
|
|
|
|
if sys.platform == "win32":
|
|
import win32console
|
|
import win32con
|
|
import pywintypes
|
|
import win32file
|
|
from win32console import STD_OUTPUT_HANDLE, STD_INPUT_HANDLE
|
|
from win32file import GENERIC_READ, FILE_SHARE_READ, OPEN_ALWAYS, \
|
|
GENERIC_WRITE, FILE_SHARE_WRITE
|
|
|
|
class _WindowsScreen(Screen):
|
|
"""
|
|
Windows screen implementation.
|
|
"""
|
|
|
|
# Virtual key code mapping.
|
|
_KEY_MAP = {
|
|
win32con.VK_ESCAPE: Screen.KEY_ESCAPE,
|
|
win32con.VK_F1: Screen.KEY_F1,
|
|
win32con.VK_F2: Screen.KEY_F2,
|
|
win32con.VK_F3: Screen.KEY_F3,
|
|
win32con.VK_F4: Screen.KEY_F4,
|
|
win32con.VK_F5: Screen.KEY_F5,
|
|
win32con.VK_F6: Screen.KEY_F6,
|
|
win32con.VK_F7: Screen.KEY_F7,
|
|
win32con.VK_F8: Screen.KEY_F8,
|
|
win32con.VK_F9: Screen.KEY_F9,
|
|
win32con.VK_F10: Screen.KEY_F10,
|
|
win32con.VK_F11: Screen.KEY_F11,
|
|
win32con.VK_F12: Screen.KEY_F12,
|
|
win32con.VK_F13: Screen.KEY_F13,
|
|
win32con.VK_F14: Screen.KEY_F14,
|
|
win32con.VK_F15: Screen.KEY_F15,
|
|
win32con.VK_F16: Screen.KEY_F16,
|
|
win32con.VK_F17: Screen.KEY_F17,
|
|
win32con.VK_F18: Screen.KEY_F18,
|
|
win32con.VK_F19: Screen.KEY_F19,
|
|
win32con.VK_F20: Screen.KEY_F20,
|
|
win32con.VK_F21: Screen.KEY_F21,
|
|
win32con.VK_F22: Screen.KEY_F22,
|
|
win32con.VK_F23: Screen.KEY_F23,
|
|
win32con.VK_F24: Screen.KEY_F24,
|
|
win32con.VK_PRINT: Screen.KEY_PRINT_SCREEN,
|
|
win32con.VK_INSERT: Screen.KEY_INSERT,
|
|
win32con.VK_DELETE: Screen.KEY_DELETE,
|
|
win32con.VK_HOME: Screen.KEY_HOME,
|
|
win32con.VK_END: Screen.KEY_END,
|
|
win32con.VK_LEFT: Screen.KEY_LEFT,
|
|
win32con.VK_UP: Screen.KEY_UP,
|
|
win32con.VK_RIGHT: Screen.KEY_RIGHT,
|
|
win32con.VK_DOWN: Screen.KEY_DOWN,
|
|
win32con.VK_PRIOR: Screen.KEY_PAGE_UP,
|
|
win32con.VK_NEXT: Screen.KEY_PAGE_DOWN,
|
|
win32con.VK_BACK: Screen.KEY_BACK,
|
|
win32con.VK_TAB: Screen.KEY_TAB,
|
|
}
|
|
|
|
_EXTRA_KEY_MAP = {
|
|
win32con.VK_NUMPAD0: Screen.KEY_NUMPAD0,
|
|
win32con.VK_NUMPAD1: Screen.KEY_NUMPAD1,
|
|
win32con.VK_NUMPAD2: Screen.KEY_NUMPAD2,
|
|
win32con.VK_NUMPAD3: Screen.KEY_NUMPAD3,
|
|
win32con.VK_NUMPAD4: Screen.KEY_NUMPAD4,
|
|
win32con.VK_NUMPAD5: Screen.KEY_NUMPAD5,
|
|
win32con.VK_NUMPAD6: Screen.KEY_NUMPAD6,
|
|
win32con.VK_NUMPAD7: Screen.KEY_NUMPAD7,
|
|
win32con.VK_NUMPAD8: Screen.KEY_NUMPAD8,
|
|
win32con.VK_NUMPAD9: Screen.KEY_NUMPAD9,
|
|
win32con.VK_MULTIPLY: Screen.KEY_MULTIPLY,
|
|
win32con.VK_ADD: Screen.KEY_ADD,
|
|
win32con.VK_SUBTRACT: Screen.KEY_SUBTRACT,
|
|
win32con.VK_DECIMAL: Screen.KEY_DECIMAL,
|
|
win32con.VK_DIVIDE: Screen.KEY_DIVIDE,
|
|
win32con.VK_CAPITAL: Screen.KEY_CAPS_LOCK,
|
|
win32con.VK_NUMLOCK: Screen.KEY_NUM_LOCK,
|
|
win32con.VK_SCROLL: Screen.KEY_SCROLL_LOCK,
|
|
win32con.VK_SHIFT: Screen.KEY_SHIFT,
|
|
win32con.VK_CONTROL: Screen.KEY_CONTROL,
|
|
win32con.VK_MENU: Screen.KEY_MENU,
|
|
}
|
|
|
|
# Foreground colour lookup table.
|
|
_COLOURS = {
|
|
Screen.COLOUR_BLACK: 0,
|
|
Screen.COLOUR_RED: win32console.FOREGROUND_RED,
|
|
Screen.COLOUR_GREEN: win32console.FOREGROUND_GREEN,
|
|
Screen.COLOUR_YELLOW: (win32console.FOREGROUND_RED |
|
|
win32console.FOREGROUND_GREEN),
|
|
Screen.COLOUR_BLUE: win32console.FOREGROUND_BLUE,
|
|
Screen.COLOUR_MAGENTA: (win32console.FOREGROUND_RED |
|
|
win32console.FOREGROUND_BLUE),
|
|
Screen.COLOUR_CYAN: (win32console.FOREGROUND_BLUE |
|
|
win32console.FOREGROUND_GREEN),
|
|
Screen.COLOUR_WHITE: (win32console.FOREGROUND_RED |
|
|
win32console.FOREGROUND_GREEN |
|
|
win32console.FOREGROUND_BLUE)
|
|
}
|
|
|
|
# Background colour lookup table.
|
|
_BG_COLOURS = {
|
|
Screen.COLOUR_BLACK: 0,
|
|
Screen.COLOUR_RED: win32console.BACKGROUND_RED,
|
|
Screen.COLOUR_GREEN: win32console.BACKGROUND_GREEN,
|
|
Screen.COLOUR_YELLOW: (win32console.BACKGROUND_RED |
|
|
win32console.BACKGROUND_GREEN),
|
|
Screen.COLOUR_BLUE: win32console.BACKGROUND_BLUE,
|
|
Screen.COLOUR_MAGENTA: (win32console.BACKGROUND_RED |
|
|
win32console.BACKGROUND_BLUE),
|
|
Screen.COLOUR_CYAN: (win32console.BACKGROUND_BLUE |
|
|
win32console.BACKGROUND_GREEN),
|
|
Screen.COLOUR_WHITE: (win32console.BACKGROUND_RED |
|
|
win32console.BACKGROUND_GREEN |
|
|
win32console.BACKGROUND_BLUE)
|
|
}
|
|
|
|
# Attribute lookup table
|
|
_ATTRIBUTES = {
|
|
0: lambda x: x,
|
|
Screen.A_BOLD: lambda x: x | win32console.FOREGROUND_INTENSITY,
|
|
Screen.A_NORMAL: lambda x: x,
|
|
# Windows console uses a bitmap where background is the top nibble,
|
|
# so we can reverse by swapping nibbles.
|
|
Screen.A_REVERSE: lambda x: ((x & 15) * 16) + ((x & 240) // 16),
|
|
Screen.A_UNDERLINE: lambda x: x
|
|
}
|
|
|
|
def __init__(self, stdout, stdin, buffer_height, old_out, old_in,
|
|
unicode_aware=False):
|
|
"""
|
|
:param stdout: The win32console PyConsoleScreenBufferType object for
|
|
stdout.
|
|
:param stdin: The win32console PyConsoleScreenBufferType object for
|
|
stdin.
|
|
:param buffer_height: The buffer height for this window (if using
|
|
scrolling).
|
|
:param old_out: The original win32console PyConsoleScreenBufferType
|
|
object for stdout that should be restored on exit.
|
|
:param old_in: The original stdin state that should be restored on
|
|
exit.
|
|
:param unicode_aware: Whether this Screen can use unicode or not.
|
|
"""
|
|
# Save off the screen details and set up the scrolling pad.
|
|
info = stdout.GetConsoleScreenBufferInfo()['Window']
|
|
width = info.Right - info.Left + 1
|
|
height = info.Bottom - info.Top + 1
|
|
|
|
# Detect UTF-8 if needed and then construct the Screen.
|
|
if unicode_aware is None:
|
|
# According to MSDN, 65001 is the Windows UTF-8 code page.
|
|
unicode_aware = win32console.GetConsoleCP() == 65001
|
|
super(_WindowsScreen, self).__init__(
|
|
height, width, buffer_height, unicode_aware)
|
|
|
|
# Save off the console details.
|
|
self._stdout = stdout
|
|
self._stdin = stdin
|
|
self._last_width = width
|
|
self._last_height = height
|
|
self._old_out = old_out
|
|
self._old_in = old_in
|
|
|
|
# Windows is limited to the ANSI colour set.
|
|
self.colours = 8
|
|
|
|
# Opt for compatibility with Linux by default
|
|
self._map_all = False
|
|
|
|
# Set of keys currently pressed.
|
|
self._keys = set()
|
|
|
|
def close(self, restore=True):
|
|
"""
|
|
Close down this Screen and tidy up the environment as required.
|
|
|
|
:param restore: whether to restore the environment or not.
|
|
"""
|
|
if restore:
|
|
# Reset the original screen settings.
|
|
self._old_out.SetConsoleActiveScreenBuffer()
|
|
self._stdin.SetConsoleMode(self._old_in)
|
|
|
|
def map_all_keys(self, state):
|
|
"""
|
|
Switch on extended keyboard mapping for this Screen.
|
|
|
|
:param state: Boolean flag where true means map all keys.
|
|
|
|
Enabling this setting will allow Windows to tell you when any key
|
|
is pressed, including metakeys (like shift and control) and whether
|
|
the numeric keypad keys have been used.
|
|
|
|
.. warning::
|
|
|
|
Using this means your application will not be compatible across
|
|
all platforms.
|
|
"""
|
|
self._map_all = state
|
|
|
|
def get_event(self):
|
|
"""
|
|
Check for any event without waiting.
|
|
"""
|
|
# Look for a new event and consume it if there is one.
|
|
while len(self._stdin.PeekConsoleInput(1)) > 0:
|
|
event = self._stdin.ReadConsoleInput(1)[0]
|
|
if event.EventType == win32console.KEY_EVENT:
|
|
# Pasting unicode text appears to just generate key-up
|
|
# events (as if you had pressed the Alt keys plus the
|
|
# keypad code for the character), but the rest of the
|
|
# console input simply doesn't
|
|
# work with key up events - e.g. misses keyboard repeats.
|
|
#
|
|
# We therefore allow any key press (i.e. KeyDown) event and
|
|
# _any_ event that appears to have popped up from nowhere
|
|
# as long as the Alt key is present.
|
|
key_code = ord(event.Char)
|
|
logger.debug("Processing key: %x", key_code)
|
|
if (event.KeyDown or
|
|
(key_code > 0 and key_code not in self._keys and
|
|
event.VirtualKeyCode == win32con.VK_MENU)):
|
|
# Record any keys that were pressed.
|
|
if event.KeyDown:
|
|
self._keys.add(key_code)
|
|
|
|
# Translate keys into a KeyboardEvent object.
|
|
if event.VirtualKeyCode in self._KEY_MAP:
|
|
key_code = self._KEY_MAP[event.VirtualKeyCode]
|
|
|
|
# Sadly, we are limited to Linux terminal input and so
|
|
# can't return modifier states in a cross-platform way.
|
|
# If the user decided not to be cross-platform, so be
|
|
# it, otherwise map some standard bindings for extended
|
|
# keys.
|
|
if (self._map_all and
|
|
event.VirtualKeyCode in self._EXTRA_KEY_MAP):
|
|
key_code = self._EXTRA_KEY_MAP[event.VirtualKeyCode]
|
|
else:
|
|
if (event.VirtualKeyCode == win32con.VK_TAB and
|
|
event.ControlKeyState &
|
|
win32con.SHIFT_PRESSED):
|
|
key_code = Screen.KEY_BACK_TAB
|
|
|
|
# Don't return anything if we didn't have a valid
|
|
# mapping.
|
|
if key_code:
|
|
return KeyboardEvent(key_code)
|
|
else:
|
|
# Tidy up any key that was previously pressed. At
|
|
# start-up, we may be mid-key, so can't assume this must
|
|
# always match up.
|
|
if key_code in self._keys:
|
|
self._keys.remove(key_code)
|
|
|
|
elif event.EventType == win32console.MOUSE_EVENT:
|
|
# Translate into a MouseEvent object.
|
|
logger.debug("Processing mouse: %d, %d",
|
|
event.MousePosition.X, event.MousePosition.Y)
|
|
button = 0
|
|
if event.EventFlags == 0:
|
|
# Button pressed - translate it.
|
|
if (event.ButtonState &
|
|
win32con.FROM_LEFT_1ST_BUTTON_PRESSED != 0):
|
|
button |= MouseEvent.LEFT_CLICK
|
|
if (event.ButtonState &
|
|
win32con.RIGHTMOST_BUTTON_PRESSED != 0):
|
|
button |= MouseEvent.RIGHT_CLICK
|
|
elif event.EventFlags & win32con.DOUBLE_CLICK != 0:
|
|
button |= MouseEvent.DOUBLE_CLICK
|
|
|
|
return MouseEvent(event.MousePosition.X,
|
|
event.MousePosition.Y,
|
|
button)
|
|
|
|
# If we get here, we've fully processed the event queue and found
|
|
# nothing interesting.
|
|
return None
|
|
|
|
def has_resized(self):
|
|
"""
|
|
Check whether the screen has been re-sized.
|
|
"""
|
|
# Get the current Window dimensions and check them against last
|
|
# time.
|
|
re_sized = False
|
|
info = self._stdout.GetConsoleScreenBufferInfo()['Window']
|
|
width = info.Right - info.Left + 1
|
|
height = info.Bottom - info.Top + 1
|
|
if width != self._last_width or height != self._last_height:
|
|
re_sized = True
|
|
return re_sized
|
|
|
|
def _change_colours(self, colour, attr, bg):
|
|
"""
|
|
Change current colour if required.
|
|
|
|
:param colour: New colour to use.
|
|
:param attr: New attributes to use.
|
|
:param bg: New background colour to use.
|
|
"""
|
|
# Change attribute first as this will reset colours when swapping
|
|
# modes.
|
|
if colour != self._colour or attr != self._attr or self._bg != bg:
|
|
new_attr = self._ATTRIBUTES[attr](
|
|
self._COLOURS[colour] + self._BG_COLOURS[bg])
|
|
self._stdout.SetConsoleTextAttribute(new_attr)
|
|
self._attr = attr
|
|
self._colour = colour
|
|
self._bg = bg
|
|
|
|
def _print_at(self, text, x, y, width):
|
|
"""
|
|
Print string at the required location.
|
|
|
|
:param text: The text string to print.
|
|
:param x: The x coordinate
|
|
:param y: The Y coordinate
|
|
:param width: The width of the character (for dual-width glyphs in CJK languages).
|
|
"""
|
|
# We can throw temporary errors on resizing, so catch and ignore
|
|
# them on the assumption that we'll resize shortly.
|
|
try:
|
|
# Move the cursor if necessary
|
|
if x != self._cur_x or y != self._cur_y:
|
|
self._stdout.SetConsoleCursorPosition(
|
|
win32console.PyCOORDType(x, y))
|
|
|
|
# Print the text at the required location and update the current
|
|
# position.
|
|
self._stdout.WriteConsole(text)
|
|
self._cur_x = x + width
|
|
self._cur_y = y
|
|
except pywintypes.error:
|
|
pass
|
|
|
|
def _scroll(self, lines):
|
|
"""
|
|
Scroll the window up or down.
|
|
|
|
:param lines: Number of lines to scroll. Negative numbers scroll
|
|
down.
|
|
"""
|
|
# Scroll the visible screen up by one line
|
|
info = self._stdout.GetConsoleScreenBufferInfo()['Window']
|
|
rectangle = win32console.PySMALL_RECTType(
|
|
info.Left, info.Top + lines, info.Right, info.Bottom)
|
|
new_pos = win32console.PyCOORDType(0, info.Top)
|
|
self._stdout.ScrollConsoleScreenBuffer(
|
|
rectangle, None, new_pos, " ", 0)
|
|
|
|
def _clear(self):
|
|
"""
|
|
Clear the terminal.
|
|
"""
|
|
info = self._stdout.GetConsoleScreenBufferInfo()['Window']
|
|
width = info.Right - info.Left + 1
|
|
height = info.Bottom - info.Top + 1
|
|
box_size = width * height
|
|
self._stdout.FillConsoleOutputAttribute(
|
|
0, box_size, win32console.PyCOORDType(0, 0))
|
|
self._stdout.FillConsoleOutputCharacter(
|
|
" ", box_size, win32console.PyCOORDType(0, 0))
|
|
self._stdout.SetConsoleCursorPosition(
|
|
win32console.PyCOORDType(0, 0))
|
|
|
|
def set_title(self, title):
|
|
"""
|
|
Set the title for this terminal/console session. This will
|
|
typically change the text displayed in the window title bar.
|
|
|
|
:param title: The title to be set.
|
|
"""
|
|
win32console.SetConsoleTitle(title)
|
|
|
|
else:
|
|
# UNIX compatible platform - use curses
|
|
import curses
|
|
|
|
class _CursesScreen(Screen):
|
|
"""
|
|
Curses screen implementation.
|
|
"""
|
|
|
|
# Virtual key code mapping.
|
|
_KEY_MAP = {
|
|
27: Screen.KEY_ESCAPE,
|
|
curses.KEY_F1: Screen.KEY_F1,
|
|
curses.KEY_F2: Screen.KEY_F2,
|
|
curses.KEY_F3: Screen.KEY_F3,
|
|
curses.KEY_F4: Screen.KEY_F4,
|
|
curses.KEY_F5: Screen.KEY_F5,
|
|
curses.KEY_F6: Screen.KEY_F6,
|
|
curses.KEY_F7: Screen.KEY_F7,
|
|
curses.KEY_F8: Screen.KEY_F8,
|
|
curses.KEY_F9: Screen.KEY_F9,
|
|
curses.KEY_F10: Screen.KEY_F10,
|
|
curses.KEY_F11: Screen.KEY_F11,
|
|
curses.KEY_F12: Screen.KEY_F12,
|
|
curses.KEY_F13: Screen.KEY_F13,
|
|
curses.KEY_F14: Screen.KEY_F14,
|
|
curses.KEY_F15: Screen.KEY_F15,
|
|
curses.KEY_F16: Screen.KEY_F16,
|
|
curses.KEY_F17: Screen.KEY_F17,
|
|
curses.KEY_F18: Screen.KEY_F18,
|
|
curses.KEY_F19: Screen.KEY_F19,
|
|
curses.KEY_F20: Screen.KEY_F20,
|
|
curses.KEY_F21: Screen.KEY_F21,
|
|
curses.KEY_F22: Screen.KEY_F22,
|
|
curses.KEY_F23: Screen.KEY_F23,
|
|
curses.KEY_F24: Screen.KEY_F24,
|
|
curses.KEY_PRINT: Screen.KEY_PRINT_SCREEN,
|
|
curses.KEY_IC: Screen.KEY_INSERT,
|
|
curses.KEY_DC: Screen.KEY_DELETE,
|
|
curses.KEY_HOME: Screen.KEY_HOME,
|
|
curses.KEY_END: Screen.KEY_END,
|
|
curses.KEY_LEFT: Screen.KEY_LEFT,
|
|
curses.KEY_UP: Screen.KEY_UP,
|
|
curses.KEY_RIGHT: Screen.KEY_RIGHT,
|
|
curses.KEY_DOWN: Screen.KEY_DOWN,
|
|
curses.KEY_PPAGE: Screen.KEY_PAGE_UP,
|
|
curses.KEY_NPAGE: Screen.KEY_PAGE_DOWN,
|
|
curses.KEY_BACKSPACE: Screen.KEY_BACK,
|
|
9: Screen.KEY_TAB,
|
|
curses.KEY_BTAB: Screen.KEY_BACK_TAB,
|
|
# Terminals translate keypad keys, so no need for a special
|
|
# mapping here.
|
|
|
|
# Terminals don't transmit meta keys (like control, shift, etc), so
|
|
# there's no translation for them either.
|
|
}
|
|
|
|
def __init__(self, win, height=200, catch_interrupt=False,
|
|
unicode_aware=False):
|
|
"""
|
|
:param win: The window object as returned by the curses wrapper
|
|
method.
|
|
:param height: The height of the screen buffer to be used.
|
|
:param catch_interrupt: Whether to catch SIGINT or not.
|
|
:param unicode_aware: Whether this Screen can use unicode or not.
|
|
"""
|
|
# Determine unicode support if needed.
|
|
if unicode_aware is None:
|
|
encoding = getlocale()[1]
|
|
if not encoding:
|
|
encoding = getdefaultlocale()[1]
|
|
unicode_aware = (encoding is not None and
|
|
encoding.lower() == "utf-8")
|
|
|
|
# Save off the screen details.
|
|
super(_CursesScreen, self).__init__(
|
|
win.getmaxyx()[0], win.getmaxyx()[1], height, unicode_aware)
|
|
self._screen = win
|
|
self._screen.keypad(1)
|
|
|
|
# Set up basic colour schemes.
|
|
self.colours = curses.COLORS
|
|
|
|
# Disable the cursor.
|
|
curses.curs_set(0)
|
|
|
|
# Non-blocking key checks.
|
|
self._screen.nodelay(1)
|
|
|
|
# Set up signal handler for screen resizing.
|
|
self._re_sized = False
|
|
signal.signal(signal.SIGWINCH, self._resize_handler)
|
|
|
|
# Catch SIGINTs and translated them to ctrl-c if needed.
|
|
if catch_interrupt:
|
|
# Ignore SIGINT (ctrl-c) and SIGTSTP (ctrl-z) signals.
|
|
signal.signal(signal.SIGINT, self._catch_interrupt)
|
|
signal.signal(signal.SIGTSTP, self._catch_interrupt)
|
|
|
|
# Enable mouse events
|
|
curses.mousemask(curses.ALL_MOUSE_EVENTS |
|
|
curses.REPORT_MOUSE_POSITION)
|
|
|
|
# Lookup the necessary escape codes in the terminfo database.
|
|
self._move_y_x = curses.tigetstr("cup")
|
|
self._up_line = curses.tigetstr("ri").decode("utf-8")
|
|
self._down_line = curses.tigetstr("ind").decode("utf-8")
|
|
self._fg_color = curses.tigetstr("setaf")
|
|
self._bg_color = curses.tigetstr("setab")
|
|
if curses.tigetflag("hs"):
|
|
self._start_title = curses.tigetstr("tsl").decode("utf-8")
|
|
self._end_title = curses.tigetstr("fsl").decode("utf-8")
|
|
else:
|
|
self._start_title = self._end_title = None
|
|
self._a_normal = curses.tigetstr("sgr0").decode("utf-8")
|
|
self._a_bold = curses.tigetstr("bold").decode("utf-8")
|
|
self._a_reverse = curses.tigetstr("rev").decode("utf-8")
|
|
self._a_underline = curses.tigetstr("smul").decode("utf-8")
|
|
self._clear_screen = curses.tigetstr("clear").decode("utf-8")
|
|
|
|
# Conversion from Screen attributes to curses equivalents.
|
|
self._ATTRIBUTES = {
|
|
Screen.A_BOLD: self._a_bold,
|
|
Screen.A_NORMAL: self._a_normal,
|
|
Screen.A_REVERSE: self._a_reverse,
|
|
Screen.A_UNDERLINE: self._a_underline
|
|
}
|
|
|
|
# Byte stream processing for unicode input.
|
|
self._bytes_to_read = 0
|
|
self._bytes_to_return = b""
|
|
|
|
# We'll actually break out into low-level output, so flush any
|
|
# high level buffers now.
|
|
self._screen.refresh()
|
|
|
|
def close(self, restore=True):
|
|
"""
|
|
Close down this Screen and tidy up the environment as required.
|
|
|
|
:param restore: whether to restore the environment or not.
|
|
"""
|
|
if restore:
|
|
self._screen.keypad(0)
|
|
curses.echo()
|
|
curses.nocbreak()
|
|
curses.endwin()
|
|
|
|
@staticmethod
|
|
def _safe_write(msg):
|
|
"""
|
|
Safe write to screen - catches IOErrors on screen resize.
|
|
|
|
:param msg: The message to write to the screen.
|
|
"""
|
|
try:
|
|
sys.stdout.write(msg)
|
|
except IOError:
|
|
# Screen resize can throw IOErrors. These can be safely
|
|
# ignored as the screen will be shortly reset anyway.
|
|
pass
|
|
|
|
def _resize_handler(self, *_):
|
|
"""
|
|
Window resize signal handler. We don't care about any of the
|
|
parameters passed in beyond the object reference.
|
|
"""
|
|
curses.endwin()
|
|
curses.initscr()
|
|
self._re_sized = True
|
|
|
|
def _scroll(self, lines):
|
|
"""
|
|
Scroll the window up or down.
|
|
|
|
:param lines: Number of lines to scroll. Negative numbers scroll
|
|
down.
|
|
"""
|
|
if lines < 0:
|
|
self._safe_write("{}{}".format(
|
|
curses.tparm(self._move_y_x, 0, 0).decode("utf-8"),
|
|
self._up_line * -lines))
|
|
else:
|
|
self._safe_write("{}{}".format(curses.tparm(
|
|
self._move_y_x, self.height, 0).decode("utf-8"),
|
|
self._down_line * lines))
|
|
|
|
def _clear(self):
|
|
"""
|
|
Clear the Screen of all content.
|
|
"""
|
|
self._safe_write(self._clear_screen)
|
|
sys.stdout.flush()
|
|
|
|
def refresh(self):
|
|
"""
|
|
Refresh the screen.
|
|
"""
|
|
super(_CursesScreen, self).refresh()
|
|
try:
|
|
sys.stdout.flush()
|
|
except IOError:
|
|
pass
|
|
|
|
@staticmethod
|
|
def _catch_interrupt(signal_no, frame):
|
|
"""
|
|
SIGINT handler. We ignore the signal and frame info passed in.
|
|
"""
|
|
# Stop pep-8 shouting at me for unused params I can't control.
|
|
del frame
|
|
|
|
# The OS already caught the ctrl-c, so inject it now for the next
|
|
# input.
|
|
if signal_no == signal.SIGINT:
|
|
curses.ungetch(3)
|
|
elif signal_no == signal.SIGTSTP:
|
|
curses.ungetch(26)
|
|
return
|
|
|
|
def get_event(self):
|
|
"""
|
|
Check for an event without waiting.
|
|
"""
|
|
# Spin through notifications until we find something we want.
|
|
key = 0
|
|
while key != -1:
|
|
# Get the next key
|
|
key = self._screen.getch()
|
|
|
|
if key == curses.KEY_RESIZE:
|
|
# Handle screen resize
|
|
self._re_sized = True
|
|
elif key == curses.KEY_MOUSE:
|
|
# Handle a mouse event
|
|
_, x, y, _, bstate = curses.getmouse()
|
|
buttons = 0
|
|
# Some Linux modes only report clicks, so check for any
|
|
# button down or click events.
|
|
if (bstate & curses.BUTTON1_PRESSED != 0 or
|
|
bstate & curses.BUTTON1_CLICKED != 0):
|
|
buttons |= MouseEvent.LEFT_CLICK
|
|
if (bstate & curses.BUTTON3_PRESSED != 0 or
|
|
bstate & curses.BUTTON3_CLICKED != 0):
|
|
buttons |= MouseEvent.RIGHT_CLICK
|
|
if bstate & curses.BUTTON1_DOUBLE_CLICKED != 0:
|
|
buttons |= MouseEvent.DOUBLE_CLICK
|
|
return MouseEvent(x, y, buttons)
|
|
elif key != -1:
|
|
# Handle any byte streams first
|
|
logger.debug("Processing key: %x", key)
|
|
if self._unicode_aware and key > 0:
|
|
if key & 0xC0 == 0xC0:
|
|
self._bytes_to_return = struct.pack(b"B", key)
|
|
self._bytes_to_read = bin(key)[2:].index("0") - 1
|
|
logger.debug("Byte stream: %d bytes left",
|
|
self._bytes_to_read)
|
|
continue
|
|
elif self._bytes_to_read > 0:
|
|
self._bytes_to_return += struct.pack(b"B", key)
|
|
self._bytes_to_read -= 1
|
|
if self._bytes_to_read > 0:
|
|
continue
|
|
else:
|
|
key = ord(self._bytes_to_return.decode("utf-8"))
|
|
|
|
# Handle a genuine key press.
|
|
logger.debug("Returning key: %x", key)
|
|
if key in self._KEY_MAP:
|
|
return KeyboardEvent(self._KEY_MAP[key])
|
|
elif key != -1:
|
|
return KeyboardEvent(key)
|
|
|
|
return None
|
|
|
|
def has_resized(self):
|
|
"""
|
|
Check whether the screen has been re-sized.
|
|
"""
|
|
re_sized = self._re_sized
|
|
self._re_sized = False
|
|
return re_sized
|
|
|
|
def _change_colours(self, colour, attr, bg):
|
|
"""
|
|
Change current colour if required.
|
|
|
|
:param colour: New colour to use.
|
|
:param attr: New attributes to use.
|
|
:param bg: New background colour to use.
|
|
"""
|
|
# Change attribute first as this will reset colours when swapping
|
|
# modes.
|
|
if attr != self._attr:
|
|
self._safe_write(self._a_normal)
|
|
if attr != 0:
|
|
self._safe_write(self._ATTRIBUTES[attr])
|
|
self._attr = attr
|
|
self._colour = None
|
|
self._bg = None
|
|
|
|
# Now swap colours if required.
|
|
if colour != self._colour:
|
|
self._safe_write(curses.tparm(
|
|
self._fg_color, colour).decode("utf-8"))
|
|
self._colour = colour
|
|
if bg != self._bg:
|
|
self._safe_write(curses.tparm(
|
|
self._bg_color, bg).decode("utf-8"))
|
|
self._bg = bg
|
|
|
|
def _print_at(self, text, x, y, width):
|
|
"""
|
|
Print string at the required location.
|
|
|
|
:param text: The text string to print.
|
|
:param x: The x coordinate
|
|
:param y: The Y coordinate
|
|
:param width: The width of the character (for dual-width glyphs in CJK languages).
|
|
"""
|
|
# Move the cursor if necessary
|
|
cursor = u""
|
|
if x != self._cur_x or y != self._cur_y:
|
|
cursor = curses.tparm(self._move_y_x, y, x).decode("utf-8")
|
|
|
|
# Print the text at the required location and update the current
|
|
# position.
|
|
try:
|
|
self._safe_write(cursor + text)
|
|
except UnicodeEncodeError:
|
|
# This is probably a sign that the user has the wrong locale.
|
|
# Try to soldier on anyway.
|
|
self._safe_write(cursor + "?" * len(text))
|
|
|
|
# Update cursor position for next time...
|
|
self._cur_x = x + width
|
|
self._cur_y = y
|
|
|
|
def set_title(self, title):
|
|
"""
|
|
Set the title for this terminal/console session. This will
|
|
typically change the text displayed in the window title bar.
|
|
|
|
:param title: The title to be set.
|
|
"""
|
|
if self._start_line is not None:
|
|
self._safe_write("{}{}{}".format(self._start_title, title,
|
|
self._end_title))
|