import curses import time from datetime import datetime, date, timedelta import os from big_digits import BIG_DIGITS from ncurses_ui_draw import _draw_big_digit, _draw_main_clock, _draw_add_alarm, _draw_list_alarms, _draw_error from logging_config import setup_logging # Set up logging logger = setup_logging() class UI: def __init__(self, alarm_system_manager): """ Initialize the ncurses UI for the alarm system Args: alarm_system_manager (AlarmSystemManager): The main alarm system manager """ self.alarm_system = alarm_system_manager self.stop_event = alarm_system_manager.stop_event self.storage = alarm_system_manager.storage # UI state variables self.selected_menu = 0 self.new_alarm_name = "Alarm" self.editing_name = " " self.new_alarm_hour = datetime.now().hour self.new_alarm_minute = datetime.now().minute self.new_alarm_selected = 4 self.new_alarm_date = None self.new_alarm_weekdays = [] self.new_alarm_enabled = True self.weekday_names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] self.alarms = [] self.error_message = None self.error_timestamp = None def run(self): """ Start the ncurses UI """ def ui_thread(): try: curses.wrapper(self._main_loop) except Exception as e: logger.error(f"Error in UI thread: {e}") print(f"Error in UI thread: {e}") finally: self.stop_event.set() import threading ui_thread_obj = threading.Thread(target=ui_thread, daemon=True) ui_thread_obj.start() return ui_thread_obj def _handle_add_alarm_input(self, key): try: if key == 27: # Escape # If in name editing, exit name editing if self.editing_name: self.new_alarm_name = self.temp_alarm_name self.editing_name = False return # Otherwise return to main clock self.selected_menu = 0 return if key == 10: # Enter if self.editing_name: # Exit name editing mode self.editing_name = False # Move focus to time selection self.new_alarm_selected = 0 return try: alarm_data = { "name": self.new_alarm_name, "time": f"{self.new_alarm_hour:02d}:{self.new_alarm_minute:02d}:00", "enabled": self.new_alarm_enabled, "repeat_rule": { "type": "weekly" if self.new_alarm_weekdays else "once", "days": self.new_alarm_weekdays if self.new_alarm_weekdays else [], "date": self.new_alarm_date.strftime("%Y-%m-%d") if self.new_alarm_date else date.today().strftime("%Y-%m-%d") } } self.storage.save_new_alert(alarm_data) self.selected_menu = 0 except Exception as e: self._show_error(str(e)) return # Numeric input for time when on time selection if self.new_alarm_selected in [0, 1] and not self.editing_name: if 48 <= key <= 57: # 0-9 keys current_digit = int(chr(key)) if self.new_alarm_selected == 0: # Hour self.new_alarm_hour = current_digit if self.new_alarm_hour < 10 else (self.new_alarm_hour % 10 * 10 + current_digit) % 24 else: # Minute self.new_alarm_minute = current_digit if self.new_alarm_minute < 10 else (self.new_alarm_minute % 10 * 10 + current_digit) % 60 return # Use hjkl for navigation and selection if key in [ord('h'), curses.KEY_LEFT]: self.new_alarm_selected = (self.new_alarm_selected - 1) % 6 elif key in [ord('l'), curses.KEY_RIGHT]: self.new_alarm_selected = (self.new_alarm_selected + 1) % 6 elif key in [ord('k'), curses.KEY_UP]: if self.new_alarm_selected == 0: self.new_alarm_hour = (self.new_alarm_hour + 1) % 24 elif self.new_alarm_selected == 1: self.new_alarm_minute = (self.new_alarm_minute + 1) % 60 elif self.new_alarm_selected == 2: if not self.new_alarm_date: self.new_alarm_date = date.today() else: self.new_alarm_date += timedelta(days=1) elif self.new_alarm_selected == 3 and len(self.new_alarm_weekdays) < 7: # Add whole groups of days if key == ord('k'): # Options: M-F (0-4), Weekends (5-6), All days if not self.new_alarm_weekdays: self.new_alarm_weekdays = list(range(5)) # M-F elif self.new_alarm_weekdays == list(range(5)): self.new_alarm_weekdays = [5, 6] # Weekends elif self.new_alarm_weekdays == [5, 6]: self.new_alarm_weekdays = list(range(7)) # All days else: self.new_alarm_weekdays = [] # Reset self.new_alarm_weekdays.sort() elif key in [ord('j'), curses.KEY_DOWN]: if self.new_alarm_selected == 0: self.new_alarm_hour = (self.new_alarm_hour - 1) % 24 elif self.new_alarm_selected == 1: self.new_alarm_minute = (self.new_alarm_minute - 1) % 60 elif self.new_alarm_selected == 2 and self.new_alarm_date: self.new_alarm_date -= timedelta(days=1) elif self.new_alarm_selected == 3: # Cycle through weekday groups if key == ord('j'): if not self.new_alarm_weekdays: self.new_alarm_weekdays = [5, 6] # Weekends elif self.new_alarm_weekdays == [5, 6]: self.new_alarm_weekdays = list(range(7)) # All days elif self.new_alarm_weekdays == list(range(7)): self.new_alarm_weekdays = list(range(5)) # M-F else: self.new_alarm_weekdays = [] # Reset self.new_alarm_weekdays.sort() elif key == 32: # Space if self.new_alarm_selected == 4: # Name editing if not self.editing_name: self.editing_name = True self.temp_alarm_name = self.new_alarm_name self.new_alarm_name = "" elif self.new_alarm_selected == 2: # Date self.new_alarm_date = None elif self.new_alarm_selected == 3: # Weekdays # Toggle specific day when on weekday selection current_day = len(self.new_alarm_weekdays) if current_day < 7: if current_day in self.new_alarm_weekdays: self.new_alarm_weekdays.remove(current_day) else: self.new_alarm_weekdays.append(current_day) self.new_alarm_weekdays.sort() elif self.new_alarm_selected == 5: # Enabled toggle self.new_alarm_enabled = not self.new_alarm_enabled # Name editing handling if self.editing_name: if key == curses.KEY_BACKSPACE or key == 127: self.new_alarm_name = self.new_alarm_name[:-1] elif 32 <= key <= 126: # Printable ASCII self.new_alarm_name += chr(key) except Exception as e: logger.error(f"Error: {e}") self._show_error(str(e)) def _handle_list_alarms_input(self, key): """ Handle input for the list alarms screen """ if key == 27: # Escape self.selected_menu = 0 elif key == ord('d'): # Delete last alarm if exists if self.alarms: last_alarm = self.alarms[-1] try: self.storage.remove_saved_alert(last_alarm['id']) except Exception as e: _show_error(f"Failed to delete alarm: {e}") logger.error(f"Failed to delete alarm: {e}") def _show_error(self, message, duration=3): """Display an error message for a specified duration""" self.error_message = message self.error_timestamp = time.time() def _clear_error_if_expired(self): """Clear error message if it has been displayed long enough""" if self.error_message and self.error_timestamp: if time.time() - self.error_timestamp > 3: # 3 seconds self.error_message = None self.error_timestamp = None def _main_loop(self, stdscr): """ Main ncurses event loop """ # Initialize color pairs 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) # Configure screen stdscr.keypad(1) stdscr.timeout(100) time.sleep(0.2) stdscr.clear() while not self.stop_event.is_set(): # Clear the screen #stdscr.clear() stdscr.erase() # Draw appropriate screen based on selected menu if self.selected_menu == 0: _draw_main_clock(stdscr) elif self.selected_menu == 1: _draw_add_alarm(stdscr, { 'new_alarm_selected': self.new_alarm_selected, 'new_alarm_name': self.new_alarm_name, 'editing_name': getattr(self, 'editing_name', False), 'new_alarm_hour': self.new_alarm_hour, 'new_alarm_minute': self.new_alarm_minute, 'new_alarm_enabled': self.new_alarm_enabled, 'new_alarm_date': self.new_alarm_date, 'new_alarm_weekdays': self.new_alarm_weekdays, 'weekday_names': self.weekday_names, 'new_alarm_snooze_enabled': getattr(self, 'new_alarm_snooze_enabled', False), 'new_alarm_snooze_duration': getattr(self, 'new_alarm_snooze_duration', 5), 'new_alarm_snooze_max_count': getattr(self, 'new_alarm_snooze_max_count', 3) }) elif self.selected_menu == 2: _draw_list_alarms(stdscr, { 'alarms': self.alarms, 'weekday_names': self.weekday_names }) # Draw error if exists if self.error_message: _draw_error(stdscr, self.error_message) self._clear_error_if_expired() # Refresh the screen stdscr.refresh() # Small sleep to reduce CPU usage time.sleep(0.2) # Handle input key = stdscr.getch() if key != -1: # Menu navigation and input handling if key == ord('q') or key == 27: # 'q' or Escape if self.selected_menu != 0: self.selected_menu = 0 else: break elif key == ord('c'): # Clock/Home screen self.selected_menu = 0 elif key == ord('a'): # Add Alarm self.selected_menu = 1 elif key == ord('l'): # List Alarms self.selected_menu = 2 self.alarms = self.storage.get_saved_alerts() # Context-specific input handling if self.selected_menu == 1: self._handle_add_alarm_input(key) elif self.selected_menu == 2: self._handle_list_alarms_input(key)