Files
roadpad/renderer.py.bak

144 lines
4.5 KiB
Python

"""
RoadPad Renderer - Deterministic terminal output.
No dynamic content, no AI references, diff-safe rendering.
Black and white only.
"""
import curses
# Fixed header - exact spacing preserved
HEADER = """\
▗ ▗ ▖ ▖ RoadPad v0.1.0
Lucidia · BlackRoad OS
▘▘ ▝▝ ~"""
HEADER_LINES = HEADER.split("\n")
HEADER_HEIGHT = len(HEADER_LINES) + 1 # +1 for blank line after
# Accept modes
ACCEPT_MODES = ["on", "review", "off"]
class Renderer:
def __init__(self, stdscr):
self.stdscr = stdscr
self.width = 0
self.height = 0
self.accept_mode_idx = 0
self.editor_offset = HEADER_HEIGHT + 5
# No colors - black and white only
curses.start_color()
curses.use_default_colors()
curses.curs_set(1)
def update_size(self) -> tuple[int, int]:
"""Update and return terminal dimensions."""
self.height, self.width = self.stdscr.getmaxyx()
return self.height, self.width
def draw_header(self) -> None:
"""Draw fixed header."""
for i, line in enumerate(HEADER_LINES):
try:
self.stdscr.addstr(i, 0, line)
except curses.error:
pass
def draw_separator(self, row: int, length: int = None) -> None:
"""Draw horizontal separator line."""
if length is None:
length = min(200, self.width - 1)
sep = "" * length
try:
self.stdscr.addstr(row, 0, sep, curses.A_DIM)
except curses.error:
pass
def draw_input_line(self, text: str, placeholder: str = "describe a task to get started") -> None:
"""Draw the active input line with > prompt."""
row = HEADER_HEIGHT
self.draw_separator(row)
input_row = row + 1
display_text = text if text else placeholder
prompt = f"> {display_text}"
try:
self.stdscr.addstr(input_row, 0, prompt[:self.width - 1])
except curses.error:
pass
self.draw_separator(input_row + 1)
def draw_status_bar(self, accept_mode: int, mode: str = "editor", status_text: str = "") -> None:
"""Draw bottom status bar."""
row = HEADER_HEIGHT + 3
self.accept_mode_idx = accept_mode
# Use custom status_text if provided, otherwise default
if status_text:
left = f" {status_text}"
else:
left = f" ⏵⏵ accept edits {ACCEPT_MODES[accept_mode]} (shift+tab to cycle)"
right = f"{mode} mode"
try:
self.stdscr.addstr(row, 0, left, curses.A_DIM)
self.stdscr.addstr(row, self.width - len(right) - 1, right, curses.A_DIM)
except curses.error:
pass
def draw_buffer(self, buffer, scroll_offset: int = 0) -> None:
"""Draw buffer contents in editor area."""
start_row = self.editor_offset
visible_lines = self.height - start_row
for i in range(visible_lines):
line_idx = scroll_offset + i
row = start_row + i
try:
self.stdscr.move(row, 0)
self.stdscr.clrtoeol()
if line_idx < buffer.line_count:
line = buffer.lines[line_idx]
display = line[:self.width - 1]
self.stdscr.addstr(row, 0, display)
except curses.error:
pass
def draw_cursor(self, input_text: str) -> None:
"""Position cursor on the active input line."""
input_row = HEADER_HEIGHT + 1
screen_col = min(3 + len(input_text), self.width - 1)
if 0 <= input_row < self.height - 1:
try:
self.stdscr.move(input_row, screen_col)
except curses.error:
pass
def render(self, buffer, input_text: str, accept_mode: int, scroll_offset: int = 0, mode: str = "editor", status_text: str = "") -> None:
"""Full render pass - deterministic output."""
self.stdscr.erase()
self.update_size()
self.draw_header()
self.draw_input_line(input_text)
self.draw_buffer(buffer, scroll_offset)
self.draw_status_bar(accept_mode, mode, status_text)
self.draw_cursor(input_text)
self.stdscr.refresh()
def show_message(self, msg: str, row: int | None = None) -> None:
"""Show temporary message."""
if row is None:
row = self.height - 2
try:
self.stdscr.addstr(row, 0, msg[:self.width - 1])
self.stdscr.refresh()
except curses.error:
pass