We have sound! And mostly working ncurses ui!

This commit is contained in:
Kalzu Rekku
2025-01-25 23:24:27 +02:00
parent 4e1c838eaa
commit a48976a762
4 changed files with 428 additions and 442 deletions

View File

@@ -1,225 +1,176 @@
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
from logging_config import setup_logging
import threading
import logging
# Set up logging
logger = setup_logging()
# Import drawing methods from the new module
from ncurses_ui_draw import (
_draw_main_clock,
_draw_add_alarm,
_draw_list_alarms,
_draw_error
)
# class AlarmClockUI:
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
"""
# UI State Management
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.editing_name = " "
self.new_alarm_hour = datetime.now().hour
self.new_alarm_minute = datetime.now().minute
self.new_alarm_selected = 4
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 = []
# Logging
self.logger = logging.getLogger(__name__)
# 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']
def run(self):
"""
Start the ncurses UI
"""
"""Start the ncurses UI in a separate thread"""
def ui_thread():
try:
curses.wrapper(self._main_loop)
except Exception as e:
logger.error(f"Error in UI thread: {e}")
print(f"Error in UI thread: {e}")
self.logger.error(f"UI Thread Error: {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_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()
def _handle_add_alarm_input(self, key):
try:
if key == 27: # Escape
# If in name editing, exit name editing
if self.editing_name:
self.new_alarm_name = self.temp_alarm_name
self.editing_name = False
return
# Otherwise return to main clock
self.selected_menu = 0
return
if key == 10: # Enter
if self.editing_name:
# Exit name editing mode
self.editing_name = False
# Move focus to time selection
self.new_alarm_selected = 0
return
"""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
try:
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,
"name": alarm['name'],
"time": f"{alarm['hour']:02d}:{alarm['minute']:02d}:00",
"enabled": alarm['enabled'],
"repeat_rule": {
"type": "weekly" if self.new_alarm_weekdays else "once",
"days": self.new_alarm_weekdays if self.new_alarm_weekdays else [],
"date": self.new_alarm_date.strftime("%Y-%m-%d") if self.new_alarm_date else date.today().strftime("%Y-%m-%d")
}
"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
}
}
self.storage.save_new_alert(alarm_data)
self.selected_menu = 0
self.current_view = 'CLOCK'
except Exception as e:
self._show_error(str(e))
return
# Numeric input for time when on time selection
if self.new_alarm_selected in [0, 1] and not self.editing_name:
if 48 <= key <= 57: # 0-9 keys
current_digit = int(chr(key))
if self.new_alarm_selected == 0: # Hour
self.new_alarm_hour = current_digit if self.new_alarm_hour < 10 else (self.new_alarm_hour % 10 * 10 + current_digit) % 24
else: # Minute
self.new_alarm_minute = current_digit if self.new_alarm_minute < 10 else (self.new_alarm_minute % 10 * 10 + current_digit) % 60
return
# Use hjkl for navigation and selection
# Navigation and editing
if not alarm['editing_name']:
if key in [ord('h'), curses.KEY_LEFT]:
self.new_alarm_selected = (self.new_alarm_selected - 1) % 6
self.selected_item = (self.selected_item - 1) % 6
elif key in [ord('l'), curses.KEY_RIGHT]:
self.new_alarm_selected = (self.new_alarm_selected + 1) % 6
elif key in [ord('k'), 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 == 3 and len(self.new_alarm_weekdays) < 7:
# Add whole groups of days
if key == ord('k'):
# Options: M-F (0-4), Weekends (5-6), All days
if not self.new_alarm_weekdays:
self.new_alarm_weekdays = list(range(5)) # M-F
elif self.new_alarm_weekdays == list(range(5)):
self.new_alarm_weekdays = [5, 6] # Weekends
elif self.new_alarm_weekdays == [5, 6]:
self.new_alarm_weekdays = list(range(7)) # All days
else:
self.new_alarm_weekdays = [] # Reset
self.new_alarm_weekdays.sort()
elif key in [ord('j'), 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 == 3:
# Cycle through weekday groups
if key == ord('j'):
if not self.new_alarm_weekdays:
self.new_alarm_weekdays = [5, 6] # Weekends
elif self.new_alarm_weekdays == [5, 6]:
self.new_alarm_weekdays = list(range(7)) # All days
elif self.new_alarm_weekdays == list(range(7)):
self.new_alarm_weekdays = list(range(5)) # M-F
else:
self.new_alarm_weekdays = [] # Reset
self.new_alarm_weekdays.sort()
elif key == 32: # Space
if self.new_alarm_selected == 4: # Name editing
if not self.editing_name:
self.editing_name = True
self.temp_alarm_name = self.new_alarm_name
self.new_alarm_name = ""
elif self.new_alarm_selected == 2: # Date
self.new_alarm_date = None
elif self.new_alarm_selected == 3: # Weekdays
# Toggle specific day when on weekday selection
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
# Name editing handling
if self.editing_name:
if key == curses.KEY_BACKSPACE or key == 127:
self.new_alarm_name = self.new_alarm_name[:-1]
elif 32 <= key <= 126: # Printable ASCII
self.new_alarm_name += chr(key)
except Exception as e:
logger.error(f"Error: {e}")
self._show_error(str(e))
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)
def _handle_list_alarms_input(self, key):
"""
Handle input for the list alarms screen
"""
if key == 27: # Escape
self.selected_menu = 0
"""Handle input for alarm list view"""
if key == 27: # ESC
self.current_view = 'CLOCK'
elif key == ord('d'):
# Delete last alarm if exists
if self.alarms:
last_alarm = self.alarms[-1]
# Delete last alarm
if self.alarm_list:
last_alarm = self.alarm_list[-1]
try:
self.storage.remove_saved_alert(last_alarm['id'])
self.alarm_list = self.storage.get_saved_alerts()
except Exception as e:
_show_error(f"Failed to delete alarm: {e}")
logger.error(f"Failed to delete alarm: {e}")
self._show_error(f"Failed to delete alarm: {e}")
def _show_error(self, message, duration=3):
"""Display an error message for a specified duration"""
def _show_error(self, message, duration=30):
"""Display an error message"""
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)
"""Main ncurses event loop"""
curses.curs_set(0)
# Configure screen
stdscr.keypad(1)
stdscr.timeout(100)
@@ -227,64 +178,59 @@ class UI:
stdscr.clear()
while not self.stop_event.is_set():
# Clear the screen
#stdscr.clear()
stdscr.erase()
# Draw appropriate screen based on selected menu
if self.selected_menu == 0:
# Draw view based on current state
if self.current_view == 'CLOCK':
_draw_main_clock(stdscr)
elif self.selected_menu == 1:
elif self.current_view == 'ADD_ALARM':
_draw_add_alarm(stdscr, {
'new_alarm_selected': self.new_alarm_selected,
'new_alarm_name': self.new_alarm_name,
'editing_name': getattr(self, 'editing_name', False),
'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)
'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
})
elif self.selected_menu == 2:
elif self.current_view == 'LIST_ALARMS':
_draw_list_alarms(stdscr, {
'alarms': self.alarms,
'alarms': self.alarm_list or [],
'weekday_names': self.weekday_names
})
# Draw error if exists
# Render error if exists
if self.error_message:
_draw_error(stdscr, self.error_message)
self._clear_error_if_expired()
# Refresh the screen
stdscr.refresh()
# Small sleep to reduce CPU usage
time.sleep(0.2)
# Handle input
key = stdscr.getch()
if key != -1:
# Menu navigation and input handling
if key == ord('q') or key == 27: # 'q' or Escape
if self.selected_menu != 0:
self.selected_menu = 0
if key == ord('q'):
# Context-sensitive 'q' key handling
if self.current_view == 'CLOCK':
break # Exit the application only from clock view
else:
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()
self.current_view = 'CLOCK' # Return to clock view from other views
continue
# Context-specific input handling
if self.selected_menu == 1:
if self.current_view == 'CLOCK':
self._handle_clock_input(key)
elif self.current_view == 'ADD_ALARM':
self._handle_add_alarm_input(key)
elif self.selected_menu == 2:
elif self.current_view == 'LIST_ALARMS':
self._handle_list_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