Initial Commit
This commit is contained in:
165
v2a-player/v2a_player/terminal.py
Normal file
165
v2a-player/v2a_player/terminal.py
Normal file
@@ -0,0 +1,165 @@
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import math
|
||||
from typing import Tuple, Optional
|
||||
|
||||
def get_terminal_size() -> Tuple[int, int]:
|
||||
size = shutil.get_terminal_size()
|
||||
return (size.columns, size.lines)
|
||||
|
||||
def calculate_scaled_dimensions(
|
||||
src_width: int,
|
||||
src_height: int,
|
||||
max_width: Optional[int] = None,
|
||||
max_height: Optional[int] = None,
|
||||
) -> Tuple[int, int]:
|
||||
if max_width is None or max_height is None:
|
||||
max_width, max_height = get_terminal_size()
|
||||
|
||||
max_height = max_height - 2
|
||||
|
||||
if src_width <= max_width and src_height <= max_height:
|
||||
return (src_width, src_height)
|
||||
|
||||
width_scale = max_width / (2.0 * src_width)
|
||||
height_scale = max_height / src_height
|
||||
scale = min(width_scale, height_scale)
|
||||
|
||||
scaled_width = int(2.0 * scale * src_width)
|
||||
scaled_height = int(scale * src_height)
|
||||
|
||||
scaled_width = max(1, scaled_width)
|
||||
scaled_height = max(1, scaled_height)
|
||||
|
||||
return (scaled_width, scaled_height)
|
||||
|
||||
def calculate_centering_offset(
|
||||
src_width: int,
|
||||
src_height: int,
|
||||
container_width: int,
|
||||
container_height: int,
|
||||
) -> Tuple[int, int]:
|
||||
x = (container_width - src_width) // 2
|
||||
y = (container_height - src_height) // 2
|
||||
return (max(0, x), max(0, y))
|
||||
|
||||
def ansi_color_fg(index: int) -> str:
|
||||
return f"\x1b[38;5;{index}m"
|
||||
|
||||
def ansi_color_bg(index: int) -> str:
|
||||
return f"\x1b[48;5;{index}m"
|
||||
|
||||
def ansi_reset() -> str:
|
||||
return "\x1b[0m"
|
||||
|
||||
def ansi_clear_screen() -> str:
|
||||
return "\x1b[2J\x1b[H"
|
||||
|
||||
def ansi_move_cursor(row: int, col: int) -> str:
|
||||
return f"\x1b[{row};{col}H"
|
||||
|
||||
def ansi_hide_cursor() -> str:
|
||||
return "\x1b[?25l"
|
||||
|
||||
def ansi_show_cursor() -> str:
|
||||
return "\x1b[?25h"
|
||||
|
||||
def render_half_block(top_color: int, bottom_color: int) -> str:
|
||||
if top_color == bottom_color:
|
||||
|
||||
return f"{ansi_color_fg(top_color)}█{ansi_reset()}"
|
||||
else:
|
||||
return f"{ansi_color_fg(top_color)}{ansi_color_bg(bottom_color)}▀{ansi_reset()}"
|
||||
|
||||
class TerminalRenderer:
|
||||
|
||||
def __init__(self):
|
||||
self.term_width, self.term_height = get_terminal_size()
|
||||
self.scaled_width = 0
|
||||
self.scaled_height = 0
|
||||
self.src_width = 0
|
||||
self.src_height = 0
|
||||
self.offset_x = 0
|
||||
self.offset_y = 0
|
||||
|
||||
def update_layout(self, src_width: int, src_height: int):
|
||||
self.src_width = src_width
|
||||
self.src_height = src_height
|
||||
self.scaled_width, self.scaled_height = calculate_scaled_dimensions(
|
||||
src_width, src_height, self.term_width, self.term_height
|
||||
)
|
||||
self.offset_x, self.offset_y = calculate_centering_offset(
|
||||
self.scaled_width, self.scaled_height, self.term_width, self.term_height
|
||||
)
|
||||
|
||||
def check_resize(self) -> bool:
|
||||
new_width, new_height = get_terminal_size()
|
||||
if new_width != self.term_width or new_height != self.term_height:
|
||||
self.term_width, self.term_height = new_width, new_height
|
||||
if self.src_width > 0 and self.src_height > 0:
|
||||
self.update_layout(self.src_width, self.src_height)
|
||||
return True
|
||||
return False
|
||||
|
||||
def render_frame(self, frame_pixel_pairs, frame_width: int, frame_height: int) -> str:
|
||||
|
||||
|
||||
if (self.scaled_width, self.scaled_height) != (frame_width, frame_height):
|
||||
|
||||
return self._render_scaled_frame(frame_pixel_pairs, frame_width, frame_height)
|
||||
else:
|
||||
return self._render_exact_frame(frame_pixel_pairs, frame_width, frame_height)
|
||||
|
||||
def _render_exact_frame(self, pixel_pairs, width: int, height: int) -> str:
|
||||
result = []
|
||||
for y in range(height):
|
||||
|
||||
row = self.offset_y + 1 + y
|
||||
col = self.offset_x + 1
|
||||
result.append(ansi_move_cursor(row, col))
|
||||
line_parts = []
|
||||
for x in range(width):
|
||||
idx = y * width + x
|
||||
top, bottom = pixel_pairs[idx]
|
||||
line_parts.append(render_half_block(top, bottom))
|
||||
result.append("".join(line_parts))
|
||||
return "".join(result)
|
||||
|
||||
def _render_scaled_frame(self, pixel_pairs, src_width: int, src_height: int) -> str:
|
||||
dst_width, dst_height = self.scaled_width, self.scaled_height
|
||||
result = []
|
||||
for dy in range(dst_height):
|
||||
|
||||
row = self.offset_y + 1 + dy
|
||||
col = self.offset_x + 1
|
||||
result.append(ansi_move_cursor(row, col))
|
||||
|
||||
sy = int(dy * src_height / dst_height)
|
||||
line_parts = []
|
||||
for dx in range(dst_width):
|
||||
sx = int(dx * src_width / dst_width)
|
||||
idx = sy * src_width + sx
|
||||
top, bottom = pixel_pairs[idx]
|
||||
line_parts.append(render_half_block(top, bottom))
|
||||
result.append("".join(line_parts))
|
||||
return "".join(result)
|
||||
|
||||
def prepare_display(self) -> str:
|
||||
return ansi_clear_screen() + ansi_hide_cursor()
|
||||
|
||||
def restore_display(self) -> str:
|
||||
return ansi_show_cursor() + ansi_clear_screen()
|
||||
|
||||
def frame_prefix(self) -> str:
|
||||
return ansi_move_cursor(self.offset_y + 1, self.offset_x + 1)
|
||||
|
||||
def clear_video_area(self) -> str:
|
||||
if self.scaled_width <= 0 or self.scaled_height <= 0:
|
||||
return ""
|
||||
result = []
|
||||
for row in range(self.offset_y + 1, self.offset_y + self.scaled_height + 1):
|
||||
result.append(ansi_move_cursor(row, self.offset_x + 1))
|
||||
result.append(ansi_reset())
|
||||
result.append(" " * self.scaled_width)
|
||||
return "".join(result)
|
||||
Reference in New Issue
Block a user