import curses from dataclasses import dataclass from typing import Optional from .big_digits import BIG_DIGITS @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) def init_colors(): """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 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() # Truncate message if too long max_width = width - 4 if len(error_message) > max_width: error_message = error_message[:max_width-3] + "..." # Position near bottom of screen error_y = height - 4 ViewUtils.draw_centered_text( stdscr, error_y, error_message, ColorScheme.ERROR, curses.A_BOLD ) 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 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