pretty sure i fucked up
This commit is contained in:
commit
b5e09107da
101
kitty.py
101
kitty.py
@ -1,21 +1,18 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
from base64 import standard_b64encode
|
||||||
|
from enum import Enum
|
||||||
|
import array
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import fcntl
|
||||||
|
import mmap
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
import select
|
||||||
import sys
|
import sys
|
||||||
import termios
|
import termios
|
||||||
import tty
|
import tty
|
||||||
from base64 import standard_b64encode
|
|
||||||
import array
|
|
||||||
import fcntl
|
|
||||||
|
|
||||||
import zlib
|
import zlib
|
||||||
|
|
||||||
|
|
||||||
import mmap
|
|
||||||
import os
|
|
||||||
import select
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
from terminalio import TerminalIO
|
from terminalio import TerminalIO
|
||||||
|
|
||||||
SHM_NAME = "/kitty-shm-frame"
|
SHM_NAME = "/kitty-shm-frame"
|
||||||
@ -26,6 +23,10 @@ class Kitty_Format(Enum):
|
|||||||
RGBA = 32
|
RGBA = 32
|
||||||
PNG = 100
|
PNG = 100
|
||||||
|
|
||||||
|
class TerminalProtocolError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Terminal():
|
class Terminal():
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.terminal_io = TerminalIO()
|
self.terminal_io = TerminalIO()
|
||||||
@ -42,7 +43,6 @@ class Terminal():
|
|||||||
async def get_terminal_cell_size(self) -> tuple[int, int]:
|
async def get_terminal_cell_size(self) -> tuple[int, int]:
|
||||||
"""Get (height, width) of a single cell in px"""
|
"""Get (height, width) of a single cell in px"""
|
||||||
reply = await self._query_terminal("\x1b[16t", "t")
|
reply = await self._query_terminal("\x1b[16t", "t")
|
||||||
|
|
||||||
match = re.search(r"\[6;(\d+);(\d+)t", reply)
|
match = re.search(r"\[6;(\d+);(\d+)t", reply)
|
||||||
if match:
|
if match:
|
||||||
v_pix, h_pix = map(int, match.groups())
|
v_pix, h_pix = map(int, match.groups())
|
||||||
@ -54,7 +54,6 @@ class Terminal():
|
|||||||
async def get_terminal_size_pixel(self) -> tuple[int, int]:
|
async def get_terminal_size_pixel(self) -> tuple[int, int]:
|
||||||
"""Get (height, width) of the terminal in px"""
|
"""Get (height, width) of the terminal in px"""
|
||||||
reply = await self._query_terminal("\x1b[14t", "t")
|
reply = await self._query_terminal("\x1b[14t", "t")
|
||||||
|
|
||||||
match = re.search(r"\[4;(\d+);(\d+)t", reply)
|
match = re.search(r"\[4;(\d+);(\d+)t", reply)
|
||||||
if match:
|
if match:
|
||||||
height, width = map(int, match.groups())
|
height, width = map(int, match.groups())
|
||||||
@ -120,7 +119,7 @@ class Terminal():
|
|||||||
# set shm name as payload
|
# set shm name as payload
|
||||||
data = SHM_NAME
|
data = SHM_NAME
|
||||||
|
|
||||||
await _write_chunked(**kwargs, data=data)
|
await _write_chunked(**kwargs, data=data, wait_for_ok=True if use_shm else False)
|
||||||
await asyncio.sleep(0.001)
|
await asyncio.sleep(0.001)
|
||||||
|
|
||||||
async def _query_terminal(self, escape: str, endchar: str) -> str:
|
async def _query_terminal(self, escape: str, endchar: str) -> str:
|
||||||
@ -128,7 +127,6 @@ class Terminal():
|
|||||||
Send `escape` to the terminal, read the response until
|
Send `escape` to the terminal, read the response until
|
||||||
`endchar`, return response (including `endchar`)
|
`endchar`, return response (including `endchar`)
|
||||||
"""
|
"""
|
||||||
# Save the current terminal settings
|
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
fd = sys.stdin.fileno()
|
fd = sys.stdin.fileno()
|
||||||
old_settings = termios.tcgetattr(fd)
|
old_settings = termios.tcgetattr(fd)
|
||||||
@ -151,6 +149,24 @@ class Terminal():
|
|||||||
termios.tcsetattr(fd, termios.TCSANOW, old_settings)
|
termios.tcsetattr(fd, termios.TCSANOW, old_settings)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
async def _write_chunked(self, wait_for_ok: bool = True, **cmd) -> None:
|
||||||
|
image_id = cmd['i']
|
||||||
|
if cmd["t"] == "s":
|
||||||
|
data = standard_b64encode(bytes(cmd.pop("data"), "utf-8"))
|
||||||
|
else:
|
||||||
|
data = standard_b64encode(cmd.pop("data"))
|
||||||
|
while data:
|
||||||
|
chunk, data = data[:4096], data[4096:]
|
||||||
|
m = 1 if data else 0
|
||||||
|
payload = _serialize_gr_command(payload=chunk, m=m, **cmd)
|
||||||
|
sys.stdout.buffer.write(payload)
|
||||||
|
sys.stdout.flush()
|
||||||
|
cmd.clear()
|
||||||
|
|
||||||
|
if wait_for_ok:
|
||||||
|
# Wait for terminal to confirm OK response
|
||||||
|
await self.terminal_io.wait_for_ok()
|
||||||
|
|
||||||
def _mmap(np_image, width: int, height: int) -> None:
|
def _mmap(np_image, width: int, height: int) -> None:
|
||||||
"""Write image data to shared memory"""
|
"""Write image data to shared memory"""
|
||||||
shm_size = width * height * 4 # RGBA
|
shm_size = width * height * 4 # RGBA
|
||||||
@ -205,18 +221,59 @@ def _serialize_gr_command(**cmd) -> bytes:
|
|||||||
return b"".join(ans)
|
return b"".join(ans)
|
||||||
|
|
||||||
|
|
||||||
async def _write_chunked(**cmd) -> None:
|
#async def _write_chunked(**cmd) -> None:
|
||||||
|
# if cmd["t"] == "s":
|
||||||
|
# data = standard_b64encode(bytes(cmd.pop("data"), "utf-8"))
|
||||||
|
# else:
|
||||||
|
# data = standard_b64encode(cmd.pop("data"))
|
||||||
|
# while data:
|
||||||
|
# chunk, data = data[:4096], data[4096:]
|
||||||
|
# m = 1 if data else 0
|
||||||
|
# payload = _serialize_gr_command(payload=chunk, m=m, **cmd)
|
||||||
|
# sys.stdout.buffer.write(payload)
|
||||||
|
# sys.stdout.flush()
|
||||||
|
# cmd.clear()
|
||||||
|
|
||||||
|
async def _write_chunked(wait_for_ok: bool = True, **cmd) -> None:
|
||||||
|
image_id = cmd['i']
|
||||||
if cmd["t"] == "s":
|
if cmd["t"] == "s":
|
||||||
data = standard_b64encode(bytes(cmd.pop("data"), "utf-8"))
|
data = standard_b64encode(bytes(cmd.pop("data"), "utf-8"))
|
||||||
else:
|
else:
|
||||||
data = standard_b64encode(cmd.pop("data"))
|
data = standard_b64encode(cmd.pop("data"))
|
||||||
while data:
|
|
||||||
chunk, data = data[:4096], data[4096:]
|
fd = sys.stdin.fileno()
|
||||||
m = 1 if data else 0
|
old_settings = termios.tcgetattr(fd)
|
||||||
payload = _serialize_gr_command(payload=chunk, m=m, **cmd)
|
tty.setraw(fd)
|
||||||
sys.stdout.buffer.write(payload)
|
|
||||||
sys.stdout.flush()
|
loop = asyncio.get_running_loop()
|
||||||
cmd.clear()
|
|
||||||
|
try:
|
||||||
|
while data:
|
||||||
|
chunk, data = data[:4096], data[4096:]
|
||||||
|
m = 1 if data else 0
|
||||||
|
payload = _serialize_gr_command(payload=chunk, m=m, **cmd)
|
||||||
|
sys.stdout.buffer.write(payload)
|
||||||
|
sys.stdout.flush()
|
||||||
|
cmd.clear()
|
||||||
|
|
||||||
|
if wait_for_ok:
|
||||||
|
# Wait for terminal to confirm OK response
|
||||||
|
response = os.read(fd, 1024).decode("utf-8", "ignore")
|
||||||
|
if not response.startswith(f"\033_Gi={image_id};OK"):
|
||||||
|
return
|
||||||
|
#raise RuntimeError(f"Unexpected response: {repr(response)}")
|
||||||
|
#return response.startswith("\033_GOK")
|
||||||
|
# def _read_ok():
|
||||||
|
# response = ""
|
||||||
|
# while not response.endswith("\033\\"):
|
||||||
|
# response += sys.stdin.read(1)
|
||||||
|
# return response
|
||||||
|
|
||||||
|
# response = await loop.run_in_executor(None, _read_ok)
|
||||||
|
# if not response.startswith(f"\033_Gi={image_id};OK"):
|
||||||
|
# raise RuntimeError(f"Unexpected response: {repr(response)}")
|
||||||
|
finally:
|
||||||
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||||
|
|
||||||
def _write_stdout(cmd: str) -> None:
|
def _write_stdout(cmd: str) -> None:
|
||||||
"""Write a command string to stdout and flush."""
|
"""Write a command string to stdout and flush."""
|
||||||
|
@ -56,7 +56,7 @@ class TerminalIO:
|
|||||||
self._buffer += char
|
self._buffer += char
|
||||||
if self._buffer.endswith("\033\\"):
|
if self._buffer.endswith("\033\\"):
|
||||||
#if "\033_GOK" in self._buffer:
|
#if "\033_GOK" in self._buffer:
|
||||||
if re.search('\033_G(i=\d+;)?OK', self._buffer):
|
if re.search('\033_G(i=\\d+;)?OK', self._buffer):
|
||||||
self.ok_event.set()
|
self.ok_event.set()
|
||||||
self._buffer = ""
|
self._buffer = ""
|
||||||
continue
|
continue
|
||||||
@ -65,4 +65,5 @@ class TerminalIO:
|
|||||||
if not char.startswith("\033"):
|
if not char.startswith("\033"):
|
||||||
for callback in self.input_callbacks:
|
for callback in self.input_callbacks:
|
||||||
callback(char)
|
callback(char)
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
|
|
||||||
|
@ -29,12 +29,13 @@ class TerminalPlotter(pv.Plotter):
|
|||||||
self.set_background([0.0, 0.0, 0.0])
|
self.set_background([0.0, 0.0, 0.0])
|
||||||
self.ren_win.SetAlphaBitPlanes(1)
|
self.ren_win.SetAlphaBitPlanes(1)
|
||||||
self.ren_win.SetMultiSamples(0)
|
self.ren_win.SetMultiSamples(0)
|
||||||
|
self.terminal = kitty.Terminal()
|
||||||
|
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
h_pix, _ = await kitty.get_terminal_cell_size()
|
h_pix, _ = await self.terminal.get_terminal_cell_size()
|
||||||
self.rows, _ = kitty.get_terminal_size()
|
self.rows, _ = self.terminal.get_terminal_size()
|
||||||
self.start_y, self.start_x = await kitty.get_position()
|
self.start_y, self.start_x = await self.terminal.get_position()
|
||||||
self.needed_lines = math.ceil(self.height / h_pix)
|
self.needed_lines = math.ceil(self.height / h_pix)
|
||||||
|
|
||||||
# Add vertical space if needed
|
# Add vertical space if needed
|
||||||
@ -42,41 +43,61 @@ class TerminalPlotter(pv.Plotter):
|
|||||||
missing = self.needed_lines - (self.rows - self.start_y)
|
missing = self.needed_lines - (self.rows - self.start_y)
|
||||||
self.start_y -= missing
|
self.start_y -= missing
|
||||||
print("\n" * self.needed_lines, end="")
|
print("\n" * self.needed_lines, end="")
|
||||||
kitty.set_position(self.start_y, self.start_x)
|
self.terminal.set_position(self.start_y, self.start_x)
|
||||||
self.orbit = self.generate_orbital_path()
|
self.orbit = self.generate_orbital_path()
|
||||||
|
|
||||||
async def _handle_key(self, c):
|
def _handle_key(c):
|
||||||
if c == "a":
|
if c == "a":
|
||||||
self.camera.Azimuth(10)
|
self.camera.Azimuth(10)
|
||||||
elif c == "d":
|
elif c == "d":
|
||||||
self.camera.Azimuth(-10)
|
self.camera.Azimuth(-10)
|
||||||
elif c == "w":
|
elif c == "w":
|
||||||
self.camera.Elevation(10)
|
self.camera.Elevation(10)
|
||||||
elif c == "s":
|
elif c == "s":
|
||||||
self.camera.Elevation(-10)
|
self.camera.Elevation(-10)
|
||||||
elif c == "+":
|
elif c == "+":
|
||||||
self.camera.zoom(1.1)
|
self.camera.zoom(1.1)
|
||||||
elif c == "-":
|
elif c == "-":
|
||||||
self.camera.zoom(0.9)
|
self.camera.zoom(0.9)
|
||||||
elif c == "p":
|
elif c == "p":
|
||||||
self.fly_to(self.orbit.points[15])
|
self.fly_to(self.orbit.points[15])
|
||||||
|
elif c == "q":
|
||||||
|
self._running = False
|
||||||
|
self.terminal.terminal_io.register_input_callback(_handle_key)
|
||||||
|
|
||||||
async def _input_loop(self):
|
|
||||||
fd = sys.stdin.fileno()
|
# async def _handle_key(self, c):
|
||||||
old_settings = termios.tcgetattr(fd)
|
# if c == "a":
|
||||||
try:
|
# self.camera.Azimuth(10)
|
||||||
tty.setcbreak(fd)
|
# elif c == "d":
|
||||||
while self._running:
|
# self.camera.Azimuth(-10)
|
||||||
rlist, _, _ = select.select([sys.stdin], [], [], 0)
|
# elif c == "w":
|
||||||
if rlist:
|
# self.camera.Elevation(10)
|
||||||
c = sys.stdin.read(1)
|
# elif c == "s":
|
||||||
if c == "q": # quit on q
|
# self.camera.Elevation(-10)
|
||||||
self._running = False
|
# elif c == "+":
|
||||||
else:
|
# self.camera.zoom(1.1)
|
||||||
await self._handle_key(c)
|
# elif c == "-":
|
||||||
await asyncio.sleep(0.001)
|
# self.camera.zoom(0.9)
|
||||||
finally:
|
# elif c == "p":
|
||||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
# self.fly_to(self.orbit.points[15])
|
||||||
|
|
||||||
|
#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:
|
||||||
|
# await self._handle_key(c)
|
||||||
|
# await asyncio.sleep(0.001)
|
||||||
|
# finally:
|
||||||
|
# termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||||
|
|
||||||
async def _render_loop(self):
|
async def _render_loop(self):
|
||||||
import time
|
import time
|
||||||
@ -93,22 +114,23 @@ class TerminalPlotter(pv.Plotter):
|
|||||||
np_image = numpy_support.vtk_to_numpy(vtk_array).reshape(height, width, 4)
|
np_image = numpy_support.vtk_to_numpy(vtk_array).reshape(height, width, 4)
|
||||||
np_image = np_image[::-1] # Flip vertically
|
np_image = np_image[::-1] # Flip vertically
|
||||||
np_image = np.ascontiguousarray(np_image) # Ensure memory layout is C-contiguous
|
np_image = np.ascontiguousarray(np_image) # Ensure memory layout is C-contiguous
|
||||||
print("Render & copy:", time.perf_counter() - start)
|
#print("Render & copy:", time.perf_counter() - start)
|
||||||
|
|
||||||
#await kitty.draw_to_terminal(np_image, width, height, compress=True)
|
#await self.terminal.draw_to_terminal(np_image, width, height, compress=True)
|
||||||
await kitty.draw_to_terminal(np_image, width, height, use_shm=True)
|
await self.terminal.draw(np_image, width, height, use_shm=True)
|
||||||
print("Draw:", time.perf_counter() - start)
|
#print("Draw:", time.perf_counter() - start)
|
||||||
kitty.set_position(self.start_y, self.start_x)
|
self.terminal.set_position(self.start_y, self.start_x)
|
||||||
self.camera.Azimuth(1)
|
self.camera.Azimuth(1)
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
await self.initialize()
|
await self.initialize()
|
||||||
self.iren.initialize()
|
self.iren.initialize()
|
||||||
tasks = [
|
#tasks = [
|
||||||
asyncio.create_task(self._input_loop()),
|
# #asyncio.create_task(self._input_loop()),
|
||||||
asyncio.create_task(self._render_loop()),
|
# asyncio.create_task(self._render_loop()),
|
||||||
]
|
#]
|
||||||
await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
|
#await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
|
||||||
|
await self._render_loop()
|
||||||
self._running = False
|
self._running = False
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
|
Loading…
Reference in New Issue
Block a user