231 lines
8.9 KiB
Python
231 lines
8.9 KiB
Python
![]() |
import curses
|
||
|
import time
|
||
|
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
|
||
|
|
||
|
class UI:
|
||
|
def __init__(self, alarm_system_manager):
|
||
|
"""
|
||
|
Initialize the ncurses UI for the alarm system
|
||
|
|
||
|
Args:
|
||
|
alarm_system_manager (AlarmSystemManager): The main alarm system manager
|
||
|
"""
|
||
|
self.alarm_system = alarm_system_manager
|
||
|
self.stop_event = alarm_system_manager.stop_event
|
||
|
self.storage = alarm_system_manager.storage
|
||
|
|
||
|
# UI state variables
|
||
|
self.selected_menu = 0
|
||
|
self.new_alarm_name = "Alarm"
|
||
|
self.new_alarm_hour = datetime.now().hour
|
||
|
self.new_alarm_minute = datetime.now().minute
|
||
|
self.new_alarm_selected = 0
|
||
|
self.new_alarm_date = None
|
||
|
self.new_alarm_weekdays = []
|
||
|
self.new_alarm_enabled = True
|
||
|
self.weekday_names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||
|
self.alarms = []
|
||
|
self.error_message = None
|
||
|
self.error_timestamp = None
|
||
|
|
||
|
def run(self):
|
||
|
"""
|
||
|
Start the ncurses UI
|
||
|
"""
|
||
|
def ui_thread():
|
||
|
try:
|
||
|
curses.wrapper(self._main_loop)
|
||
|
except Exception as e:
|
||
|
print(f"Error in UI thread: {e}")
|
||
|
finally:
|
||
|
self.stop_event.set()
|
||
|
|
||
|
import threading
|
||
|
ui_thread_obj = threading.Thread(target=ui_thread, daemon=True)
|
||
|
ui_thread_obj.start()
|
||
|
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
|
||
|
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)
|
||
|
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:
|
||
|
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]
|
||
|
|
||
|
def _handle_list_alarms_input(self, key):
|
||
|
"""
|
||
|
Handle input for the list alarms screen
|
||
|
"""
|
||
|
if key == 27: # Escape
|
||
|
self.selected_menu = 0
|
||
|
elif key == ord('d'):
|
||
|
# Delete last alarm if exists
|
||
|
if self.alarms:
|
||
|
last_alarm = self.alarms[-1]
|
||
|
try:
|
||
|
self.storage.remove_saved_alert(last_alarm['id'])
|
||
|
except Exception as e:
|
||
|
print(f"Failed to delete alarm: {e}")
|
||
|
|
||
|
def _show_error(self, message, duration=3):
|
||
|
"""Display an error message for a specified duration"""
|
||
|
self.error_message = message
|
||
|
self.error_timestamp = time.time()
|
||
|
|
||
|
def _clear_error_if_expired(self):
|
||
|
"""Clear error message if it has been displayed long enough"""
|
||
|
if self.error_message and self.error_timestamp:
|
||
|
if time.time() - self.error_timestamp > 3: # 3 seconds
|
||
|
self.error_message = None
|
||
|
self.error_timestamp = None
|
||
|
|
||
|
def _main_loop(self, stdscr):
|
||
|
"""
|
||
|
Main ncurses event loop
|
||
|
"""
|
||
|
# Initialize color pairs
|
||
|
curses.start_color()
|
||
|
curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK)
|
||
|
curses.init_pair(2, curses.COLOR_YELLOW, curses.COLOR_BLACK)
|
||
|
curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
|
||
|
curses.curs_set(0)
|
||
|
|
||
|
# Configure screen
|
||
|
stdscr.keypad(1)
|
||
|
stdscr.timeout(100)
|
||
|
|
||
|
while not self.stop_event.is_set():
|
||
|
# Clear the screen
|
||
|
stdscr.clear()
|
||
|
|
||
|
# Draw appropriate screen based on selected menu
|
||
|
if self.selected_menu == 0:
|
||
|
_draw_main_clock(stdscr)
|
||
|
elif self.selected_menu == 1:
|
||
|
_draw_add_alarm(stdscr, {
|
||
|
'new_alarm_selected': self.new_alarm_selected,
|
||
|
'new_alarm_name': self.new_alarm_name,
|
||
|
'new_alarm_hour': self.new_alarm_hour,
|
||
|
'new_alarm_minute': self.new_alarm_minute,
|
||
|
'new_alarm_enabled': self.new_alarm_enabled,
|
||
|
'new_alarm_date': self.new_alarm_date,
|
||
|
'new_alarm_weekdays': self.new_alarm_weekdays,
|
||
|
'weekday_names': self.weekday_names,
|
||
|
'new_alarm_snooze_enabled': getattr(self, 'new_alarm_snooze_enabled', False),
|
||
|
'new_alarm_snooze_duration': getattr(self, 'new_alarm_snooze_duration', 5),
|
||
|
'new_alarm_snooze_max_count': getattr(self, 'new_alarm_snooze_max_count', 3)
|
||
|
})
|
||
|
elif self.selected_menu == 2:
|
||
|
_draw_list_alarms(stdscr, {
|
||
|
'alarms': self.alarms,
|
||
|
'weekday_names': self.weekday_names
|
||
|
})
|
||
|
|
||
|
# Refresh the screen
|
||
|
stdscr.refresh()
|
||
|
|
||
|
# Small sleep to reduce CPU usage
|
||
|
time.sleep(0.1)
|
||
|
|
||
|
# Handle input
|
||
|
key = stdscr.getch()
|
||
|
if key != -1:
|
||
|
# Menu navigation and input handling
|
||
|
if key == ord('q') or key == 27: # 'q' or Escape
|
||
|
break
|
||
|
elif key == ord('c'): # Clock/Home screen
|
||
|
self.selected_menu = 0
|
||
|
elif key == ord('a'): # Add Alarm
|
||
|
self.selected_menu = 1
|
||
|
elif key == ord('l'): # List Alarms
|
||
|
self.selected_menu = 2
|
||
|
self.alarms = self.storage.get_saved_alerts()
|
||
|
|
||
|
# Context-specific input handling
|
||
|
if self.selected_menu == 1:
|
||
|
self._handle_add_alarm_input(key)
|
||
|
elif self.selected_menu == 2:
|
||
|
self._handle_list_alarms_input(key)
|