import curses import time from datetime import datetime, date, timedelta import threading import logging import queue from .utils import draw_error, init_colors from .active_alarm import draw_active_alarms from .add_alarm import draw_add_alarm from .list_alarms import draw_list_alarms from .main_clock import draw_main_clock from .input_handlers import InputHandling class UI(InputHandling): def __init__(self, alarm_system_manager, control_queue): # UI State Management self.alarm_system = alarm_system_manager self.stop_event = alarm_system_manager.stop_event self.storage = alarm_system_manager.storage # Control queue for interacting with AlarmSiren self.control_queue = control_queue # Logging self.logger = logging.getLogger(__name__) # Active alarm tracking self.active_alarms = {} # UI State self.reset_ui_state() def reset_ui_state(self): """Reset all UI state variables to their initial values""" # Menu states self.current_view = 'CLOCK' self.selected_item = 0 # Alarm Creation State self.alarm_draft = { 'name': 'New Alarm', 'hour': datetime.now().hour, 'minute': datetime.now().minute, 'enabled': True, 'date': None, 'weekdays': [], 'editing_name': False, 'temp_name': '' } # Error handling self.error_message = None self.error_timestamp = None # Alarm list self.alarm_list = [] # Weekday names (to match specification) self.weekday_names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] # Clear active alarms self.active_alarms.clear() def run(self): """Start the ncurses UI in a separate thread""" def ui_thread(): try: # Start a thread to monitor control queue monitor_thread = threading.Thread(target=self._monitor_control_queue, daemon=True) monitor_thread.start() curses.wrapper(self._main_loop) except Exception as e: self.logger.error(f"UI Thread Error: {e}") finally: self.stop_event.set() ui_thread_obj = threading.Thread(target=ui_thread, daemon=True) ui_thread_obj.start() return ui_thread_obj def _monitor_control_queue(self): """Monitor the control queue for alarm events""" while not self.stop_event.is_set(): try: # Non-blocking check of control queue try: control_msg = self.control_queue.get(timeout=1) # Handle different types of control messages if control_msg['type'] == 'trigger': # Store triggered alarm alarm_id = control_msg['alarm_id'] self.active_alarms[alarm_id] = control_msg['info'] # If not already in alarm view, switch to it if self.current_view != 'ACTIVE_ALARMS': self.current_view = 'ACTIVE_ALARMS' except queue.Empty: pass time.sleep(0.1) except Exception as e: self.logger.error(f"Error monitoring control queue: {e}") time.sleep(1) def _show_error(self, message, duration=30): """Display an error message""" self.error_message = message self.error_timestamp = time.time() def _main_loop(self, stdscr): """Main ncurses event loop""" curses.curs_set(0) stdscr.keypad(1) stdscr.timeout(100) time.sleep(0.2) stdscr.clear() while not self.stop_event.is_set(): stdscr.erase() # Draw view based on current state if self.current_view == 'CLOCK': draw_main_clock(stdscr) elif self.current_view == 'ADD_ALARM': draw_add_alarm(stdscr, { 'new_alarm_selected': self.selected_item, 'new_alarm_name': self.alarm_draft['name'], 'new_alarm_hour': self.alarm_draft['hour'], 'new_alarm_minute': self.alarm_draft['minute'], 'new_alarm_enabled': self.alarm_draft['enabled'], 'new_alarm_date': self.alarm_draft['date'], 'new_alarm_weekdays': self.alarm_draft['weekdays'], 'weekday_names': self.weekday_names, 'date_edit_pos': getattr(self, 'date_edit_pos', 2) }) elif self.current_view == 'LIST_ALARMS': draw_list_alarms(stdscr, { 'alarms': self.alarm_list or [], 'weekday_names': self.weekday_names, 'selected_index': self.selected_item }) elif self.current_view == 'ACTIVE_ALARMS': # Draw active alarm view draw_active_alarms(stdscr, {'active_alarms': self.active_alarms }) # Render error if exists if self.error_message: draw_error(stdscr, self.error_message) self._clear_error_if_expired() stdscr.refresh() # Handle input key = stdscr.getch() if key != -1: if key == ord('q'): # Context-sensitive 'q' key handling if self.current_view == 'CLOCK': break # Exit the application only from clock view else: self.current_view = 'CLOCK' # Return to clock view from other views continue # Context-specific input handling if self.current_view == 'CLOCK': self._handle_clock_input(key) elif self.current_view == 'ADD_ALARM': self._handle_add_alarm_input(key) elif self.current_view == 'LIST_ALARMS': self._handle_list_alarms_input(key) elif self.current_view == 'ACTIVE_ALARMS': self._handle_active_alarms_input(key) time.sleep(0.2) def _clear_error_if_expired(self): """Clear error message if expired""" if self.error_message and self.error_timestamp: if time.time() - self.error_timestamp > 3: self.error_message = None self.error_timestamp = None