180 lines
6.1 KiB
Python
180 lines
6.1 KiB
Python
import curses
|
|
import time
|
|
import threading
|
|
import logging
|
|
import queue
|
|
from datetime import datetime
|
|
|
|
from .utils import init_colors, draw_error, ColorScheme
|
|
from .active_alarm import ActiveAlarmView
|
|
from .add_alarm import AddAlarmView
|
|
from .list_alarms import ListAlarmsView
|
|
from .main_clock import MainClockView
|
|
|
|
class UI:
|
|
def __init__(self, alarm_system_manager, control_queue):
|
|
"""Initialize the UI system"""
|
|
# System components
|
|
self.alarm_system = alarm_system_manager
|
|
self.stop_event = alarm_system_manager.stop_event
|
|
self.storage = alarm_system_manager.storage
|
|
self.control_queue = control_queue
|
|
|
|
# Logging
|
|
self.logger = logging.getLogger(__name__)
|
|
|
|
# Initialize views
|
|
self._init_views()
|
|
|
|
# UI State
|
|
self.current_view = 'CLOCK'
|
|
self.error_message = None
|
|
self.error_timestamp = None
|
|
|
|
def _init_views(self):
|
|
"""Initialize all view classes"""
|
|
self.views = {
|
|
'CLOCK': MainClockView(),
|
|
'ADD_ALARM': AddAlarmView(self.storage, self.control_queue),
|
|
'LIST_ALARMS': ListAlarmsView(self.storage),
|
|
'ACTIVE_ALARMS': ActiveAlarmView(self.storage, self.control_queue)
|
|
}
|
|
|
|
def run(self):
|
|
"""Start the ncurses UI in a separate thread"""
|
|
def ui_thread():
|
|
try:
|
|
# Start control queue monitor
|
|
monitor_thread = threading.Thread(
|
|
target=self._monitor_control_queue,
|
|
daemon=True
|
|
)
|
|
monitor_thread.start()
|
|
|
|
# Start UI
|
|
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)
|
|
|
|
if control_msg['type'] == 'trigger':
|
|
# Update active alarms view
|
|
active_view = self.views['ACTIVE_ALARMS']
|
|
alarm_id = control_msg['alarm_id']
|
|
active_view.update_active_alarms({
|
|
alarm_id: control_msg['info']
|
|
})
|
|
|
|
# Switch to active alarms view
|
|
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=3):
|
|
"""Display an error message"""
|
|
self.error_message = message
|
|
self.error_timestamp = time.time()
|
|
|
|
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
|
|
|
|
def _main_loop(self, stdscr):
|
|
"""Main ncurses event loop"""
|
|
# Setup curses
|
|
curses.curs_set(0) # Hide cursor
|
|
stdscr.keypad(1) # Enable keypad
|
|
stdscr.timeout(100) # Non-blocking input
|
|
init_colors() # Initialize color pairs
|
|
|
|
while not self.stop_event.is_set():
|
|
# Clear screen
|
|
stdscr.erase()
|
|
|
|
# Get current view
|
|
current_view = self.views.get(self.current_view)
|
|
if not current_view:
|
|
self.logger.error(f"Invalid view: {self.current_view}")
|
|
break
|
|
|
|
try:
|
|
# Draw current view
|
|
current_view.draw(stdscr)
|
|
|
|
# Handle any error messages
|
|
if self.error_message:
|
|
draw_error(stdscr, self.error_message)
|
|
self._clear_error_if_expired()
|
|
|
|
# Refresh screen
|
|
stdscr.refresh()
|
|
|
|
# Handle input
|
|
key = stdscr.getch()
|
|
if key != -1:
|
|
# Handle quit key globally
|
|
if key == ord('q'):
|
|
if self.current_view == 'CLOCK':
|
|
break # Exit application from clock view
|
|
else:
|
|
self.current_view = 'CLOCK' # Return to clock from other views
|
|
continue
|
|
|
|
# Let current view handle input
|
|
result = current_view.handle_input(key)
|
|
|
|
# Handle tuple result (view, data) or just a view change
|
|
if isinstance(result, tuple):
|
|
next_view, data = result
|
|
else:
|
|
next_view, data = result, None
|
|
|
|
# Handle quitting
|
|
if next_view == 'QUIT':
|
|
break
|
|
|
|
elif next_view:
|
|
# Update list alarms view data when switching to it
|
|
if next_view == 'LIST_ALARMS':
|
|
self.views['LIST_ALARMS'].update_alarms(
|
|
self.storage.get_saved_alerts()
|
|
)
|
|
|
|
# Handle editing an alarm by passing data to AddAlarmView
|
|
elif next_view == 'ADD_ALARM' and data:
|
|
self.views['ADD_ALARM'].set_alarm_data(data)
|
|
|
|
self.current_view = next_view
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error in main loop: {e}")
|
|
self._show_error(str(e))
|
|
|
|
time.sleep(0.1) # Prevent CPU hogging
|
|
|
|
def stop(self):
|
|
"""Stop the UI system"""
|
|
self.stop_event.set()
|