This UX stuff is hard!. Trying to make the add alarm functionality to work over the ncurses...
This commit is contained in:
parent
56827825b3
commit
4e1c838eaa
@ -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()
|
|
||||||
|
@ -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')
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user