This UX stuff is hard!. Trying to make the add alarm functionality to work over the ncurses...

This commit is contained in:
Kalzu Rekku 2025-01-25 15:53:21 +02:00
parent 56827825b3
commit 4e1c838eaa
4 changed files with 154 additions and 127 deletions

View File

@ -181,38 +181,3 @@ class AlarmSiren:
if alarm_id in self.active_alarms: if alarm_id in self.active_alarms:
logger.info(f"Dismissed alarm {alarm_id}") logger.info(f"Dismissed alarm {alarm_id}")
del self.active_alarms[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()

View File

@ -9,7 +9,6 @@ def setup_logging(log_dir="logs", log_file="alarm_system.log", level=logging.DEB
level=level, level=level,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[ handlers=[
logging.StreamHandler(),
logging.FileHandler(log_path, mode='a', encoding='utf-8') logging.FileHandler(log_path, mode='a', encoding='utf-8')
] ]
) )

View File

@ -4,6 +4,10 @@ from datetime import datetime, date, timedelta
import os import os
from big_digits import BIG_DIGITS 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 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: class UI:
def __init__(self, alarm_system_manager): def __init__(self, alarm_system_manager):
@ -20,9 +24,10 @@ class UI:
# UI state variables # UI state variables
self.selected_menu = 0 self.selected_menu = 0
self.new_alarm_name = "Alarm" self.new_alarm_name = "Alarm"
self.editing_name = " "
self.new_alarm_hour = datetime.now().hour self.new_alarm_hour = datetime.now().hour
self.new_alarm_minute = datetime.now().minute 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_date = None
self.new_alarm_weekdays = [] self.new_alarm_weekdays = []
self.new_alarm_enabled = True self.new_alarm_enabled = True
@ -39,6 +44,7 @@ class UI:
try: try:
curses.wrapper(self._main_loop) curses.wrapper(self._main_loop)
except Exception as e: except Exception as e:
logger.error(f"Error in UI thread: {e}")
print(f"Error in UI thread: {e}") print(f"Error in UI thread: {e}")
finally: finally:
self.stop_event.set() self.stop_event.set()
@ -49,90 +55,130 @@ class UI:
return ui_thread_obj return ui_thread_obj
def _handle_add_alarm_input(self, key): def _handle_add_alarm_input(self, key):
""" try:
Handle input for adding a new alarm if key == 27: # Escape
""" # If in name editing, exit name editing
if key == 27: # Escape if self.editing_name:
self.selected_menu = 0 self.new_alarm_name = self.temp_alarm_name
return self.editing_name = False
return
if key == 10: # Enter # Otherwise return to main clock
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
self.selected_menu = 0 self.selected_menu = 0
self.new_alarm_hour = datetime.now().hour return
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: if key == 10: # Enter
self.new_alarm_selected = (self.new_alarm_selected - 1) % 6 if self.editing_name:
elif key == curses.KEY_RIGHT: # Exit name editing mode
self.new_alarm_selected = (self.new_alarm_selected + 1) % 6 self.editing_name = False
elif key == 32: # Space # Move focus to time selection
if self.new_alarm_selected == 2: # Date self.new_alarm_selected = 0
self.new_alarm_date = None return
elif self.new_alarm_selected == 3: # Weekdays
current_day = len(self.new_alarm_weekdays) try:
if current_day < 7: alarm_data = {
if current_day in self.new_alarm_weekdays: "name": self.new_alarm_name,
self.new_alarm_weekdays.remove(current_day) "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: 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() self.new_alarm_weekdays.sort()
elif self.new_alarm_selected == 5: # Enabled toggle elif key in [ord('j'), curses.KEY_DOWN]:
self.new_alarm_enabled = not self.new_alarm_enabled 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
elif key == curses.KEY_UP: # Name editing handling
if self.new_alarm_selected == 0: if self.editing_name:
self.new_alarm_hour = (self.new_alarm_hour + 1) % 24 if key == curses.KEY_BACKSPACE or key == 127:
elif self.new_alarm_selected == 1: self.new_alarm_name = self.new_alarm_name[:-1]
self.new_alarm_minute = (self.new_alarm_minute + 1) % 60 elif 32 <= key <= 126: # Printable ASCII
elif self.new_alarm_selected == 2: self.new_alarm_name += chr(key)
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: except Exception as e:
if self.new_alarm_selected == 0: logger.error(f"Error: {e}")
self.new_alarm_hour = (self.new_alarm_hour - 1) % 24 self._show_error(str(e))
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]
def _handle_list_alarms_input(self, key): def _handle_list_alarms_input(self, key):
""" """
@ -147,7 +193,8 @@ class UI:
try: try:
self.storage.remove_saved_alert(last_alarm['id']) self.storage.remove_saved_alert(last_alarm['id'])
except Exception as e: 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): def _show_error(self, message, duration=3):
"""Display an error message for a specified duration""" """Display an error message for a specified duration"""
@ -176,9 +223,13 @@ class UI:
stdscr.keypad(1) stdscr.keypad(1)
stdscr.timeout(100) stdscr.timeout(100)
time.sleep(0.2)
stdscr.clear()
while not self.stop_event.is_set(): while not self.stop_event.is_set():
# Clear the screen # Clear the screen
stdscr.clear() #stdscr.clear()
stdscr.erase()
# Draw appropriate screen based on selected menu # Draw appropriate screen based on selected menu
if self.selected_menu == 0: if self.selected_menu == 0:
@ -187,6 +238,7 @@ class UI:
_draw_add_alarm(stdscr, { _draw_add_alarm(stdscr, {
'new_alarm_selected': self.new_alarm_selected, 'new_alarm_selected': self.new_alarm_selected,
'new_alarm_name': self.new_alarm_name, 'new_alarm_name': self.new_alarm_name,
'editing_name': getattr(self, 'editing_name', False),
'new_alarm_hour': self.new_alarm_hour, 'new_alarm_hour': self.new_alarm_hour,
'new_alarm_minute': self.new_alarm_minute, 'new_alarm_minute': self.new_alarm_minute,
'new_alarm_enabled': self.new_alarm_enabled, 'new_alarm_enabled': self.new_alarm_enabled,
@ -203,18 +255,26 @@ class UI:
'weekday_names': self.weekday_names '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 # Refresh the screen
stdscr.refresh() stdscr.refresh()
# Small sleep to reduce CPU usage # Small sleep to reduce CPU usage
time.sleep(0.1) time.sleep(0.2)
# Handle input # Handle input
key = stdscr.getch() key = stdscr.getch()
if key != -1: if key != -1:
# Menu navigation and input handling # Menu navigation and input handling
if key == ord('q') or key == 27: # 'q' or Escape 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 elif key == ord('c'): # Clock/Home screen
self.selected_menu = 0 self.selected_menu = 0
elif key == ord('a'): # Add Alarm elif key == ord('a'): # Add Alarm

View File

@ -6,13 +6,16 @@ def _draw_error(stdscr, error_message):
"""Draw error message if present""" """Draw error message if present"""
if error_message: if error_message:
height, width = stdscr.getmaxyx() 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 error_y = height - 4 # Show near bottom of screen
# Red color for errors # 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.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): def _draw_big_digit(stdscr, y, x, digit, big_digits):
""" """
@ -83,7 +86,7 @@ def _draw_add_alarm(stdscr, context):
height, width = stdscr.getmaxyx() height, width = stdscr.getmaxyx()
form_y = height // 2 - 3 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 # Name input
if context['new_alarm_selected'] == 4: if context['new_alarm_selected'] == 4: