trying to make the ui sub systems add_alarm part to work better.

This commit is contained in:
Kalzu Rekku 2025-02-09 19:17:31 +02:00
parent 1072973f3f
commit 7eafbf5ebd
3 changed files with 120 additions and 222 deletions

View File

@ -18,13 +18,22 @@ class RepeatRule:
def validate(self) -> bool: def validate(self) -> bool:
"""Validate repeat rule configuration""" """Validate repeat rule configuration"""
valid_types = {"daily", "weekly", "once"} 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: if self.type not in valid_types:
logger.error(f"Invalid RepeatRule type: '{self.type}'. Must be one of {valid_types}") logger.error(f"Invalid RepeatRule type: '{self.type}'. Must be one of {valid_types}")
return False return False
if self.type == "weekly": 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] invalid_days = [day for day in self.days_of_week if day.lower() not in valid_days]
if invalid_days: if invalid_days:
logger.error(f"Invalid RepeatRule weekday names: {invalid_days}. Must be one of {valid_days}") logger.error(f"Invalid RepeatRule weekday names: {invalid_days}. Must be one of {valid_days}")

View File

@ -1,24 +1,21 @@
import curses import curses
from datetime import datetime 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""" """Draw the add alarm screen"""
# Ensure context is a dictionary with default values if alarm_draft is None:
if context is None: alarm_draft = {
context = {} 'hour': datetime.now().hour,
'minute': datetime.now().minute,
# Provide default values with more explicit checks 'name': 'New Alarm',
context = { 'enabled': True,
'new_alarm_selected': context.get('new_alarm_selected', 0), 'date': None,
'new_alarm_name': context.get('new_alarm_name', 'New Alarm'), 'weekdays': [],
'new_alarm_hour': context.get('new_alarm_hour', datetime.now().hour), 'current_weekday': 0,
'new_alarm_minute': context.get('new_alarm_minute', datetime.now().minute), 'editing_name': False,
'new_alarm_enabled': context.get('new_alarm_enabled', True), 'temp_name': '',
'new_alarm_date': context.get('new_alarm_date') or None, 'selected_item': 0 # Added to handle selection
'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
} }
init_colors() init_colors()
@ -27,99 +24,63 @@ def draw_add_alarm(stdscr, context):
# Center the form vertically with good spacing # Center the form vertically with good spacing
form_y = height // 2 - 8 form_y = height // 2 - 8
# Title with green color and bold # Title
title = "Add New Alarm" title = "Add New Alarm"
stdscr.attron(curses.color_pair(1) | curses.A_BOLD) stdscr.attron(curses.color_pair(1) | curses.A_BOLD)
stdscr.addstr(form_y, width // 2 - len(title) // 2, title) stdscr.addstr(form_y, width // 2 - len(title) // 2, title)
stdscr.attroff(curses.color_pair(1) | curses.A_BOLD) stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)
# Helper function to draw a labeled field def draw_field(y, label, value, is_selected):
def draw_field(y, label, value, is_selected, center_offset=0):
label_str = f"{label}: " label_str = f"{label}: "
total_width = len(label_str) + len(str(value)) x = width // 2 - (len(label_str) + len(str(value))) // 2
x = width // 2 - total_width // 2 + center_offset
# Draw label in green
stdscr.attron(curses.color_pair(1)) stdscr.attron(curses.color_pair(1))
stdscr.addstr(y, x, label_str) stdscr.addstr(y, x, label_str)
stdscr.attroff(curses.color_pair(1)) stdscr.attroff(curses.color_pair(1))
# Draw value (highlighted if selected) # Draw value (highlighted if selected)
if is_selected: if is_selected:
stdscr.attron(curses.color_pair(2)) # Yellow for selected stdscr.attron(curses.color_pair(2)) # Highlighted selection
stdscr.addstr(y, x + len(label_str), str(value)) stdscr.addstr(y, x + len(label_str), str(value))
if is_selected:
stdscr.attroff(curses.color_pair(2)) stdscr.attroff(curses.color_pair(2))
# Get selected_item either from alarm_draft or passed separately
selected_item = alarm_draft.get('selected_item', 0)
# 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)
# Date Selection
date_y = form_y + 4
if alarm_draft['weekdays']:
draw_field(date_y, "Date", "Repeating weekly", selected_item == 2)
else: else:
stdscr.attron(curses.color_pair(1)) # Green for normal draw_field(date_y, "Date", alarm_draft['date'].strftime("%Y-%m-%d") if alarm_draft['date'] else "None",
stdscr.addstr(y, x + len(label_str), str(value)) selected_item == 2)
stdscr.attroff(curses.color_pair(1))
# Name field with proper spacing # Weekday Selection
name_str = str(context['new_alarm_name'])
draw_field(form_y + 2, "Name", name_str, context['new_alarm_selected'] == 4)
# 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}"
# 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))
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 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_y = form_y + 6 weekday_y = form_y + 6
weekdays_label = "Repeat: " weekday_label = "Repeat: "
weekday_x = width // 2 - (len(weekdays_label) + len(context['weekday_names']) * 4) // 2 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.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)) stdscr.attroff(curses.color_pair(1))
# Draw each weekday # Draw weekdays starting right after the label
for i, day in enumerate(context['weekday_names']): weekday_x = label_x + len(weekday_label)
x_pos = weekday_x + len(weekdays_label) + i * 4 weekday_names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
is_selected = context['new_alarm_selected'] == 3 and i == context.get('weekday_edit_pos', 0) for i, day in enumerate(weekday_names):
is_active = i in context['new_alarm_weekdays'] 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: if is_selected:
stdscr.attron(curses.color_pair(2)) stdscr.attron(curses.color_pair(2)) # Highlight current selection
elif is_active: elif is_active:
stdscr.attron(curses.color_pair(1) | curses.A_BOLD) stdscr.attron(curses.color_pair(1) | curses.A_BOLD) # Selected weekday
else: else:
stdscr.attron(curses.color_pair(1)) stdscr.attron(curses.color_pair(1))
@ -132,88 +93,16 @@ def draw_add_alarm(stdscr, context):
else: else:
stdscr.attroff(curses.color_pair(1)) stdscr.attroff(curses.color_pair(1))
# Date selection # Name field
date_y = form_y + 8 draw_field(form_y + 8, "Name", alarm_draft['name'], selected_item == 4)
if context['new_alarm_weekdays']: # Enabled Toggle
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
status_y = form_y + 10 status_y = form_y + 10
enabled_str = "● Enabled" if context['new_alarm_enabled'] else "○ Disabled" enabled_str = "● Enabled" if alarm_draft['enabled'] else "○ Disabled"
draw_field(status_y, "Status", enabled_str, context['new_alarm_selected'] == 5, -2) draw_field(status_y, "Status", enabled_str, selected_item == 5)
# Instructions in green at the bottom # Instructions
instructions = "j/k: Change h/l: Switch Space: Toggle Enter: Save Esc: Cancel" instructions = "j/k: Change h/l: Move Space: Toggle Enter: Save Esc: Cancel"
stdscr.attron(curses.color_pair(1)) stdscr.attron(curses.color_pair(1))
stdscr.addstr(height - 2, width // 2 - len(instructions) // 2, instructions) stdscr.addstr(height - 2, width // 2 - len(instructions) // 2, instructions)
stdscr.attroff(curses.color_pair(1)) stdscr.attroff(curses.color_pair(1))

View File

@ -1,4 +1,5 @@
import curses import curses
from datetime import datetime, date, timedelta
class InputHandling: class InputHandling:
@ -46,6 +47,19 @@ class InputHandling:
def _handle_add_alarm_input(self, key): def _handle_add_alarm_input(self, key):
"""Handle input for alarm creation""" """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 alarm = self.alarm_draft
# Escape key handling # Escape key handling
@ -84,27 +98,29 @@ class InputHandling:
self._show_error(str(e)) self._show_error(str(e))
return return
# Date editing mode # Move selection using Left (h) and Right (l)
if self.selected_item == 2: # Date field selected if not alarm['editing_name']:
if not hasattr(self, 'date_edit_pos'):
self.date_edit_pos = 2 # Default to day (2)
if key == 32: # SPACE
if alarm['date'] is None:
alarm['date'] = datetime.now().date()
else:
alarm['date'] = None
elif alarm['date'] is not None:
if key in [ord('h'), curses.KEY_LEFT]: if key in [ord('h'), curses.KEY_LEFT]:
self.date_edit_pos = (self.date_edit_pos - 1) % 3 self.selected_item = (self.selected_item - 1) % 6
elif key in [ord('l'), curses.KEY_RIGHT]: elif key in [ord('l'), curses.KEY_RIGHT]:
self.date_edit_pos = (self.date_edit_pos + 1) % 3 self.selected_item = (self.selected_item + 1) % 6
elif key in [ord('j'), curses.KEY_DOWN, ord('k'), curses.KEY_UP]:
# 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] is_up = key in [ord('k'), curses.KEY_UP]
delta = 1 if is_up else -1 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:
try: try:
if not hasattr(self, 'date_edit_pos'):
self.date_edit_pos = 2 # Default to editing the day
current_date = alarm['date'] current_date = alarm['date']
if self.date_edit_pos == 0: # Year if self.date_edit_pos == 0: # Year
alarm['date'] = current_date.replace(year=max(current_date.year + delta, datetime.now().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))) alarm['date'] = current_date.replace(day=max(1, min(max_day.day, current_date.day + delta)))
except ValueError as e: except ValueError as e:
self._show_error(str(e)) self._show_error(str(e))
return
# Navigation and editing # Weekday Selection
if not alarm['editing_name']: elif self.selected_item == 3: # When weekdays are selected
if key in [ord('h'), curses.KEY_LEFT]: if key in [ord('h'), curses.KEY_LEFT]:
self.selected_item = (self.selected_item - 1) % 6 # Move selection left
self.date_edit_pos = 2 # Reset to day when moving away alarm['current_weekday'] = (alarm['current_weekday'] - 1) % 7
elif key in [ord('l'), curses.KEY_RIGHT]: elif key in [ord('l'), curses.KEY_RIGHT]:
self.selected_item = (self.selected_item + 1) % 6 # Move selection right
self.date_edit_pos = 2 # Reset to day when moving away alarm['current_weekday'] = (alarm['current_weekday'] + 1) % 7
elif key == 32: # SPACE to toggle selection
# Up/Down for editing values current_day = alarm['current_weekday']
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)
if current_day in alarm['weekdays']: if current_day in alarm['weekdays']:
alarm['weekdays'].remove(current_day) alarm['weekdays'].remove(current_day)
else: else:
@ -164,13 +154,23 @@ class InputHandling:
if alarm['weekdays']: if alarm['weekdays']:
alarm['date'] = None alarm['date'] = None
# Name editing # Name Editing
if alarm['editing_name']: 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: if key == curses.KEY_BACKSPACE or key == 127:
alarm['name'] = alarm['name'][:-1] alarm['name'] = alarm['name'][:-1]
elif 32 <= key <= 126: # Printable ASCII elif 32 <= key <= 126: # Printable ASCII
alarm['name'] += chr(key) 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): def _handle_list_alarms_input(self, key):
"""Handle input for alarm list view""" """Handle input for alarm list view"""
total_items = len(self.alarm_list) + 1 # +1 for "Add new alarm" option total_items = len(self.alarm_list) + 1 # +1 for "Add new alarm" option