import asyncio import io import math import select import sys import termios import tty import pyvista as pv import kitty class TerminalPlotter(pv.Plotter): def __init__(self, width, height, **kwargs): super().__init__(off_screen=True, window_size=(width, height), **kwargs) self.width = width self.height = height self._running = True h_pix, _ = kitty.get_terminal_cell_size() self.rows, _ = kitty.get_terminal_size() self.start_y, self.start_x = kitty.get_position() # the image requires height/cell_height lines self.needed_lines = math.ceil(height / h_pix) self.set_background([0.0, 0.0, 0.0]) # if we are too close to the bottom of the terminal, create some space. if self.rows - self.start_y < self.needed_lines: missing = self.needed_lines - (self.rows - self.start_y) self.start_y -= missing print("\n" * self.needed_lines, end="") kitty.set_position(self.start_y, self.start_x) def render_to_kitty(self): self.render() buf = io.BytesIO() self.screenshot(buf, transparent_background=True, window_size=(self.width, self.height)) kitty.draw_to_terminal(buf) # print("y:", self.start_y, "rows:", self.rows, end="") kitty.set_position(self.start_y, self.start_x) async def _input_loop(self): fd = sys.stdin.fileno() old_settings = termios.tcgetattr(fd) try: tty.setcbreak(fd) while self._running: rlist, _, _ = select.select([sys.stdin], [], [], 0) if rlist: c = sys.stdin.read(1) if c == "q": # quit on q self._running = False else: self._handle_key(c) await asyncio.sleep(0.01) finally: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) def _handle_key(self, c): if c == "a": self.camera.Azimuth(10) elif c == "d": self.camera.Azimuth(-10) elif c == "w": self.camera.Elevation(10) elif c == "s": self.camera.Elevation(-10) self.render_to_kitty() async def run(self): self.render_to_kitty() await self._input_loop()