2025-02-06 23:04:50 +02:00
|
|
|
import curses
|
2025-02-09 22:31:24 +02:00
|
|
|
from dataclasses import dataclass
|
|
|
|
from typing import Optional
|
2025-02-06 23:04:50 +02:00
|
|
|
from .big_digits import BIG_DIGITS
|
|
|
|
|
2025-02-09 22:31:24 +02:00
|
|
|
@dataclass
|
|
|
|
class ColorScheme:
|
|
|
|
"""Color scheme configuration for the UI"""
|
|
|
|
PRIMARY = 1 # Green on black
|
|
|
|
HIGHLIGHT = 2 # Yellow on black
|
|
|
|
ERROR = 3 # Red on black
|
|
|
|
|
|
|
|
class ViewUtils:
|
|
|
|
"""Common utility functions for view classes"""
|
|
|
|
@staticmethod
|
|
|
|
def center_x(width: int, text_width: int) -> int:
|
|
|
|
"""Calculate x coordinate to center text horizontally"""
|
|
|
|
return max(0, (width - text_width) // 2)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def center_y(height: int, text_height: int) -> int:
|
|
|
|
"""Calculate y coordinate to center text vertically"""
|
|
|
|
return max(0, (height - text_height) // 2)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def draw_centered_text(stdscr, y: int, text: str, color_pair: Optional[int] = None, attrs: int = 0):
|
|
|
|
"""Draw text centered horizontally on the screen with optional color and attributes"""
|
|
|
|
height, width = stdscr.getmaxyx()
|
|
|
|
x = ViewUtils.center_x(width, len(text))
|
|
|
|
|
|
|
|
if color_pair is not None:
|
|
|
|
stdscr.attron(curses.color_pair(color_pair) | attrs)
|
|
|
|
|
|
|
|
try:
|
|
|
|
stdscr.addstr(y, x, text)
|
|
|
|
except curses.error:
|
|
|
|
pass # Ignore errors from writing at invalid positions
|
|
|
|
|
|
|
|
if color_pair is not None:
|
|
|
|
stdscr.attroff(curses.color_pair(color_pair) | attrs)
|
2025-02-06 23:04:50 +02:00
|
|
|
|
|
|
|
def init_colors():
|
2025-02-09 22:31:24 +02:00
|
|
|
"""Initialize color pairs for the application"""
|
|
|
|
try:
|
|
|
|
curses.start_color()
|
|
|
|
curses.use_default_colors()
|
|
|
|
|
|
|
|
# Primary color (green text on black background)
|
|
|
|
curses.init_pair(ColorScheme.PRIMARY, curses.COLOR_GREEN, curses.COLOR_BLACK)
|
|
|
|
|
|
|
|
# Highlight color (yellow text on black background)
|
|
|
|
curses.init_pair(ColorScheme.HIGHLIGHT, curses.COLOR_YELLOW, curses.COLOR_BLACK)
|
|
|
|
|
|
|
|
# Error color (red text on black background)
|
|
|
|
curses.init_pair(ColorScheme.ERROR, curses.COLOR_RED, curses.COLOR_BLACK)
|
|
|
|
except Exception as e:
|
|
|
|
# Log error or handle gracefully if color initialization fails
|
|
|
|
pass
|
2025-02-06 23:04:50 +02:00
|
|
|
|
2025-02-09 22:31:24 +02:00
|
|
|
def draw_error(stdscr, error_message: str, duration_sec: int = 3):
|
|
|
|
"""
|
|
|
|
Draw error message at the bottom of the screen
|
|
|
|
|
|
|
|
Args:
|
|
|
|
stdscr: Curses window object
|
|
|
|
error_message: Message to display
|
|
|
|
duration_sec: How long the error should be displayed (for reference by caller)
|
|
|
|
"""
|
|
|
|
height, width = stdscr.getmaxyx()
|
|
|
|
|
2025-02-06 23:04:50 +02:00
|
|
|
# Truncate message if too long
|
2025-02-09 22:31:24 +02:00
|
|
|
max_width = width - 4
|
|
|
|
if len(error_message) > max_width:
|
|
|
|
error_message = error_message[:max_width-3] + "..."
|
2025-02-06 23:04:50 +02:00
|
|
|
|
2025-02-09 22:31:24 +02:00
|
|
|
# Position near bottom of screen
|
|
|
|
error_y = height - 4
|
|
|
|
|
|
|
|
ViewUtils.draw_centered_text(
|
|
|
|
stdscr,
|
|
|
|
error_y,
|
|
|
|
error_message,
|
|
|
|
ColorScheme.ERROR,
|
|
|
|
curses.A_BOLD
|
|
|
|
)
|
2025-02-06 23:04:50 +02:00
|
|
|
|
2025-02-09 22:31:24 +02:00
|
|
|
def draw_big_digit(stdscr, y: int, x: int, digit: str):
|
|
|
|
"""
|
|
|
|
Draw a large digit using the predefined patterns
|
|
|
|
|
|
|
|
Args:
|
|
|
|
stdscr: Curses window object
|
|
|
|
y: Starting y coordinate
|
|
|
|
x: Starting x coordinate
|
|
|
|
digit: Character to draw ('0'-'9', ':', etc)
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
patterns = BIG_DIGITS.get(digit, BIG_DIGITS['?'])
|
|
|
|
for i, line in enumerate(patterns):
|
|
|
|
try:
|
|
|
|
stdscr.addstr(y + i, x, line)
|
|
|
|
except curses.error:
|
|
|
|
continue # Skip lines that would write outside the window
|
|
|
|
except (curses.error, IndexError):
|
|
|
|
pass # Ignore any drawing errors
|
2025-02-06 23:04:50 +02:00
|
|
|
|
2025-02-09 22:31:24 +02:00
|
|
|
def safe_addstr(stdscr, y: int, x: int, text: str, color_pair: Optional[int] = None, attrs: int = 0):
|
|
|
|
"""
|
|
|
|
Safely add a string to the screen, handling boundary conditions
|
|
|
|
|
|
|
|
Args:
|
|
|
|
stdscr: Curses window object
|
|
|
|
y: Y coordinate
|
|
|
|
x: X coordinate
|
|
|
|
text: Text to draw
|
|
|
|
color_pair: Optional color pair number
|
|
|
|
attrs: Additional curses attributes
|
|
|
|
"""
|
|
|
|
height, width = stdscr.getmaxyx()
|
|
|
|
|
|
|
|
# Check if the position is within bounds
|
|
|
|
if y < 0 or y >= height or x < 0 or x >= width:
|
|
|
|
return
|
|
|
|
|
|
|
|
# Truncate text if it would extend beyond screen width
|
|
|
|
if x + len(text) > width:
|
|
|
|
text = text[:width - x]
|
|
|
|
|
|
|
|
try:
|
|
|
|
if color_pair is not None:
|
|
|
|
stdscr.attron(curses.color_pair(color_pair) | attrs)
|
|
|
|
|
|
|
|
stdscr.addstr(y, x, text)
|
|
|
|
|
|
|
|
if color_pair is not None:
|
|
|
|
stdscr.attroff(curses.color_pair(color_pair) | attrs)
|
|
|
|
except curses.error:
|
|
|
|
pass # Ignore any drawing errors
|