import curses from datetime import datetime from big_digits import BIG_DIGITS def _init_colors(): """Initialize color pairs matching specification""" curses.start_color() curses.use_default_colors() # Green text on black background (primary color) curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK) # Highlight color (yellow) curses.init_pair(2, curses.COLOR_YELLOW, curses.COLOR_BLACK) # Error color (red) curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK) def _draw_error(stdscr, error_message): """Draw error message following specification""" height, width = stdscr.getmaxyx() # Truncate message if too long error_message = error_message[:width-4] error_x = max(0, width // 2 - len(error_message) // 2) error_y = height - 4 # Show near bottom of screen stdscr.attron(curses.color_pair(3) | curses.A_BOLD) stdscr.addstr(error_y, error_x, error_message) stdscr.attroff(curses.color_pair(3) | curses.A_BOLD) def _draw_active_alarms(stdscr, context): """Draw the active alarms""" _init_colors() height, width = stdscr.getmaxyx() current_time = datetime.now() # Draw the main clock (original position) time_str = current_time.strftime("%H:%M:%S") digit_width = 14 total_width = digit_width * len(time_str) start_x = (width - total_width) // 2 start_y = (height - 7) // 2 - 4 # Original position from _draw_main_clock # Draw blinking dot if int(current_time.timestamp()) % 2 == 0: stdscr.attron(curses.color_pair(1)) stdscr.addstr(start_y - 1, start_x + total_width - 2, "•") stdscr.attroff(curses.color_pair(1)) # Green color for big time stdscr.attron(curses.color_pair(1)) for i, digit in enumerate(time_str): _draw_big_digit(stdscr, start_y, start_x + i * digit_width, digit) stdscr.attroff(curses.color_pair(1)) # Draw date (as in main clock) date_str = current_time.strftime("%Y-%m-%d") date_x = width // 2 - len(date_str) // 2 date_y = height // 2 + 4 stdscr.attron(curses.color_pair(2)) stdscr.addstr(date_y, date_x, date_str) stdscr.attroff(curses.color_pair(2)) # Get active alarm info active_alarms = context.get('active_alarms', {}) if not active_alarms: return # Get the first (or only) active alarm alarm_id = list(active_alarms.keys())[0] alarm_info = active_alarms[alarm_id] alarm_config = alarm_info['config'] # Format alarm info alarm_name = alarm_config.get('name', 'Unnamed Alarm') alarm_time = alarm_config.get('time', 'Unknown Time') #snooze_count = alarm_info.get('snooze_count', 0) # Draw alarm info under the clock info_y = start_y + 8 # Position below the clock #alarm_str = f"[ {alarm_name} - {alarm_time} - Snoozed: {snooze_count}x ]" alarm_str = f"[ {alarm_name} - {alarm_time} ]" alarm_x = max(0, width // 2 - len(alarm_str) // 2) alarm_y = date_y - 2 # Just above the date # Center the alarm info info_x = max(0, width // 2 - len(alarm_str) // 2) # Draw with green color stdscr.attron(curses.color_pair(1)) stdscr.addstr(alarm_y, alarm_x, alarm_str) stdscr.attroff(curses.color_pair(1)) # Instructions stdscr.addstr(height - 2, width // 2 - 15, "S: Snooze D: Dismiss") def _draw_big_digit(stdscr, y, x, digit): """Draw a big digit using predefined patterns""" patterns = BIG_DIGITS[digit] for i, line in enumerate(patterns): stdscr.addstr(y + i, x, line) def _draw_main_clock(stdscr, context=None): """Draw the main clock screen""" _init_colors() height, width = stdscr.getmaxyx() current_time = datetime.now() # Big time display time_str = current_time.strftime("%H:%M:%S") digit_width = 14 # Width of each digit pattern total_width = digit_width * len(time_str) start_x = (width - total_width) // 2 start_y = (height - 7) // 2 - 4 # Green color for big time stdscr.attron(curses.color_pair(1)) for i, digit in enumerate(time_str): _draw_big_digit(stdscr, start_y, start_x + i * digit_width, digit) stdscr.attroff(curses.color_pair(1)) # Date display date_str = current_time.strftime("%Y-%m-%d") date_x = width // 2 - len(date_str) // 2 date_y = height // 2 + 4 stdscr.attron(curses.color_pair(2)) stdscr.addstr(date_y, date_x, date_str) stdscr.attroff(curses.color_pair(2)) # Menu options menu_str = "A: Add Alarm S: List Alarms Q: Quit" menu_x = width // 2 - len(menu_str) // 2 stdscr.addstr(height - 2, menu_x, menu_str) def _draw_add_alarm(stdscr, context): """Draw the add alarm screen""" # Ensure context is a dictionary with default values if context is None: context = {} # Provide default values with more explicit checks context = { 'new_alarm_selected': context.get('new_alarm_selected', 0), 'new_alarm_name': context.get('new_alarm_name', 'New Alarm'), 'new_alarm_hour': context.get('new_alarm_hour', datetime.now().hour), 'new_alarm_minute': context.get('new_alarm_minute', datetime.now().minute), 'new_alarm_enabled': context.get('new_alarm_enabled', True), 'new_alarm_date': context.get('new_alarm_date') or None, 'new_alarm_weekdays': context.get('new_alarm_weekdays', []) or [], 'weekday_names': context.get('weekday_names', ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']) } _init_colors() height, width = stdscr.getmaxyx() # Center the form vertically with good spacing form_y = height // 2 - 6 # Title with green color and bold title = "Add New Alarm" stdscr.attron(curses.color_pair(1) | curses.A_BOLD) stdscr.addstr(form_y, width // 2 - len(title) // 2, title) stdscr.attroff(curses.color_pair(1) | curses.A_BOLD) # Helper function to draw a labeled field def draw_field(y, label, value, is_selected, center_offset=0): label_str = f"{label}: " total_width = len(label_str) + len(str(value)) x = width // 2 - total_width // 2 + center_offset # Draw label in green stdscr.attron(curses.color_pair(1)) stdscr.addstr(y, x, label_str) stdscr.attroff(curses.color_pair(1)) # Draw value (highlighted if selected) if is_selected: stdscr.attron(curses.color_pair(2)) # Yellow for selected stdscr.addstr(y, x + len(label_str), str(value)) stdscr.attroff(curses.color_pair(2)) else: stdscr.attron(curses.color_pair(1)) # Green for normal stdscr.addstr(y, x + len(label_str), str(value)) stdscr.attroff(curses.color_pair(1)) # Name field with proper spacing name_str = str(context['new_alarm_name']) draw_field(form_y + 2, "Name", name_str, context['new_alarm_selected'] == 4) # Time selection with centered layout time_y = form_y + 4 time_label = "Time: " hour_str = f"{int(context['new_alarm_hour']):02d}" minute_str = f"{int(context['new_alarm_minute']):02d}" # Calculate center position for time time_total_width = len(time_label) + 5 # 5 = HH:MM time_x = width // 2 - time_total_width // 2 # Draw time label stdscr.attron(curses.color_pair(1)) stdscr.addstr(time_y, time_x, time_label) stdscr.attroff(curses.color_pair(1)) # Draw hour if context['new_alarm_selected'] == 0: stdscr.attron(curses.color_pair(2)) else: stdscr.attron(curses.color_pair(1)) stdscr.addstr(time_y, time_x + len(time_label), hour_str) if context['new_alarm_selected'] == 0: stdscr.attroff(curses.color_pair(2)) else: stdscr.attroff(curses.color_pair(1)) # Draw colon stdscr.attron(curses.color_pair(1)) stdscr.addstr(time_y, time_x + len(time_label) + 2, ":") stdscr.attroff(curses.color_pair(1)) # Draw minute if context['new_alarm_selected'] == 1: stdscr.attron(curses.color_pair(2)) else: stdscr.attron(curses.color_pair(1)) stdscr.addstr(time_y, time_x + len(time_label) + 3, minute_str) if context['new_alarm_selected'] == 1: stdscr.attroff(curses.color_pair(2)) else: stdscr.attroff(curses.color_pair(1)) # Date selection if context['new_alarm_date'] and hasattr(context['new_alarm_date'], 'strftime'): date_str = context['new_alarm_date'].strftime("%Y-%m-%d") else: date_str = 'No specific date' draw_field(form_y + 6, "Date", date_str, context['new_alarm_selected'] == 2) # Weekday selection with improved visual style weekday_y = form_y + 8 weekday_label = "Repeat: " weekdays_str = " ".join( f"[{context['weekday_names'][i]}]" if i in context['new_alarm_weekdays'] else f" {context['weekday_names'][i]} " for i in range(len(context['weekday_names'])) ) # Center weekdays total_width = len(weekday_label) + len(weekdays_str) weekday_x = width // 2 - total_width // 2 # Draw weekday selection stdscr.attron(curses.color_pair(1)) stdscr.addstr(weekday_y, weekday_x, weekday_label) stdscr.attroff(curses.color_pair(1)) if context['new_alarm_selected'] == 3: stdscr.attron(curses.color_pair(2)) else: stdscr.attron(curses.color_pair(1)) stdscr.addstr(weekday_y, weekday_x + len(weekday_label), weekdays_str) if context['new_alarm_selected'] == 3: stdscr.attroff(curses.color_pair(2)) else: stdscr.attroff(curses.color_pair(1)) # Enabled/Disabled toggle with visual indicator status_y = form_y + 10 enabled_str = "● Enabled" if context['new_alarm_enabled'] else "○ Disabled" draw_field(status_y, "Status", enabled_str, context['new_alarm_selected'] == 5, -2) # Instructions in green at the bottom instructions = "j/k: Change h/l: Switch Space: Toggle Enter: Save Esc: Cancel" stdscr.attron(curses.color_pair(1)) stdscr.addstr(height - 2, width // 2 - len(instructions) // 2, instructions) stdscr.attroff(curses.color_pair(1)) def _draw_list_alarms(stdscr, context): """Draw the list of alarms screen""" _init_colors() height, width = stdscr.getmaxyx() # Get required data from context alarms = context.get('alarms', []) selected_index = context.get('selected_index', 0) weekday_names = context.get('weekday_names', ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']) # Calculate visible range for scrolling max_visible_items = height - 8 # Leave space for header and footer total_items = len(alarms) + 1 # +1 for "Add new alarm" option # Calculate scroll position start_idx = max(0, min(selected_index - max_visible_items // 2, total_items - max_visible_items)) if start_idx < 0: start_idx = 0 end_idx = min(start_idx + max_visible_items, total_items) # Header header_text = "Alarms" stdscr.attron(curses.color_pair(1) | curses.A_BOLD) stdscr.addstr(2, width // 2 - len(header_text) // 2, header_text) stdscr.attroff(curses.color_pair(1) | curses.A_BOLD) # Draw alarms for i in range(start_idx, end_idx): y_pos = 4 + (i - start_idx) if i == len(alarms): # "Add new alarm" option display_str = "Add new alarm..." else: alarm = alarms[i] # Format time time_str = alarm.get('time', 'Unknown') # Format repeat info repeat_info = "" repeat_rule = alarm.get('repeat_rule', {}) if repeat_rule: if repeat_rule.get('type') == 'weekly': days = repeat_rule.get('days', []) repeat_info = f" (Every {', '.join(weekday_names[d] for d in days)})" elif repeat_rule.get('type') == 'once' and repeat_rule.get('date'): repeat_info = f" (On {repeat_rule['date']})" # Status indicator (in green) status = "✓" if alarm.get('enabled', True) else "✗" display_str = f"{status} {time_str} {alarm.get('name', 'Unnamed')}{repeat_info}" # Truncate if too long (leaving space for selection brackets) max_length = width - 6 if len(display_str) > max_length: display_str = display_str[:max_length-3] + "..." # Center the item x_pos = width // 2 - len(display_str) // 2 # Highlight selected item if i == selected_index: # Draw selection brackets in green stdscr.attron(curses.color_pair(1)) stdscr.addstr(y_pos, x_pos - 2, "[ ") stdscr.addstr(y_pos, x_pos + len(display_str), " ]") stdscr.attroff(curses.color_pair(1)) # Draw text in yellow (highlighted) stdscr.attron(curses.color_pair(2)) stdscr.addstr(y_pos, x_pos, display_str) stdscr.attroff(curses.color_pair(2)) else: # Draw normal items in green stdscr.attron(curses.color_pair(1)) stdscr.addstr(y_pos, x_pos, display_str) stdscr.attroff(curses.color_pair(1)) # Instructions instructions = "j/k: Move d: Delete a: Add/Edit Esc: Back" stdscr.attron(curses.color_pair(1)) stdscr.addstr(height - 2, width // 2 - len(instructions) // 2, instructions) stdscr.attroff(curses.color_pair(1))