eee_alarm_clock/clock/ui/ncurses_ui.py

187 lines
6.5 KiB
Python

import curses
import time
from datetime import datetime, date, timedelta
import threading
import logging
import queue
from .utils import draw_error, init_colors
from .active_alarm import draw_active_alarms
from .add_alarm import draw_add_alarm
from .list_alarms import draw_list_alarms
from .main_clock import draw_main_clock
from .input_handlers import InputHandling
class UI(InputHandling):
def __init__(self, alarm_system_manager, control_queue):
# UI State Management
self.alarm_system = alarm_system_manager
self.stop_event = alarm_system_manager.stop_event
self.storage = alarm_system_manager.storage
# Control queue for interacting with AlarmSiren
self.control_queue = control_queue
# Logging
self.logger = logging.getLogger(__name__)
# Active alarm tracking
self.active_alarms = {}
# 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
self.error_message = None
self.error_timestamp = None
# Alarm list
self.alarm_list = []
# Weekday names (to match specification)
self.weekday_names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
# Clear active alarms
self.active_alarms.clear()
def run(self):
"""Start the ncurses UI in a separate thread"""
def ui_thread():
try:
# Start a thread to monitor control queue
monitor_thread = threading.Thread(target=self._monitor_control_queue, daemon=True)
monitor_thread.start()
curses.wrapper(self._main_loop)
except Exception as e:
self.logger.error(f"UI Thread Error: {e}")
finally:
self.stop_event.set()
ui_thread_obj = threading.Thread(target=ui_thread, daemon=True)
ui_thread_obj.start()
return ui_thread_obj
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 _show_error(self, message, duration=30):
"""Display an error message"""
self.error_message = message
self.error_timestamp = time.time()
def _main_loop(self, stdscr):
"""Main ncurses event loop"""
curses.curs_set(0)
stdscr.keypad(1)
stdscr.timeout(100)
time.sleep(0.2)
stdscr.clear()
while not self.stop_event.is_set():
stdscr.erase()
# Draw view based on current state
if self.current_view == 'CLOCK':
draw_main_clock(stdscr)
elif self.current_view == 'ADD_ALARM':
draw_add_alarm(stdscr, {
'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,
'date_edit_pos': getattr(self, 'date_edit_pos', 2)
})
elif self.current_view == 'LIST_ALARMS':
draw_list_alarms(stdscr, {
'alarms': self.alarm_list or [],
'weekday_names': self.weekday_names,
'selected_index': self.selected_item
})
elif self.current_view == 'ACTIVE_ALARMS':
# Draw active alarm view
draw_active_alarms(stdscr, {'active_alarms': self.active_alarms })
# Render error if exists
if self.error_message:
draw_error(stdscr, self.error_message)
self._clear_error_if_expired()
stdscr.refresh()
# Handle input
key = stdscr.getch()
if key != -1:
if key == ord('q'):
# Context-sensitive 'q' key handling
if self.current_view == 'CLOCK':
break # Exit the application only from clock view
else:
self.current_view = 'CLOCK' # Return to clock view from other views
continue
# Context-specific input handling
if self.current_view == 'CLOCK':
self._handle_clock_input(key)
elif self.current_view == 'ADD_ALARM':
self._handle_add_alarm_input(key)
elif self.current_view == 'LIST_ALARMS':
self._handle_list_alarms_input(key)
elif self.current_view == 'ACTIVE_ALARMS':
self._handle_active_alarms_input(key)
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