From 7eafbf5ebd8142dd27eb952f5063e5cbcfad7cdb Mon Sep 17 00:00:00 2001 From: Kalzu Rekku Date: Sun, 9 Feb 2025 19:17:31 +0200 Subject: [PATCH] trying to make the ui sub systems add_alarm part to work better. --- clock/data_classes.py | 11 +- clock/ui/add_alarm.py | 217 +++++++++---------------------------- clock/ui/input_handlers.py | 114 +++++++++---------- 3 files changed, 120 insertions(+), 222 deletions(-) diff --git a/clock/data_classes.py b/clock/data_classes.py index cb4ebea..4d1f41b 100644 --- a/clock/data_classes.py +++ b/clock/data_classes.py @@ -18,13 +18,22 @@ class RepeatRule: def validate(self) -> bool: """Validate repeat rule configuration""" valid_types = {"daily", "weekly", "once"} - valid_days = {"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"} + valid_days = {"mon", "tue", "wed", "thu", "fri", "sat", "sun"} + if self.type not in valid_types: logger.error(f"Invalid RepeatRule type: '{self.type}'. Must be one of {valid_types}") return False if self.type == "weekly": + if self.days_of_week is None: + logger.error("days_of_week is None") + return False + + if not self.days_of_week: + logger.error("days_of_week is empty") + return False + invalid_days = [day for day in self.days_of_week if day.lower() not in valid_days] if invalid_days: logger.error(f"Invalid RepeatRule weekday names: {invalid_days}. Must be one of {valid_days}") diff --git a/clock/ui/add_alarm.py b/clock/ui/add_alarm.py index fc691a1..4eed874 100644 --- a/clock/ui/add_alarm.py +++ b/clock/ui/add_alarm.py @@ -1,25 +1,22 @@ import curses from datetime import datetime -from .utils import init_colors, draw_big_digit +from .utils import init_colors -def draw_add_alarm(stdscr, context): +def draw_add_alarm(stdscr, alarm_draft): """Draw the add alarm screen""" - # Ensure context is a dictionary with default values - if context is None: - context = {} - - # Provide default values with more explicit checks - context = { - 'new_alarm_selected': context.get('new_alarm_selected', 0), - 'new_alarm_name': context.get('new_alarm_name', 'New Alarm'), - 'new_alarm_hour': context.get('new_alarm_hour', datetime.now().hour), - 'new_alarm_minute': context.get('new_alarm_minute', datetime.now().minute), - 'new_alarm_enabled': context.get('new_alarm_enabled', True), - 'new_alarm_date': context.get('new_alarm_date') or None, - 'new_alarm_weekdays': context.get('new_alarm_weekdays', []) or [], - 'weekday_names': context.get('weekday_names', ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']), - 'date_edit_pos': context.get('date_edit_pos', 2) # 0 = year, 1 = month, 2 = day - } + if alarm_draft is None: + alarm_draft = { + 'hour': datetime.now().hour, + 'minute': datetime.now().minute, + 'name': 'New Alarm', + 'enabled': True, + 'date': None, + 'weekdays': [], + 'current_weekday': 0, + 'editing_name': False, + 'temp_name': '', + 'selected_item': 0 # Added to handle selection + } init_colors() height, width = stdscr.getmaxyx() @@ -27,99 +24,63 @@ def draw_add_alarm(stdscr, context): # Center the form vertically with good spacing form_y = height // 2 - 8 - # Title with green color and bold + # Title title = "Add New Alarm" stdscr.attron(curses.color_pair(1) | curses.A_BOLD) stdscr.addstr(form_y, width // 2 - len(title) // 2, title) stdscr.attroff(curses.color_pair(1) | curses.A_BOLD) - # Helper function to draw a labeled field - def draw_field(y, label, value, is_selected, center_offset=0): + def draw_field(y, label, value, is_selected): label_str = f"{label}: " - total_width = len(label_str) + len(str(value)) - x = width // 2 - total_width // 2 + center_offset - - # Draw label in green + x = width // 2 - (len(label_str) + len(str(value))) // 2 stdscr.attron(curses.color_pair(1)) stdscr.addstr(y, x, label_str) stdscr.attroff(curses.color_pair(1)) # Draw value (highlighted if selected) if is_selected: - stdscr.attron(curses.color_pair(2)) # Yellow for selected - stdscr.addstr(y, x + len(label_str), str(value)) + stdscr.attron(curses.color_pair(2)) # Highlighted selection + stdscr.addstr(y, x + len(label_str), str(value)) + if is_selected: stdscr.attroff(curses.color_pair(2)) - else: - stdscr.attron(curses.color_pair(1)) # Green for normal - stdscr.addstr(y, x + len(label_str), str(value)) - stdscr.attroff(curses.color_pair(1)) - # Name field with proper spacing - name_str = str(context['new_alarm_name']) - draw_field(form_y + 2, "Name", name_str, context['new_alarm_selected'] == 4) + # Get selected_item either from alarm_draft or passed separately + selected_item = alarm_draft.get('selected_item', 0) - # Time selection with centered layout - time_y = form_y + 4 - time_label = "Time: " - hour_str = f"{int(context['new_alarm_hour']):02d}" - minute_str = f"{int(context['new_alarm_minute']):02d}" + # Order: Time → Date → Weekdays → Name → Enabled + draw_field(form_y + 2, "Time", f"{alarm_draft['hour']:02d}:{alarm_draft['minute']:02d}", + selected_item == 0 or selected_item == 1) - # Calculate center position for time - time_total_width = len(time_label) + 5 # 5 = HH:MM - time_x = width // 2 - time_total_width // 2 - - # Draw time label - stdscr.attron(curses.color_pair(1)) - stdscr.addstr(time_y, time_x, time_label) - stdscr.attroff(curses.color_pair(1)) - - # Draw hour - if context['new_alarm_selected'] == 0: - stdscr.attron(curses.color_pair(2)) + # Date Selection + date_y = form_y + 4 + if alarm_draft['weekdays']: + draw_field(date_y, "Date", "Repeating weekly", selected_item == 2) else: - stdscr.attron(curses.color_pair(1)) - stdscr.addstr(time_y, time_x + len(time_label), hour_str) - if context['new_alarm_selected'] == 0: - stdscr.attroff(curses.color_pair(2)) - else: - stdscr.attroff(curses.color_pair(1)) + draw_field(date_y, "Date", alarm_draft['date'].strftime("%Y-%m-%d") if alarm_draft['date'] else "None", + selected_item == 2) - # Draw colon - stdscr.attron(curses.color_pair(1)) - stdscr.addstr(time_y, time_x + len(time_label) + 2, ":") - stdscr.attroff(curses.color_pair(1)) - - # Draw minute - if context['new_alarm_selected'] == 1: - stdscr.attron(curses.color_pair(2)) - else: - stdscr.attron(curses.color_pair(1)) - stdscr.addstr(time_y, time_x + len(time_label) + 3, minute_str) - if context['new_alarm_selected'] == 1: - stdscr.attroff(curses.color_pair(2)) - else: - stdscr.attroff(curses.color_pair(1)) - - # Draw weekdays + # Weekday Selection weekday_y = form_y + 6 - weekdays_label = "Repeat: " - weekday_x = width // 2 - (len(weekdays_label) + len(context['weekday_names']) * 4) // 2 + weekday_label = "Repeat: " + label_x = width // 2 - 20 # Adjust this value to move the entire "Repeat" section left or right - # Draw label + # Draw the "Repeat: " label once stdscr.attron(curses.color_pair(1)) - stdscr.addstr(weekday_y, weekday_x, weekdays_label) + stdscr.addstr(weekday_y, label_x, weekday_label) stdscr.attroff(curses.color_pair(1)) - # Draw each weekday - for i, day in enumerate(context['weekday_names']): - x_pos = weekday_x + len(weekdays_label) + i * 4 - is_selected = context['new_alarm_selected'] == 3 and i == context.get('weekday_edit_pos', 0) - is_active = i in context['new_alarm_weekdays'] + # Draw weekdays starting right after the label + weekday_x = label_x + len(weekday_label) + weekday_names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + for i, day in enumerate(weekday_names): + x_pos = weekday_x + i * 4 # Space between weekdays + is_selected = (selected_item == 3 and i == alarm_draft.get('current_weekday', 0)) + is_active = i in alarm_draft.get('weekdays', []) if is_selected: - stdscr.attron(curses.color_pair(2)) + stdscr.attron(curses.color_pair(2)) # Highlight current selection elif is_active: - stdscr.attron(curses.color_pair(1) | curses.A_BOLD) + stdscr.attron(curses.color_pair(1) | curses.A_BOLD) # Selected weekday else: stdscr.attron(curses.color_pair(1)) @@ -132,88 +93,16 @@ def draw_add_alarm(stdscr, context): else: stdscr.attroff(curses.color_pair(1)) - # Date selection - date_y = form_y + 8 + # Name field + draw_field(form_y + 8, "Name", alarm_draft['name'], selected_item == 4) - if context['new_alarm_weekdays']: - draw_field(date_y, "Date", "Repeating weekly", context['new_alarm_selected'] == 2) - else: - date_label = "Date: " - if context.get('new_alarm_date'): - date = context['new_alarm_date'] - date_edit_pos = context.get('date_edit_pos', 0) - - # Calculate center position - total_width = len(date_label) + len("YYYY-MM-DD") - date_x = width // 2 - total_width // 2 - - # Draw label - stdscr.attron(curses.color_pair(1)) - stdscr.addstr(date_y, date_x, date_label) - stdscr.attroff(curses.color_pair(1)) - - # Draw date components - year_str = f"{date.year}" - month_str = f"{date.month:02d}" - day_str = f"{date.day:02d}" - current_x = date_x + len(date_label) - - # Draw year - if context['new_alarm_selected'] == 2 and date_edit_pos == 0: - stdscr.attron(curses.color_pair(2)) - else: - stdscr.attron(curses.color_pair(1)) - stdscr.addstr(date_y, current_x, year_str) - if context['new_alarm_selected'] == 2 and date_edit_pos == 0: - stdscr.attroff(curses.color_pair(2)) - else: - stdscr.attroff(curses.color_pair(1)) - current_x += len(year_str) - - # Draw first dash - stdscr.attron(curses.color_pair(1)) - stdscr.addstr(date_y, current_x, "-") - stdscr.attroff(curses.color_pair(1)) - current_x += 1 - - # Draw month - if context['new_alarm_selected'] == 2 and date_edit_pos == 1: - stdscr.attron(curses.color_pair(2)) - else: - stdscr.attron(curses.color_pair(1)) - stdscr.addstr(date_y, current_x, month_str) - if context['new_alarm_selected'] == 2 and date_edit_pos == 1: - stdscr.attroff(curses.color_pair(2)) - else: - stdscr.attroff(curses.color_pair(1)) - current_x += len(month_str) - - # Draw second dash - stdscr.attron(curses.color_pair(1)) - stdscr.addstr(date_y, current_x, "-") - stdscr.attroff(curses.color_pair(1)) - current_x += 1 - - # Draw day - if context['new_alarm_selected'] == 2 and date_edit_pos == 2: - stdscr.attron(curses.color_pair(2)) - else: - stdscr.attron(curses.color_pair(1)) - stdscr.addstr(date_y, current_x, day_str) - if context['new_alarm_selected'] == 2 and date_edit_pos == 2: - stdscr.attroff(curses.color_pair(2)) - else: - stdscr.attroff(curses.color_pair(1)) - else: - draw_field(date_y, "Date", "No specific date", context['new_alarm_selected'] == 2) - - # Enabled/Disabled toggle with visual indicator + # Enabled Toggle status_y = form_y + 10 - enabled_str = "● Enabled" if context['new_alarm_enabled'] else "○ Disabled" - draw_field(status_y, "Status", enabled_str, context['new_alarm_selected'] == 5, -2) + enabled_str = "● Enabled" if alarm_draft['enabled'] else "○ Disabled" + draw_field(status_y, "Status", enabled_str, selected_item == 5) - # Instructions in green at the bottom - instructions = "j/k: Change h/l: Switch Space: Toggle Enter: Save Esc: Cancel" + # Instructions + instructions = "j/k: Change h/l: Move Space: Toggle Enter: Save Esc: Cancel" stdscr.attron(curses.color_pair(1)) stdscr.addstr(height - 2, width // 2 - len(instructions) // 2, instructions) stdscr.attroff(curses.color_pair(1)) diff --git a/clock/ui/input_handlers.py b/clock/ui/input_handlers.py index 67e5a01..5bc417a 100644 --- a/clock/ui/input_handlers.py +++ b/clock/ui/input_handlers.py @@ -1,4 +1,5 @@ import curses +from datetime import datetime, date, timedelta class InputHandling: @@ -46,6 +47,19 @@ class InputHandling: def _handle_add_alarm_input(self, key): """Handle input for alarm creation""" + if not hasattr(self, 'alarm_draft'): + self.alarm_draft = { + 'hour': datetime.now().hour, + 'minute': datetime.now().minute, + 'name': 'New Alarm', + 'enabled': True, + 'date': None, + 'weekdays': [], + 'current_weekday': 0, # Add this to track currently selected weekday + 'editing_name': False, + 'temp_name': '' + } + alarm = self.alarm_draft # Escape key handling @@ -84,27 +98,29 @@ class InputHandling: self._show_error(str(e)) return - # Date editing mode - if self.selected_item == 2: # Date field selected - if not hasattr(self, 'date_edit_pos'): - self.date_edit_pos = 2 # Default to day (2) + # Move selection using Left (h) and Right (l) + 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 - if key == 32: # SPACE - if alarm['date'] is None: + # Handle numeric values (Hour, Minute, Date) + if key in [ord('k'), curses.KEY_UP, ord('j'), curses.KEY_DOWN]: + is_up = key in [ord('k'), curses.KEY_UP] + delta = 1 if is_up else -1 + + if self.selected_item == 0: # Hour + alarm['hour'] = (alarm['hour'] + delta) % 24 + elif self.selected_item == 1: # Minute + alarm['minute'] = (alarm['minute'] + delta) % 60 + elif self.selected_item == 2: # Date + if not alarm['date']: alarm['date'] = datetime.now().date() else: - alarm['date'] = None - - elif alarm['date'] is not None: - if key in [ord('h'), curses.KEY_LEFT]: - self.date_edit_pos = (self.date_edit_pos - 1) % 3 - elif key in [ord('l'), curses.KEY_RIGHT]: - self.date_edit_pos = (self.date_edit_pos + 1) % 3 - elif key in [ord('j'), curses.KEY_DOWN, ord('k'), curses.KEY_UP]: - is_up = key in [ord('k'), curses.KEY_UP] - delta = 1 if is_up else -1 - try: + if not hasattr(self, 'date_edit_pos'): + self.date_edit_pos = 2 # Default to editing the day current_date = alarm['date'] if self.date_edit_pos == 0: # Year alarm['date'] = current_date.replace(year=max(current_date.year + delta, datetime.now().year)) @@ -117,43 +133,17 @@ class InputHandling: alarm['date'] = current_date.replace(day=max(1, min(max_day.day, current_date.day + delta))) except ValueError as e: self._show_error(str(e)) - return - # Navigation and editing - if not alarm['editing_name']: + # Weekday Selection + elif self.selected_item == 3: # When weekdays are selected if key in [ord('h'), curses.KEY_LEFT]: - self.selected_item = (self.selected_item - 1) % 6 - self.date_edit_pos = 2 # Reset to day when moving away + # Move selection left + alarm['current_weekday'] = (alarm['current_weekday'] - 1) % 7 elif key in [ord('l'), curses.KEY_RIGHT]: - self.selected_item = (self.selected_item + 1) % 6 - self.date_edit_pos = 2 # Reset to day when moving away - - # 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 - elif self.selected_item == 3: # Weekdays - # Move selection through weekdays - if 'current_weekday' not in alarm: - alarm['current_weekday'] = 0 - alarm['current_weekday'] = (alarm['current_weekday'] + (1 if is_up else -1)) % 7 - - # 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'] - elif self.selected_item == 3: # Weekdays - # Toggle current weekday - current_day = alarm.get('current_weekday', 0) + # Move selection right + alarm['current_weekday'] = (alarm['current_weekday'] + 1) % 7 + elif key == 32: # SPACE to toggle selection + current_day = alarm['current_weekday'] if current_day in alarm['weekdays']: alarm['weekdays'].remove(current_day) else: @@ -164,12 +154,22 @@ class InputHandling: if alarm['weekdays']: alarm['date'] = None - # 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) + # Name Editing + elif self.selected_item == 4: + if key == 32: # SPACE to start editing + if not alarm['editing_name']: + alarm['editing_name'] = True + alarm['temp_name'] = alarm['name'] + alarm['name'] = '' + elif 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) + + # Enabled/Disabled Toggle + elif self.selected_item == 5 and key == 32: + alarm['enabled'] = not alarm['enabled'] def _handle_list_alarms_input(self, key): """Handle input for alarm list view"""