from .RAIILock import RAIILock import math import os import threading import time class TextCanvas: __DEFAULT_TERMINAL_WIDTH = 80 __DEFAULT_TERMINAL_HEIGHT = 22 __TERMINAL_UPDATE_INTERVAL = 1 __DRAW_RESOLUTION = 3 def __init__(self): self.__terminal_width = self.__DEFAULT_TERMINAL_WIDTH self.__terminal_height = self.__DEFAULT_TERMINAL_HEIGHT self.__last_terminal_dimensions_update = time.time() self._update_terminal_dimensions(force=True) self.__grid = [] self.__render_string = "" self.__is_dirty = True self.__last_render_time = time.time() self.__lock = threading.RLock() def _update_render_if_dirty(self, acquire_locks=True): with RAIILock(self.__lock, defer=not acquire_locks): if self.__is_dirty: self._update_render(acquire_locks=False) def _update_render(self, acquire_locks=True): with RAIILock(self.__lock, defer=not acquire_locks): self.clear(acquire_locks=acquire_locks) self._update_render_string(acquire_locks=acquire_locks) self.__last_render_time = time.time() self.__is_dirty = False def _update_render_string(self, acquire_locks=True): with RAIILock(self.__lock, defer=not acquire_locks): s = "" for line in self.__grid: for c in line: s += c s += "\n" # Don't need that last line feed s = s[:-1] self.__grid_string = s def get(self, acquire_locks=True): with RAIILock(self.__lock, defer=not acquire_locks): self._update_render_if_dirty(acquire_locks=acquire_locks) return self.__grid_string def print(self, acquire_locks=True): with RAIILock(self.__lock, defer=not acquire_locks): s = self.get(acquire_locks=acquire_locks) print(s) def draw_text(self, x, y, text, center=False, acquire_locks=True): with RAIILock(self.__lock, defer=not acquire_locks): if not isinstance(text, str): text = str(text) if len(text) == 0: return if center is True: x -= len(text) / 2 x = int(x) for c in text: self.write(x=x, y=y, character=c, acquire_locks=acquire_locks) x += 1 def draw_line(self, start_x, start_y, end_x, end_y, character="*", acquire_locks=True): with RAIILock(self.__lock, defer=not acquire_locks): distance_x = math.fabs(end_x - start_x) distance_y = math.fabs(end_y - start_y) if distance_x > distance_y: distance = distance_x else: distance = distance_y direction_x = 1 if end_x > start_x else -1 direction_y = 1 if end_y > start_y else -1 steps = round(distance * self.__DRAW_RESOLUTION) if steps == 0: return increment_x = distance_x / steps increment_x *= direction_x increment_y = distance_y / steps increment_y *= direction_y pos_x = start_x pos_y = start_y for i in range(steps): pos_x_rounded = round(pos_x) pos_y_rounded = round(pos_y) self.write(x=pos_x_rounded, y=pos_y_rounded, character=character, acquire_locks=acquire_locks) pos_x += increment_x pos_y += increment_y def write(self, x, y, character, acquire_locks=True): with RAIILock(self.__lock, defer=not acquire_locks): x = round(x) y = round(y) if 0 <= y < len(self.__grid): if 0 <= x < len(self.__grid[y]): self.__grid[y][x] = character self.__is_dirty = True def clear(self, acquire_locks=True): with RAIILock(self.__lock, defer=not acquire_locks): self._update_terminal_dimensions(acquire_locks=acquire_locks) self.__grid = [] for y in range(self.__terminal_height): self.__grid.append([]) for x in range(self.__terminal_width): self.__grid[y].append(" ") self.__is_dirty = True def get_dimensions(self, acquire_locks=True): with RAIILock(self.__lock, defer=not acquire_locks): height = len(self.__grid) if height == 0: width = 0 else: width = len(self.__grid[0]) return width, height def _update_terminal_dimensions(self, force=False, acquire_locks=True): with RAIILock(self.__lock, defer=not acquire_locks): now = time.time() if force is not False and now - self.__last_terminal_dimensions_update < self.__TERMINAL_UPDATE_INTERVAL: return self.__last_terminal_dimensions_update = now width, height = self.determine_terminal_dimensions() self.__terminal_width = width self.__terminal_height = height @staticmethod def determine_terminal_dimensions(): columns, lines = os.get_terminal_size() return columns, lines