kglobe/kitty.py
Felix Pankratz 2d22c99c92 docstrings
2025-07-21 14:42:28 +02:00

157 lines
4.2 KiB
Python
Executable File

#!/usr/bin/env python3
import re
import sys
import termios
import tty
from base64 import standard_b64encode
from io import BytesIO
import array
import fcntl
def draw_to_terminal(buffer: BytesIO) -> None:
'''Display a PNG image in the terminal.'''
write_chunked(a="T", i=1, f=100, q=2, data=buffer.getvalue())
def get_terminal_cell_size() -> tuple[int, int]:
'''Get (height, width) of a single cell in px'''
reply = _query_terminal("\x1b[16t", "t")
match = re.search(r"\[6;(\d+);(\d+)t", reply)
if match:
v_pix, h_pix = map(int, match.groups())
return v_pix, h_pix
else:
print(reply)
raise ValueError("Failed to parse terminal cell size response")
def get_terminal_size_pixel() -> tuple[int, int]:
'''Get (height, width) of the terminal in px'''
reply = _query_terminal("\x1b[14t", "t")
match = re.search(r"\[4;(\d+);(\d+)t", reply)
if match:
height, width = map(int, match.groups())
return height, width
else:
raise ValueError("Failed to parse terminal pixel size response")
def get_terminal_size() -> tuple[int, int]:
'''Get (rows, cols) of the terminal'''
buf = array.array('H', [0, 0, 0, 0])
fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, buf)
rows, cols, width, height = buf
return (rows, cols)
def serialize_gr_command(**cmd) -> bytes:
payload = cmd.pop("payload", None)
cmd = ",".join(f"{k}={v}" for k, v in cmd.items())
ans = []
w = ans.append
w(b"\033_G"), w(cmd.encode("ascii"))
if payload:
w(b";")
w(payload)
w(b"\033\\")
return b"".join(ans)
def write_chunked(**cmd) -> None:
data = standard_b64encode(cmd.pop("data"))
while data:
chunk, data = data[:4096], data[4096:]
m = 1 if data else 0
sys.stdout.buffer.write(serialize_gr_command(payload=chunk, m=m, **cmd))
sys.stdout.flush()
cmd.clear()
def hide_cursor() -> None:
'''Tell the terminal to hide the cursor.'''
_write_stdout("\x1b[?25l")
def show_cursor() -> None:
'''Tell the terminal to show the cursor.'''
_write_stdout("\x1b[?25h")
def set_position(y: int, x: int) -> None:
'''Set the cursor position to y, x'''
_write_stdout(f"\x1b[{y};{x}H")
def _write_stdout(cmd: str) -> None:
sys.stdout.write(cmd)
sys.stdout.flush()
def _query_terminal(escape: str, endchar: str) -> str:
'''
Send `escape` to the terminal, read the response until
`endchar`, return response (including `endchar`)
'''
# Save the current terminal settings
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
# Set terminal to raw mode
tty.setraw(fd)
_write_stdout(escape)
response = ""
while True:
ch = sys.stdin.read(1)
response += ch
if ch == endchar:
break
finally:
# Restore the terminal settings
termios.tcsetattr(fd, termios.TCSANOW, old_settings)
return response
def get_position() -> tuple[int, int]:
'''Get the (y, x) position of the cursor'''
reply = _query_terminal("\x1b[6n", "R")
match = re.search(r"\[(\d+);(\d+)R", reply)
if match:
y, x = map(int, match.groups())
return y, x
else:
raise ValueError("Failed to parse cursor position response")
if __name__ == "__main__":
import pyvista as pv
cols, rows = get_terminal_size()
v_pix, h_pix = get_terminal_cell_size()
height, width = get_terminal_size_pixel()
y, x = get_position()
print(f'Terminal has {rows} rows, {cols} cols = {rows * cols} cells')
print(f'Cell size: {h_pix}x{v_pix} px')
print(f'Dimensions: {width}x{height} px')
new_lines = int((height/v_pix)-6)
print('\n' * new_lines, end='')
set_position(y - new_lines - 6, x)
s = pv.Sphere()
pl = pv.Plotter(off_screen=True)
pl.add_mesh(s)
b = BytesIO()
pl.screenshot(b, transparent_background=True, window_size=(width, height))
# i = Image.new("RGB", (100, 100), (0, 0, 0))
# d = ImageDraw.Draw(i)
# d.ellipse([(5, 5), (95, 95)])
# d.ellipse([(10, 10), (90, 90)])
# d.line(((50, 0), (50, 100)))
# d.line(((0, 50), (100, 50)))
draw_to_terminal(b)