Add display abstraction

In case we want to run this on something that isn't an ANSI terminal, we
have the option to implement it however we want.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
This commit is contained in:
2021-11-20 19:38:06 -08:00
parent 4cf3608f71
commit e868d0e14f
8 changed files with 202 additions and 35 deletions

18
agame/display/__init__.py Normal file
View File

@@ -0,0 +1,18 @@
import platform
from .display import Display
if platform.system() in ("Linux", "Darwin", "Unix"):
from .unix import *
class BasicDisplay(Display):
"""
A basic display that should be supported on all platforms. It just outputs
text with some color.
"""
def line(self, line: str = ""):
print(line)
def input(self) -> str:
return input("> ")

32
agame/display/display.py Normal file
View File

@@ -0,0 +1,32 @@
import abc
from typing import Optional
class Display(metaclass=abc.ABCMeta):
"""
Displays are an abstraction over printing things to the screen.
Use a specific implementation based on the kind of display you want to use.
"""
@abc.abstractmethod
def line(self, line: str = ""):
"Print a single line to the display."
@abc.abstractmethod
def input(self) -> str:
"Get player input."
@abc.abstractmethod
def wait_for_ack(self, prompt: Optional[str] = None):
"""
Wait for the user to press a key.
The behavior is up to the implementor. This could be something like
"press any key to continue", "press enter to continue", or "click OK to
continue". This method should block until input is acknowleged by the
user.
"""
def finish(self):
"Final cleanup code for this display."

88
agame/display/unix.py Normal file
View File

@@ -0,0 +1,88 @@
import sys
import termios
from typing import Optional, TextIO
from agame.color import colorize
from .display import Display
ESC = "\u001b"
ANSI_MOVE_BOTTOM = f"{ESC}[999;0f"
class ANSIDisplay(Display):
"""
Display for a modern terminal.
This display attempts to keep the input at the bottom, and the output
scrolling up.
This utilizes `termios` and is therefore UNIX-only.
"""
def __init__(
self,
input_prompt: str = colorize("{{>}} "),
stdin: TextIO = sys.stdin,
stdout: TextIO = sys.stdout,
):
self.input_prompt = input_prompt
self.__stdin = stdin
self.__stdout = stdout
# Initial set-up:
# . Move the cursor to the bottom left part of the screen (this will set
# the current line to a really high value and will move us to the
# bottom of the screen)
self.stdout.write(ANSI_MOVE_BOTTOM)
# . Disable echo, set cbreak
fd = self.stdin.fileno()
self.__old_attrs = termios.tcgetattr(fd)
self.cbreak()
# . Clear
self.clear()
def line(self, line: str = ""):
self.stdout.write(line)
self.stdout.write("\n")
def input(self) -> str:
try:
self.nocbreak()
return input(self.input_prompt)
finally:
self.cbreak()
def wait_for_ack(self, prompt: Optional[str] = "Press any key to continue..."):
# Flush unread data
termios.tcflush(self.stdin.fileno(), termios.TCIFLUSH)
if prompt is not None:
self.line(prompt)
self.stdin.read(1)
def cbreak(self):
fd = self.stdin.fileno()
new = termios.tcgetattr(fd)
new[3] &= ~(termios.ECHO | termios.ICANON)
termios.tcsetattr(fd, termios.TCSADRAIN, new)
def nocbreak(self):
fd = self.stdin.fileno()
new = termios.tcgetattr(fd)
new[3] |= termios.ECHO | termios.ICANON
termios.tcsetattr(fd, termios.TCSADRAIN, new)
def finish(self):
# Clean up by restoring the old terminal state
fd = self.stdin.fileno()
termios.tcsetattr(fd, termios.TCSADRAIN, self.__old_attrs)
def clear(self):
self.stdout.write(f"{ESC}[2J")
@property
def stdin(self) -> TextIO:
return self.__stdin
@property
def stdout(self) -> TextIO:
return self.__stdout