144 lines
4.5 KiB
Python
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
|