2025-01-24 23:08:45 +02:00
|
|
|
import curses
|
|
|
|
import time
|
|
|
|
from datetime import datetime, date, timedelta
|
2025-01-25 23:24:27 +02:00
|
|
|
import threading
|
|
|
|
import logging
|
2025-01-26 22:02:23 +02:00
|
|
|
import queue
|
2025-01-25 23:24:27 +02:00
|
|
|
|
|
|
|
# Import drawing methods from the new module
|
|
|
|
from ncurses_ui_draw import (
|
|
|
|
_draw_main_clock,
|
|
|
|
_draw_add_alarm,
|
|
|
|
_draw_list_alarms,
|
2025-01-26 22:02:23 +02:00
|
|
|
_draw_active_alarms,
|
2025-01-25 23:24:27 +02:00
|
|
|
_draw_error
|
|
|
|
)
|
|
|
|
|
2025-01-24 23:08:45 +02:00
|
|
|
class UI:
|
2025-01-26 22:02:23 +02:00
|
|
|
def __init__(self, alarm_system_manager, control_queue):
|
2025-01-25 23:24:27 +02:00
|
|
|
# UI State Management
|
2025-01-24 23:08:45 +02:00
|
|
|
self.alarm_system = alarm_system_manager
|
|
|
|
self.stop_event = alarm_system_manager.stop_event
|
|
|
|
self.storage = alarm_system_manager.storage
|
|
|
|
|
2025-01-26 22:02:23 +02:00
|
|
|
# Control queue for interacting with AlarmSiren
|
|
|
|
self.control_queue = control_queue
|
|
|
|
|
2025-01-25 23:24:27 +02:00
|
|
|
# Logging
|
|
|
|
self.logger = logging.getLogger(__name__)
|
|
|
|
|
2025-01-26 22:02:23 +02:00
|
|
|
# Active alarm tracking
|
|
|
|
self.active_alarms = {}
|
|
|
|
|
2025-01-25 23:24:27 +02:00
|
|
|
# UI State
|
|
|
|
self.reset_ui_state()
|
|
|
|
|
|
|
|
def reset_ui_state(self):
|
|
|
|
"""Reset all UI state variables to their initial values"""
|
|
|
|
# Menu states
|
|
|
|
self.current_view = 'CLOCK'
|
|
|
|
self.selected_item = 0
|
|
|
|
|
|
|
|
# Alarm Creation State
|
|
|
|
self.alarm_draft = {
|
|
|
|
'name': 'New Alarm',
|
|
|
|
'hour': datetime.now().hour,
|
|
|
|
'minute': datetime.now().minute,
|
|
|
|
'enabled': True,
|
|
|
|
'date': None,
|
|
|
|
'weekdays': [],
|
|
|
|
'editing_name': False,
|
|
|
|
'temp_name': ''
|
|
|
|
}
|
|
|
|
|
|
|
|
# Error handling
|
2025-01-24 23:08:45 +02:00
|
|
|
self.error_message = None
|
|
|
|
self.error_timestamp = None
|
|
|
|
|
2025-01-25 23:24:27 +02:00
|
|
|
# Alarm list
|
|
|
|
self.alarm_list = []
|
|
|
|
|
|
|
|
# Weekday names (to match specification)
|
|
|
|
self.weekday_names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
|
|
|
|
2025-01-26 22:02:23 +02:00
|
|
|
# Clear active alarms
|
|
|
|
self.active_alarms.clear()
|
|
|
|
|
2025-01-24 23:08:45 +02:00
|
|
|
def run(self):
|
2025-01-25 23:24:27 +02:00
|
|
|
"""Start the ncurses UI in a separate thread"""
|
2025-01-24 23:08:45 +02:00
|
|
|
def ui_thread():
|
|
|
|
try:
|
2025-01-26 22:02:23 +02:00
|
|
|
# Start a thread to monitor control queue
|
|
|
|
monitor_thread = threading.Thread(target=self._monitor_control_queue, daemon=True)
|
|
|
|
monitor_thread.start()
|
|
|
|
|
2025-01-24 23:08:45 +02:00
|
|
|
curses.wrapper(self._main_loop)
|
|
|
|
except Exception as e:
|
2025-01-25 23:24:27 +02:00
|
|
|
self.logger.error(f"UI Thread Error: {e}")
|
2025-01-24 23:08:45 +02:00
|
|
|
finally:
|
|
|
|
self.stop_event.set()
|
|
|
|
|
|
|
|
ui_thread_obj = threading.Thread(target=ui_thread, daemon=True)
|
|
|
|
ui_thread_obj.start()
|
|
|
|
return ui_thread_obj
|
|
|
|
|
2025-01-26 22:02:23 +02:00
|
|
|
def _monitor_control_queue(self):
|
|
|
|
"""Monitor the control queue for alarm events"""
|
|
|
|
while not self.stop_event.is_set():
|
|
|
|
try:
|
|
|
|
# Non-blocking check of control queue
|
|
|
|
try:
|
|
|
|
control_msg = self.control_queue.get(timeout=1)
|
|
|
|
|
|
|
|
# Handle different types of control messages
|
|
|
|
if control_msg['type'] == 'trigger':
|
|
|
|
# Store triggered alarm
|
|
|
|
alarm_id = control_msg['alarm_id']
|
|
|
|
self.active_alarms[alarm_id] = control_msg['info']
|
|
|
|
|
|
|
|
# If not already in alarm view, switch to it
|
|
|
|
if self.current_view != 'ACTIVE_ALARMS':
|
|
|
|
self.current_view = 'ACTIVE_ALARMS'
|
|
|
|
|
|
|
|
except queue.Empty:
|
|
|
|
pass
|
|
|
|
|
|
|
|
time.sleep(0.1)
|
|
|
|
except Exception as e:
|
|
|
|
self.logger.error(f"Error monitoring control queue: {e}")
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
def _handle_active_alarms_input(self, key):
|
|
|
|
"""Handle input for active alarms view"""
|
|
|
|
if not self.active_alarms:
|
|
|
|
# No active alarms, return to clock view
|
|
|
|
self.current_view = 'CLOCK'
|
|
|
|
return
|
|
|
|
|
|
|
|
# Get the first (or only) active alarm
|
|
|
|
alarm_id = list(self.active_alarms.keys())[0]
|
|
|
|
|
|
|
|
if key == ord('s'): # Snooze
|
|
|
|
# Send snooze command to control queue
|
|
|
|
self.control_queue.put({
|
|
|
|
'type': 'snooze',
|
|
|
|
'alarm_id': alarm_id
|
|
|
|
})
|
|
|
|
# Remove from active alarms
|
|
|
|
del self.active_alarms[alarm_id]
|
|
|
|
|
|
|
|
# Optional: show a snooze confirmation
|
|
|
|
self._show_error("Alarm Snoozed")
|
|
|
|
|
|
|
|
elif key == ord('d'): # Dismiss
|
|
|
|
# Send dismiss command to control queue
|
|
|
|
self.control_queue.put({
|
|
|
|
'type': 'dismiss',
|
|
|
|
'alarm_id': alarm_id
|
|
|
|
})
|
|
|
|
# Remove from active alarms
|
|
|
|
del self.active_alarms[alarm_id]
|
|
|
|
|
|
|
|
# Return to clock view
|
|
|
|
self.current_view = 'CLOCK'
|
|
|
|
|
2025-01-25 23:24:27 +02:00
|
|
|
def _handle_clock_input(self, key):
|
|
|
|
"""Handle input on the clock view"""
|
|
|
|
if key == ord('a'):
|
|
|
|
self.current_view = 'ADD_ALARM'
|
|
|
|
elif key == ord('s'):
|
|
|
|
self.current_view = 'LIST_ALARMS'
|
|
|
|
self.alarm_list = self.storage.get_saved_alerts()
|
|
|
|
|
2025-01-24 23:08:45 +02:00
|
|
|
def _handle_add_alarm_input(self, key):
|
2025-01-25 23:24:27 +02:00
|
|
|
"""Comprehensive input handling for alarm creation"""
|
|
|
|
alarm = self.alarm_draft
|
|
|
|
|
|
|
|
# Escape key handling
|
|
|
|
if key == 27: # ESC
|
|
|
|
if alarm['editing_name']:
|
|
|
|
# Cancel name editing
|
|
|
|
alarm['name'] = alarm['temp_name']
|
|
|
|
alarm['editing_name'] = False
|
|
|
|
else:
|
|
|
|
# Return to clock view
|
|
|
|
self.current_view = 'CLOCK'
|
|
|
|
return
|
|
|
|
|
|
|
|
# Enter key handling
|
|
|
|
if key == 10: # ENTER
|
|
|
|
if alarm['editing_name']:
|
|
|
|
# Finish name editing
|
|
|
|
alarm['editing_name'] = False
|
|
|
|
self.selected_item = 0
|
|
|
|
else:
|
|
|
|
# Save alarm
|
2025-01-25 15:53:21 +02:00
|
|
|
try:
|
|
|
|
alarm_data = {
|
2025-01-25 23:24:27 +02:00
|
|
|
"name": alarm['name'],
|
|
|
|
"time": f"{alarm['hour']:02d}:{alarm['minute']:02d}:00",
|
|
|
|
"enabled": alarm['enabled'],
|
2025-01-25 15:53:21 +02:00
|
|
|
"repeat_rule": {
|
2025-01-25 23:24:27 +02:00
|
|
|
"type": "weekly" if alarm['weekdays'] else "once",
|
|
|
|
"days_of_week": [self.weekday_names[day].lower() for day in alarm['weekdays']],
|
|
|
|
"at": alarm['date'].strftime("%d.%m.%Y") if alarm['date'] else None
|
|
|
|
}
|
2025-01-25 15:53:21 +02:00
|
|
|
}
|
|
|
|
self.storage.save_new_alert(alarm_data)
|
2025-01-25 23:24:27 +02:00
|
|
|
self.current_view = 'CLOCK'
|
2025-01-25 15:53:21 +02:00
|
|
|
except Exception as e:
|
|
|
|
self._show_error(str(e))
|
|
|
|
return
|
2025-01-25 23:24:27 +02:00
|
|
|
|
|
|
|
# Navigation and editing
|
|
|
|
if not alarm['editing_name']:
|
2025-01-25 15:53:21 +02:00
|
|
|
if key in [ord('h'), curses.KEY_LEFT]:
|
2025-01-25 23:24:27 +02:00
|
|
|
self.selected_item = (self.selected_item - 1) % 6
|
2025-01-25 15:53:21 +02:00
|
|
|
elif key in [ord('l'), curses.KEY_RIGHT]:
|
2025-01-25 23:24:27 +02:00
|
|
|
self.selected_item = (self.selected_item + 1) % 6
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
# 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']
|
|
|
|
|
|
|
|
# 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)
|
2025-01-24 23:08:45 +02:00
|
|
|
|
|
|
|
def _handle_list_alarms_input(self, key):
|
2025-01-25 23:24:27 +02:00
|
|
|
"""Handle input for alarm list view"""
|
2025-01-28 10:55:14 +02:00
|
|
|
total_items = len(self.alarm_list) + 1 # +1 for "Add new alarm" option
|
|
|
|
|
2025-01-25 23:24:27 +02:00
|
|
|
if key == 27: # ESC
|
|
|
|
self.current_view = 'CLOCK'
|
2025-01-28 10:55:14 +02:00
|
|
|
elif key in [ord('j'), curses.KEY_DOWN]:
|
|
|
|
self.selected_item = (self.selected_item + 1) % total_items
|
|
|
|
elif key in [ord('k'), curses.KEY_UP]:
|
|
|
|
self.selected_item = (self.selected_item - 1) % total_items
|
2025-01-24 23:08:45 +02:00
|
|
|
elif key == ord('d'):
|
2025-01-28 10:55:14 +02:00
|
|
|
# Only delete if a real alarm is selected (not the "Add new" option)
|
|
|
|
if self.selected_item < len(self.alarm_list) and self.alarm_list:
|
2025-01-24 23:08:45 +02:00
|
|
|
try:
|
2025-01-28 10:55:14 +02:00
|
|
|
alarm_to_delete = self.alarm_list[self.selected_item]
|
|
|
|
self.storage.remove_saved_alert(alarm_to_delete['id'])
|
2025-01-25 23:24:27 +02:00
|
|
|
self.alarm_list = self.storage.get_saved_alerts()
|
2025-01-28 10:55:14 +02:00
|
|
|
# Adjust selected item if needed
|
|
|
|
if self.selected_item >= len(self.alarm_list):
|
|
|
|
self.selected_item = len(self.alarm_list)
|
2025-01-24 23:08:45 +02:00
|
|
|
except Exception as e:
|
2025-01-25 23:24:27 +02:00
|
|
|
self._show_error(f"Failed to delete alarm: {e}")
|
2025-01-28 10:55:14 +02:00
|
|
|
elif key in [ord('a'), 10]: # 'a' or Enter
|
|
|
|
if self.selected_item == len(self.alarm_list):
|
|
|
|
# "Add new alarm" option selected
|
|
|
|
self.current_view = 'ADD_ALARM'
|
|
|
|
else:
|
|
|
|
# TODO: Implement alarm editing
|
|
|
|
self._show_error("Alarm editing not implemented yet")
|
2025-01-24 23:08:45 +02:00
|
|
|
|
2025-01-25 23:24:27 +02:00
|
|
|
def _show_error(self, message, duration=30):
|
|
|
|
"""Display an error message"""
|
2025-01-24 23:08:45 +02:00
|
|
|
self.error_message = message
|
|
|
|
self.error_timestamp = time.time()
|
|
|
|
|
|
|
|
def _main_loop(self, stdscr):
|
2025-01-25 23:24:27 +02:00
|
|
|
"""Main ncurses event loop"""
|
2025-01-24 23:08:45 +02:00
|
|
|
curses.curs_set(0)
|
|
|
|
stdscr.keypad(1)
|
|
|
|
stdscr.timeout(100)
|
|
|
|
|
2025-01-25 15:53:21 +02:00
|
|
|
time.sleep(0.2)
|
|
|
|
stdscr.clear()
|
|
|
|
|
2025-01-24 23:08:45 +02:00
|
|
|
while not self.stop_event.is_set():
|
2025-01-25 15:53:21 +02:00
|
|
|
stdscr.erase()
|
2025-01-24 23:08:45 +02:00
|
|
|
|
2025-01-25 23:24:27 +02:00
|
|
|
# Draw view based on current state
|
|
|
|
if self.current_view == 'CLOCK':
|
2025-01-24 23:08:45 +02:00
|
|
|
_draw_main_clock(stdscr)
|
2025-01-25 23:24:27 +02:00
|
|
|
elif self.current_view == 'ADD_ALARM':
|
2025-01-24 23:08:45 +02:00
|
|
|
_draw_add_alarm(stdscr, {
|
2025-01-25 23:24:27 +02:00
|
|
|
'new_alarm_selected': self.selected_item,
|
|
|
|
'new_alarm_name': self.alarm_draft['name'],
|
|
|
|
'new_alarm_hour': self.alarm_draft['hour'],
|
|
|
|
'new_alarm_minute': self.alarm_draft['minute'],
|
|
|
|
'new_alarm_enabled': self.alarm_draft['enabled'],
|
|
|
|
'new_alarm_date': self.alarm_draft['date'],
|
|
|
|
'new_alarm_weekdays': self.alarm_draft['weekdays'],
|
|
|
|
'weekday_names': self.weekday_names
|
2025-01-24 23:08:45 +02:00
|
|
|
})
|
2025-01-25 23:24:27 +02:00
|
|
|
elif self.current_view == 'LIST_ALARMS':
|
2025-01-24 23:08:45 +02:00
|
|
|
_draw_list_alarms(stdscr, {
|
2025-01-25 23:24:27 +02:00
|
|
|
'alarms': self.alarm_list or [],
|
2025-01-28 10:55:14 +02:00
|
|
|
'weekday_names': self.weekday_names,
|
|
|
|
'selected_index': self.selected_item
|
2025-01-24 23:08:45 +02:00
|
|
|
})
|
2025-01-26 22:02:23 +02:00
|
|
|
elif self.current_view == 'ACTIVE_ALARMS':
|
|
|
|
# Draw active alarm view
|
|
|
|
_draw_active_alarms(stdscr, {'active_alarms': self.active_alarms })
|
2025-01-24 23:08:45 +02:00
|
|
|
|
2025-01-25 23:24:27 +02:00
|
|
|
# Render error if exists
|
2025-01-25 15:53:21 +02:00
|
|
|
if self.error_message:
|
|
|
|
_draw_error(stdscr, self.error_message)
|
|
|
|
self._clear_error_if_expired()
|
|
|
|
|
2025-01-24 23:08:45 +02:00
|
|
|
stdscr.refresh()
|
|
|
|
|
|
|
|
# Handle input
|
|
|
|
key = stdscr.getch()
|
|
|
|
if key != -1:
|
2025-01-25 23:24:27 +02:00
|
|
|
if key == ord('q'):
|
|
|
|
# Context-sensitive 'q' key handling
|
|
|
|
if self.current_view == 'CLOCK':
|
|
|
|
break # Exit the application only from clock view
|
2025-01-25 15:53:21 +02:00
|
|
|
else:
|
2025-01-25 23:24:27 +02:00
|
|
|
self.current_view = 'CLOCK' # Return to clock view from other views
|
|
|
|
continue
|
2025-01-24 23:08:45 +02:00
|
|
|
|
|
|
|
# Context-specific input handling
|
2025-01-25 23:24:27 +02:00
|
|
|
if self.current_view == 'CLOCK':
|
|
|
|
self._handle_clock_input(key)
|
|
|
|
elif self.current_view == 'ADD_ALARM':
|
2025-01-24 23:08:45 +02:00
|
|
|
self._handle_add_alarm_input(key)
|
2025-01-25 23:24:27 +02:00
|
|
|
elif self.current_view == 'LIST_ALARMS':
|
2025-01-24 23:08:45 +02:00
|
|
|
self._handle_list_alarms_input(key)
|
2025-01-26 22:02:23 +02:00
|
|
|
elif self.current_view == 'ACTIVE_ALARMS':
|
|
|
|
self._handle_active_alarms_input(key)
|
2025-01-25 23:24:27 +02:00
|
|
|
|
|
|
|
time.sleep(0.2)
|
|
|
|
|
|
|
|
def _clear_error_if_expired(self):
|
|
|
|
"""Clear error message if expired"""
|
|
|
|
if self.error_message and self.error_timestamp:
|
|
|
|
if time.time() - self.error_timestamp > 3:
|
|
|
|
self.error_message = None
|
|
|
|
self.error_timestamp = None
|