eee_alarm_clock/clock/ui/ncurses_ui.py

180 lines
6.1 KiB
Python
Raw Normal View History

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()