import curses import os from datetime import datetime from big_digits import BIG_DIGITS class NcursesUI: def __init__(self, stdscr): # Initialize curses curses.start_color() curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK) curses.init_pair(2, curses.COLOR_YELLOW, curses.COLOR_BLACK) curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK) curses.curs_set(0) self.stdscr.keypad(1) self.stdscr.timeout(100) def show_error(self, message): height, width = self.stdscr.getmaxyx() self.stdscr.attron(curses.color_pair(3)) self.stdscr.addstr(height-1, 0, f"ERROR: {message}"[:width-1]) self.stdscr.attroff(curses.color_pair(3)) self.stdscr.refresh() time.sleep(2) def draw_big_digit(self, y, x, digit): patterns = BIG_DIGITS[digit] for i, line in enumerate(patterns): self.stdscr.addstr(y + i, x, line) def draw_big_time(self, current_time): height, width = self.stdscr.getmaxyx() time_str = current_time.strftime("%H:%M:%S") # Calculate starting position to center the big clock digit_width = 14 # Width of each digit pattern including spacing total_width = digit_width * len(time_str) start_x = (width - total_width) // 2 start_y = (height - 7) // 2 - 4 # Move up a bit to make room for date self.stdscr.attron(curses.color_pair(1)) for i, digit in enumerate(time_str): self.draw_big_digit(start_y, start_x + i * digit_width, digit) self.stdscr.attroff(curses.color_pair(1)) def draw_main_clock(self): current_time = datetime.now() time_str = current_time.strftime("%H:%M:%S") date_str = current_time.strftime("%Y-%m-%d") # Get terminal dimensions height, width = self.stdscr.getmaxyx() # Draw big time self.draw_big_time(current_time) # Draw date date_x = width // 2 - len(date_str) // 2 date_y = height // 2 + 4 # Below the big clock self.stdscr.attron(curses.color_pair(2)) self.stdscr.addstr(date_y, date_x, date_str) self.stdscr.attroff(curses.color_pair(2)) # Draw menu options menu_str = "A: Add Alarm L: List Alarms S: Stop Z: Snooze Q: Quit" menu_x = width // 2 - len(menu_str) // 2 self.stdscr.addstr(height - 2, menu_x, menu_str) # Show alarm/snooze status if self.current_alarm_process: status_str = "⏰ ALARM ACTIVE - Press 'S' to stop or 'Z' to snooze" status_x = width // 2 - len(status_str) // 2 self.stdscr.attron(curses.color_pair(3)) self.stdscr.addstr(height - 4, status_x, status_str) self.stdscr.attroff(curses.color_pair(3)) elif self.snooze_until: snooze_str = f"💤 Snoozed until {self.snooze_until.strftime('%H:%M')}" snooze_x = width // 2 - len(snooze_str) // 2 self.stdscr.attron(curses.color_pair(2)) self.stdscr.addstr(height - 4, snooze_x, snooze_str) self.stdscr.attroff(curses.color_pair(2)) def draw_add_alarm(self): height, width = self.stdscr.getmaxyx() form_y = height // 2 - 3 self.stdscr.addstr(form_y, width // 2 - 10, "New Alarm") # Time selection hour_str = f"{self.new_alarm_hour:02d}" minute_str = f"{self.new_alarm_minute:02d}" # Highlight selected field if self.new_alarm_selected == 0: self.stdscr.attron(curses.color_pair(2)) self.stdscr.addstr(form_y + 1, width // 2 - 2, hour_str) if self.new_alarm_selected == 0: self.stdscr.attroff(curses.color_pair(2)) self.stdscr.addstr(form_y + 1, width // 2, ":") if self.new_alarm_selected == 1: self.stdscr.attron(curses.color_pair(2)) self.stdscr.addstr(form_y + 1, width // 2 + 1, minute_str) if self.new_alarm_selected == 1: self.stdscr.attroff(curses.color_pair(2)) # Date selection date_str = "No specific date" if not self.new_alarm_date else self.new_alarm_date.strftime("%Y-%m-%d") if self.new_alarm_selected == 2: self.stdscr.attron(curses.color_pair(2)) self.stdscr.addstr(form_y + 3, width // 2 - len(date_str) // 2, date_str) if self.new_alarm_selected == 2: self.stdscr.attroff(curses.color_pair(2)) # Weekday selection weekday_str = "Repeat: " + " ".join( self.weekday_names[i] if i in self.new_alarm_weekdays else "___" for i in range(7) ) if self.new_alarm_selected == 3: self.stdscr.attron(curses.color_pair(2)) self.stdscr.addstr(form_y + 4, width // 2 - len(weekday_str) // 2, weekday_str) if self.new_alarm_selected == 3: self.stdscr.attroff(curses.color_pair(2)) # Instructions if self.new_alarm_selected < 2: self.stdscr.addstr(height - 2, 2, "↑↓: Change value ←→: Switch field Enter: Save Esc: Cancel") elif self.new_alarm_selected == 2: self.stdscr.addstr(height - 2, 2, "↑↓: Change date Space: Clear date Enter: Save Esc: Cancel") else: # weekday selection self.stdscr.addstr(height - 2, 2, "←→: Select day Space: Toggle Enter: Save Esc: Cancel") def draw_list_alarms(self): height, width = self.stdscr.getmaxyx() self.stdscr.addstr(2, width // 2 - 5, "Alarms") try: self.alarms = self.api_client.get_alarms() for i, alarm in enumerate(self.alarms): # Parse time from the time string time_parts = alarm['time'].split(':') time_str = f"{time_parts[0]}:{time_parts[1]}" # Add repeat rule information if 'repeat_rule' in alarm: if alarm['repeat_rule']['type'] == 'once' and 'date' in alarm['repeat_rule']: time_str += f" on {alarm['repeat_rule']['date']}" elif alarm['repeat_rule']['type'] == 'weekly' and 'days' in alarm['repeat_rule']: weekdays = [self.weekday_names[d] for d in alarm['repeat_rule']['days']] time_str += f" every {', '.join(weekdays)}" status = "✓" if alarm['enabled'] else "✗" display_str = f"{status} {time_str}" self.stdscr.addstr(4 + i, width // 2 - len(display_str) // 2, display_str) if not self.alarms: self.stdscr.addstr(4, width // 2 - 7, "No alarms set") except Exception as e: self.show_error(f"Failed to display alarms: {str(e)}") self.stdscr.addstr(height - 2, 2, "D: Delete alarm Esc: Back")