diff --git a/alert_api/alarm_siren.py b/alert_api/alarm_siren.py index 52521de..6adc796 100644 --- a/alert_api/alarm_siren.py +++ b/alert_api/alarm_siren.py @@ -181,38 +181,3 @@ class AlarmSiren: if alarm_id in self.active_alarms: logger.info(f"Dismissed alarm {alarm_id}") del self.active_alarms[alarm_id] - -def main(): - """Example usage of AlarmSiren""" - siren = AlarmSiren() - - # Example alarm configuration - sample_alarm = { - 'id': 1, - 'name': 'Morning Alarm', - 'time': '07:00:00', - 'file_to_play': '/path/to/alarm.mp3', - 'repeat_rule': { - 'type': 'daily' - }, - 'snooze': { - 'enabled': True, - 'duration': 10, - 'max_count': 3 - }, - 'metadata': { - 'volume': 80 - } - } - - siren.schedule_alarm(sample_alarm) - - # Keep main thread alive (in a real app, this would be handled differently) - try: - while True: - time.sleep(1) - except KeyboardInterrupt: - print("Alarm siren stopped") - -if __name__ == "__main__": - main() diff --git a/alert_api/logging_config.py b/alert_api/logging_config.py index c07b1c7..044ba8a 100644 --- a/alert_api/logging_config.py +++ b/alert_api/logging_config.py @@ -9,7 +9,6 @@ def setup_logging(log_dir="logs", log_file="alarm_system.log", level=logging.DEB level=level, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ - logging.StreamHandler(), logging.FileHandler(log_path, mode='a', encoding='utf-8') ] ) diff --git a/alert_api/ncurses_ui.py b/alert_api/ncurses_ui.py index b892a6e..291e210 100644 --- a/alert_api/ncurses_ui.py +++ b/alert_api/ncurses_ui.py @@ -4,6 +4,10 @@ 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): @@ -20,9 +24,10 @@ class UI: # 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 = 0 + self.new_alarm_selected = 4 self.new_alarm_date = None self.new_alarm_weekdays = [] self.new_alarm_enabled = True @@ -39,6 +44,7 @@ class UI: 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() @@ -49,90 +55,130 @@ class UI: return ui_thread_obj def _handle_add_alarm_input(self, key): - """ - Handle input for adding a new alarm - """ - if key == 27: # Escape - self.selected_menu = 0 - return - - if key == 10: # Enter - try: - # Prepare alarm data - 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 - } - - # Add repeat rule if applicable - if self.new_alarm_weekdays: - alarm_data["repeat_rule"] = { - "type": "weekly", - "days": self.new_alarm_weekdays - } - elif self.new_alarm_date: - alarm_data["repeat_rule"] = { - "type": "once", - "date": self.new_alarm_date.strftime("%Y-%m-%d") - } - - # Save new alarm - self.storage.save_new_alert(alarm_data) - - # Reset form + 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 - self.new_alarm_hour = datetime.now().hour - self.new_alarm_minute = datetime.now().minute - self.new_alarm_date = None - self.new_alarm_weekdays = [] - self.new_alarm_name = "Alarm" - self.new_alarm_enabled = True - except Exception as e: - # TODO: Show error on screen - print(f"Failed to save alarm: {e}") - return - - if key == curses.KEY_LEFT: - self.new_alarm_selected = (self.new_alarm_selected - 1) % 6 - elif key == curses.KEY_RIGHT: - self.new_alarm_selected = (self.new_alarm_selected + 1) % 6 - elif key == 32: # Space - if self.new_alarm_selected == 2: # Date - self.new_alarm_date = None - elif self.new_alarm_selected == 3: # Weekdays - 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) + 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_weekdays.append(current_day) + 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 self.new_alarm_selected == 5: # Enabled toggle - self.new_alarm_enabled = not self.new_alarm_enabled - - elif key == 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 == 4: # Name - self.new_alarm_name += " " - - elif key == 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 == 4 and len(self.new_alarm_name) > 1: - self.new_alarm_name = self.new_alarm_name.rstrip()[:-1] + 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): """ @@ -147,7 +193,8 @@ class UI: try: self.storage.remove_saved_alert(last_alarm['id']) except Exception as e: - print(f"Failed to delete alarm: {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""" @@ -176,9 +223,13 @@ class UI: 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.clear() + stdscr.erase() # Draw appropriate screen based on selected menu if self.selected_menu == 0: @@ -187,6 +238,7 @@ class UI: _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, @@ -203,18 +255,26 @@ class UI: '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.1) + 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 - break + 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 diff --git a/alert_api/ncurses_ui_draw.py b/alert_api/ncurses_ui_draw.py index 8f07d66..f53b6e5 100644 --- a/alert_api/ncurses_ui_draw.py +++ b/alert_api/ncurses_ui_draw.py @@ -6,13 +6,16 @@ def _draw_error(stdscr, error_message): """Draw error message if present""" if error_message: height, width = stdscr.getmaxyx() - error_x = width // 2 - len(error_message) // 2 + # 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 - + # Red color for errors - stdscr.attron(curses.color_pair(3)) + stdscr.attron(curses.color_pair(3) | curses.A_BOLD) stdscr.addstr(error_y, error_x, error_message) - stdscr.attroff(curses.color_pair(3)) + stdscr.attroff(curses.color_pair(3) | curses.A_BOLD) def _draw_big_digit(stdscr, y, x, digit, big_digits): """ @@ -83,7 +86,7 @@ def _draw_add_alarm(stdscr, context): height, width = stdscr.getmaxyx() form_y = height // 2 - 3 - stdscr.addstr(form_y, width // 2 - 10, "Add New Alarm") + stdscr.addstr(form_y -1, width // 2 - 10, "Add New Alarm") # Name input if context['new_alarm_selected'] == 4: