import sys import termios from typing import Optional, Sequence, TextIO, Tuple, TYPE_CHECKING from agame.color import colorize from agame.action import Action from agame.dialog import DialogOption from .display import Display if TYPE_CHECKING: from agame.game import Game 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, prompt: Optional[str] = None) -> str: if prompt is None: prompt = self.input_prompt try: self.nocbreak() return 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 dialog_options(self, options: Sequence[DialogOption]) -> DialogOption: valid_opts = range(1, len(options) + 1) def print_opts(): for i, opt in zip(valid_opts, options): # TODO - format/colorize this so it stands out self.line(f"{i}. {opt.text}") print_opts() while True: got = self.input("? ") try: choice = int(got) except ValueError: print_opts() continue if choice in valid_opts: break index = choice - 1 selection = options[index] return selection def finish(self): # Clean up by restoring the old terminal state fd = self.stdin.fileno() termios.tcsetattr(fd, termios.TCSADRAIN, self.__old_attrs) 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 clear(self): self.stdout.write(f"{ESC}[2J") @property def stdin(self) -> TextIO: return self.__stdin @property def stdout(self) -> TextIO: return self.__stdout