import curses import time from datetime import datetime, date, timedelta import threading import logging import queue # Import drawing methods from the new module from ncurses_ui_draw import ( _draw_main_clock, _draw_add_alarm, _draw_list_alarms, _draw_active_alarms, _draw_error ) class UI: 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 _handle_active_alarms_input(self, key): """Handle input for active alarms view""" if not self.active_alarms: # No active alarms, return to clock view self.current_view = 'CLOCK' return # Get the first (or only) active alarm alarm_id = list(self.active_alarms.keys())[0] if key == ord('s'): # Snooze # Send snooze command to control queue self.control_queue.put({ 'type': 'snooze', 'alarm_id': alarm_id }) # Remove from active alarms del self.active_alarms[alarm_id] # Optional: show a snooze confirmation self._show_error("Alarm Snoozed") elif key == ord('d'): # Dismiss # Send dismiss command to control queue self.control_queue.put({ 'type': 'dismiss', 'alarm_id': alarm_id }) # Remove from active alarms del self.active_alarms[alarm_id] # Return to clock view self.current_view = 'CLOCK' def _handle_clock_input(self, key): """Handle input on the clock view""" if key == ord('a'): self.current_view = 'ADD_ALARM' elif key == ord('s'): self.current_view = 'LIST_ALARMS' self.alarm_list = self.storage.get_saved_alerts() def _handle_add_alarm_input(self, key): """Comprehensive input handling for alarm creation""" alarm = self.alarm_draft # Escape key handling if key == 27: # ESC if alarm['editing_name']: # Cancel name editing alarm['name'] = alarm['temp_name'] alarm['editing_name'] = False else: # Return to clock view self.current_view = 'CLOCK' return # Enter key handling if key == 10: # ENTER if alarm['editing_name']: # Finish name editing alarm['editing_name'] = False self.selected_item = 0 else: # Save alarm try: alarm_data = { "name": alarm['name'], "time": f"{alarm['hour']:02d}:{alarm['minute']:02d}:00", "enabled": alarm['enabled'], "repeat_rule": { "type": "weekly" if alarm['weekdays'] else "once", "days_of_week": [self.weekday_names[day].lower() for day in alarm['weekdays']], "at": alarm['date'].strftime("%d.%m.%Y") if alarm['date'] else None } } self.storage.save_new_alert(alarm_data) self.current_view = 'CLOCK' except Exception as e: self._show_error(str(e)) return # Navigation and editing if not alarm['editing_name']: if key in [ord('h'), curses.KEY_LEFT]: self.selected_item = (self.selected_item - 1) % 6 elif key in [ord('l'), curses.KEY_RIGHT]: self.selected_item = (self.selected_item + 1) % 6 # Up/Down for editing values if key in [ord('k'), curses.KEY_UP, ord('j'), curses.KEY_DOWN]: is_up = key in [ord('k'), curses.KEY_UP] if self.selected_item == 0: # Hour alarm['hour'] = (alarm['hour'] + (1 if is_up else -1)) % 24 elif self.selected_item == 1: # Minute alarm['minute'] = (alarm['minute'] + (1 if is_up else -1)) % 60 # Space key for toggling/editing if key == 32: # SPACE if self.selected_item == 4: # Name if not alarm['editing_name']: alarm['editing_name'] = True alarm['temp_name'] = alarm['name'] alarm['name'] = '' elif self.selected_item == 5: # Enabled alarm['enabled'] = not alarm['enabled'] # Name editing if alarm['editing_name']: if key == curses.KEY_BACKSPACE or key == 127: alarm['name'] = alarm['name'][:-1] elif 32 <= key <= 126: # Printable ASCII alarm['name'] += chr(key) def _handle_list_alarms_input(self, key): """Handle input for alarm list view""" total_items = len(self.alarm_list) + 1 # +1 for "Add new alarm" option if key == 27: # ESC self.current_view = 'CLOCK' elif key in [ord('j'), curses.KEY_DOWN]: self.selected_item = (self.selected_item + 1) % total_items elif key in [ord('k'), curses.KEY_UP]: self.selected_item = (self.selected_item - 1) % total_items elif key == ord('d'): # Only delete if a real alarm is selected (not the "Add new" option) if self.selected_item < len(self.alarm_list) and self.alarm_list: try: alarm_to_delete = self.alarm_list[self.selected_item] self.storage.remove_saved_alert(alarm_to_delete['id']) self.alarm_list = self.storage.get_saved_alerts() # Adjust selected item if needed if self.selected_item >= len(self.alarm_list): self.selected_item = len(self.alarm_list) except Exception as e: self._show_error(f"Failed to delete alarm: {e}") elif key in [ord('a'), 10]: # 'a' or Enter if self.selected_item == len(self.alarm_list): # "Add new alarm" option selected self.current_view = 'ADD_ALARM' else: # TODO: Implement alarm editing self._show_error("Alarm editing not implemented yet") 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 }) 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