Restructure of ui package is now in working state. And we now support editing old alarms.
This commit is contained in:
parent
7eafbf5ebd
commit
f59048c2d0
@ -2,68 +2,114 @@ import curses
|
||||
from datetime import datetime
|
||||
from .utils import init_colors, draw_big_digit
|
||||
|
||||
def draw_active_alarms(stdscr, context):
|
||||
"""Draw the active alarms"""
|
||||
init_colors()
|
||||
height, width = stdscr.getmaxyx()
|
||||
current_time = datetime.now()
|
||||
class ActiveAlarmView:
|
||||
def __init__(self, storage, control_queue):
|
||||
self.storage = storage
|
||||
self.control_queue = control_queue
|
||||
self.active_alarms = {}
|
||||
|
||||
# Draw the main clock (original position)
|
||||
time_str = current_time.strftime("%H:%M:%S")
|
||||
digit_width = 14
|
||||
total_width = digit_width * len(time_str)
|
||||
start_x = (width - total_width) // 2
|
||||
start_y = (height - 7) // 2 - 4 # Original position from _draw_main_clock
|
||||
def reset_state(self):
|
||||
"""Reset the view state"""
|
||||
self.active_alarms.clear()
|
||||
|
||||
# Draw blinking dot
|
||||
if int(current_time.timestamp()) % 2 == 0:
|
||||
def draw(self, stdscr):
|
||||
"""Draw the active alarm screen"""
|
||||
init_colors()
|
||||
height, width = stdscr.getmaxyx()
|
||||
current_time = datetime.now()
|
||||
|
||||
self._draw_main_clock(stdscr, height, width, current_time)
|
||||
self._draw_active_alarm_info(stdscr, height, width)
|
||||
self._draw_instructions(stdscr, height, width)
|
||||
|
||||
def handle_input(self, key):
|
||||
"""Handle user input and return the next view name or None to stay"""
|
||||
if not self.active_alarms:
|
||||
return 'CLOCK'
|
||||
|
||||
alarm_id = list(self.active_alarms.keys())[0]
|
||||
|
||||
if key == ord('s'):
|
||||
self._handle_snooze(alarm_id)
|
||||
return None
|
||||
elif key == ord('d'):
|
||||
self._handle_dismiss(alarm_id)
|
||||
return 'CLOCK'
|
||||
|
||||
return None
|
||||
|
||||
def update_active_alarms(self, active_alarms):
|
||||
"""Update the active alarms state"""
|
||||
self.active_alarms = active_alarms
|
||||
|
||||
def _draw_main_clock(self, stdscr, height, width, current_time):
|
||||
"""Draw the main clock display"""
|
||||
time_str = current_time.strftime("%H:%M:%S")
|
||||
digit_width = 14
|
||||
total_width = digit_width * len(time_str)
|
||||
start_x = (width - total_width) // 2
|
||||
start_y = (height - 7) // 2 - 4
|
||||
|
||||
# Draw blinking dot
|
||||
if int(current_time.timestamp()) % 2 == 0:
|
||||
stdscr.attron(curses.color_pair(1))
|
||||
stdscr.addstr(start_y - 1, start_x + total_width - 2, "•")
|
||||
stdscr.attroff(curses.color_pair(1))
|
||||
|
||||
# Draw big time digits
|
||||
stdscr.attron(curses.color_pair(1))
|
||||
stdscr.addstr(start_y - 1, start_x + total_width - 2, "•")
|
||||
for i, digit in enumerate(time_str):
|
||||
draw_big_digit(stdscr, start_y, start_x + i * digit_width, digit)
|
||||
stdscr.attroff(curses.color_pair(1))
|
||||
|
||||
# Green color for big time
|
||||
stdscr.attron(curses.color_pair(1))
|
||||
for i, digit in enumerate(time_str):
|
||||
draw_big_digit(stdscr, start_y, start_x + i * digit_width, digit)
|
||||
stdscr.attroff(curses.color_pair(1))
|
||||
# Draw date
|
||||
date_str = current_time.strftime("%Y-%m-%d")
|
||||
date_x = width // 2 - len(date_str) // 2
|
||||
date_y = height // 2 + 4
|
||||
stdscr.attron(curses.color_pair(2))
|
||||
stdscr.addstr(date_y, date_x, date_str)
|
||||
stdscr.attroff(curses.color_pair(2))
|
||||
|
||||
# Draw date (as in main clock)
|
||||
date_str = current_time.strftime("%Y-%m-%d")
|
||||
date_x = width // 2 - len(date_str) // 2
|
||||
date_y = height // 2 + 4
|
||||
stdscr.attron(curses.color_pair(2))
|
||||
stdscr.addstr(date_y, date_x, date_str)
|
||||
stdscr.attroff(curses.color_pair(2))
|
||||
def _draw_active_alarm_info(self, stdscr, height, width):
|
||||
"""Draw information about the active alarm"""
|
||||
if not self.active_alarms:
|
||||
return
|
||||
|
||||
# Get active alarm info
|
||||
active_alarms = context.get('active_alarms', {})
|
||||
if not active_alarms:
|
||||
return
|
||||
alarm_id = list(self.active_alarms.keys())[0]
|
||||
alarm_info = self.active_alarms[alarm_id]
|
||||
alarm_config = alarm_info['config']
|
||||
|
||||
# Get the first (or only) active alarm
|
||||
alarm_id = list(active_alarms.keys())[0]
|
||||
alarm_info = active_alarms[alarm_id]
|
||||
alarm_config = alarm_info['config']
|
||||
alarm_name = alarm_config.get('name', 'Unnamed Alarm')
|
||||
alarm_time = alarm_config.get('time', 'Unknown Time')
|
||||
alarm_str = f"[ {alarm_name} - {alarm_time} ]"
|
||||
|
||||
# Format alarm info
|
||||
alarm_name = alarm_config.get('name', 'Unnamed Alarm')
|
||||
alarm_time = alarm_config.get('time', 'Unknown Time')
|
||||
#snooze_count = alarm_info.get('snooze_count', 0)
|
||||
# Position alarm info above the date
|
||||
date_y = height // 2 + 4 # Same as in _draw_main_clock
|
||||
alarm_y = date_y - 2
|
||||
alarm_x = max(0, width // 2 - len(alarm_str) // 2)
|
||||
|
||||
# Draw alarm info under the clock
|
||||
info_y = start_y + 8 # Position below the clock
|
||||
#alarm_str = f"[ {alarm_name} - {alarm_time} - Snoozed: {snooze_count}x ]"
|
||||
alarm_str = f"[ {alarm_name} - {alarm_time} ]"
|
||||
alarm_x = max(0, width // 2 - len(alarm_str) // 2)
|
||||
alarm_y = date_y - 2 # Just above the date
|
||||
stdscr.attron(curses.color_pair(1))
|
||||
stdscr.addstr(alarm_y, alarm_x, alarm_str)
|
||||
stdscr.attroff(curses.color_pair(1))
|
||||
|
||||
# Center the alarm info
|
||||
info_x = max(0, width // 2 - len(alarm_str) // 2)
|
||||
def _draw_instructions(self, stdscr, height, width):
|
||||
"""Draw the user instructions"""
|
||||
if self.active_alarms:
|
||||
instructions = "S: Snooze D: Dismiss"
|
||||
stdscr.addstr(height - 2, width // 2 - len(instructions) // 2, instructions)
|
||||
|
||||
# Draw with green color
|
||||
stdscr.attron(curses.color_pair(1))
|
||||
stdscr.addstr(alarm_y, alarm_x, alarm_str)
|
||||
stdscr.attroff(curses.color_pair(1))
|
||||
def _handle_snooze(self, alarm_id):
|
||||
"""Handle snoozing the alarm"""
|
||||
self.control_queue.put({
|
||||
'type': 'snooze',
|
||||
'alarm_id': alarm_id
|
||||
})
|
||||
del self.active_alarms[alarm_id]
|
||||
|
||||
# Instructions
|
||||
stdscr.addstr(height - 2, width // 2 - 15, "S: Snooze D: Dismiss")
|
||||
def _handle_dismiss(self, alarm_id):
|
||||
"""Handle dismissing the alarm"""
|
||||
self.control_queue.put({
|
||||
'type': 'dismiss',
|
||||
'alarm_id': alarm_id
|
||||
})
|
||||
del self.active_alarms[alarm_id]
|
||||
|
@ -1,11 +1,45 @@
|
||||
import curses
|
||||
from datetime import datetime
|
||||
from .utils import init_colors
|
||||
from datetime import datetime, date, timedelta
|
||||
|
||||
def draw_add_alarm(stdscr, alarm_draft):
|
||||
"""Draw the add alarm screen"""
|
||||
if alarm_draft is None:
|
||||
alarm_draft = {
|
||||
class AddAlarmView:
|
||||
def __init__(self, storage, control_queue):
|
||||
self.storage = storage
|
||||
self.control_queue = control_queue
|
||||
self.weekday_names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||
self.reset_state()
|
||||
|
||||
def set_alarm_data(self, alarm):
|
||||
"""Load existing alarm data for editing."""
|
||||
self.alarm_data = alarm # Store for pre-filling the form
|
||||
|
||||
# Debug log to inspect the alarm structure
|
||||
import logging
|
||||
logging.getLogger(__name__).debug(f"set_alarm_data received: {alarm}")
|
||||
|
||||
# Ensure alarm is a dictionary
|
||||
if not isinstance(alarm, dict):
|
||||
logging.getLogger(__name__).error(f"Invalid alarm format: {type(alarm)}")
|
||||
|
||||
# Parse time string
|
||||
hour, minute, _ = map(int, alarm['time'].split(':')) # Extract hour and minute return
|
||||
|
||||
# Pre-fill the form fields with alarm data
|
||||
self.alarm_draft = {
|
||||
'hour': hour,
|
||||
'minute': minute,
|
||||
'name': alarm.get('name', 'Unnamed Alarm'),
|
||||
'enabled': alarm.get('enabled', True),
|
||||
'date': None, # Your structure doesn't use date directly
|
||||
'weekdays': alarm.get('repeat_rule', {}).get('days_of_week', []),
|
||||
'current_weekday': 0,
|
||||
'editing_name': False,
|
||||
'temp_name': alarm.get('name', 'Unnamed Alarm'),
|
||||
'selected_item': 0
|
||||
}
|
||||
|
||||
def reset_state(self):
|
||||
"""Reset all state variables to their initial values"""
|
||||
self.alarm_draft = {
|
||||
'hour': datetime.now().hour,
|
||||
'minute': datetime.now().minute,
|
||||
'name': 'New Alarm',
|
||||
@ -15,76 +49,119 @@ def draw_add_alarm(stdscr, alarm_draft):
|
||||
'current_weekday': 0,
|
||||
'editing_name': False,
|
||||
'temp_name': '',
|
||||
'selected_item': 0 # Added to handle selection
|
||||
'selected_item': 0
|
||||
}
|
||||
self.date_edit_pos = 2 # Default to editing the day
|
||||
|
||||
init_colors()
|
||||
height, width = stdscr.getmaxyx()
|
||||
def draw(self, stdscr):
|
||||
"""Draw the add alarm screen"""
|
||||
height, width = stdscr.getmaxyx()
|
||||
form_y = height // 2 - 8
|
||||
|
||||
# Center the form vertically with good spacing
|
||||
form_y = height // 2 - 8
|
||||
self._draw_title(stdscr, form_y, width)
|
||||
self._draw_time_field(stdscr, form_y + 2, width)
|
||||
self._draw_date_field(stdscr, form_y + 4, width)
|
||||
self._draw_weekdays(stdscr, form_y + 6, width)
|
||||
self._draw_name_field(stdscr, form_y + 8, width)
|
||||
self._draw_status_field(stdscr, form_y + 10, width)
|
||||
self._draw_instructions(stdscr, height - 2, width)
|
||||
|
||||
# Title
|
||||
title = "Add New Alarm"
|
||||
stdscr.attron(curses.color_pair(1) | curses.A_BOLD)
|
||||
stdscr.addstr(form_y, width // 2 - len(title) // 2, title)
|
||||
stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)
|
||||
def handle_input(self, key):
|
||||
"""Handle user input and return the next view name or None to stay"""
|
||||
if key == 27: # ESC
|
||||
return self._handle_escape()
|
||||
elif key == 10: # ENTER
|
||||
return self._handle_enter()
|
||||
|
||||
def draw_field(y, label, value, is_selected):
|
||||
if not self.alarm_draft['editing_name']:
|
||||
if key in [ord('h'), curses.KEY_LEFT]:
|
||||
self.alarm_draft['selected_item'] = (self.alarm_draft['selected_item'] - 1) % 6
|
||||
elif key in [ord('l'), curses.KEY_RIGHT]:
|
||||
self.alarm_draft['selected_item'] = (self.alarm_draft['selected_item'] + 1) % 6
|
||||
|
||||
self._handle_field_input(key)
|
||||
return None
|
||||
|
||||
def _handle_field_input(self, key):
|
||||
"""Handle input for the currently selected field"""
|
||||
selected_item = self.alarm_draft['selected_item']
|
||||
|
||||
if key in [ord('k'), curses.KEY_UP, ord('j'), curses.KEY_DOWN]:
|
||||
is_up = key in [ord('k'), curses.KEY_UP]
|
||||
if selected_item == 0:
|
||||
self._adjust_hour(is_up)
|
||||
elif selected_item == 1:
|
||||
self._adjust_minute(is_up)
|
||||
elif selected_item == 2:
|
||||
self._adjust_date(is_up)
|
||||
elif selected_item == 3:
|
||||
self._handle_weekday_input(key)
|
||||
elif selected_item == 4:
|
||||
self._handle_name_input(key)
|
||||
elif selected_item == 5 and key == 32:
|
||||
self.alarm_draft['enabled'] = not self.alarm_draft['enabled']
|
||||
|
||||
def _draw_title(self, stdscr, y, width):
|
||||
"""Draw the title of the form"""
|
||||
title = "Add New Alarm"
|
||||
stdscr.attron(curses.color_pair(1) | curses.A_BOLD)
|
||||
stdscr.addstr(y, width // 2 - len(title) // 2, title)
|
||||
stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)
|
||||
|
||||
def _draw_field(self, stdscr, y, label, value, is_selected):
|
||||
"""Draw a form field with label and value"""
|
||||
label_str = f"{label}: "
|
||||
x = width // 2 - (len(label_str) + len(str(value))) // 2
|
||||
x = self._center_text_x(label_str + str(value))
|
||||
|
||||
stdscr.attron(curses.color_pair(1))
|
||||
stdscr.addstr(y, x, label_str)
|
||||
stdscr.attroff(curses.color_pair(1))
|
||||
|
||||
# Draw value (highlighted if selected)
|
||||
if is_selected:
|
||||
stdscr.attron(curses.color_pair(2)) # Highlighted selection
|
||||
stdscr.attron(curses.color_pair(2))
|
||||
stdscr.addstr(y, x + len(label_str), str(value))
|
||||
if is_selected:
|
||||
stdscr.attroff(curses.color_pair(2))
|
||||
|
||||
# Get selected_item either from alarm_draft or passed separately
|
||||
selected_item = alarm_draft.get('selected_item', 0)
|
||||
def _draw_time_field(self, stdscr, y, width):
|
||||
"""Draw the time field"""
|
||||
self._draw_field(stdscr, y, "Time",
|
||||
f"{self.alarm_draft['hour']:02d}:{self.alarm_draft['minute']:02d}",
|
||||
self.alarm_draft['selected_item'] in [0, 1])
|
||||
|
||||
# Order: Time → Date → Weekdays → Name → Enabled
|
||||
draw_field(form_y + 2, "Time", f"{alarm_draft['hour']:02d}:{alarm_draft['minute']:02d}",
|
||||
selected_item == 0 or selected_item == 1)
|
||||
def _draw_date_field(self, stdscr, y, width):
|
||||
"""Draw the date field"""
|
||||
date_str = "Repeating weekly" if self.alarm_draft['weekdays'] else (
|
||||
self.alarm_draft['date'].strftime("%Y-%m-%d") if self.alarm_draft['date'] else "None"
|
||||
)
|
||||
self._draw_field(stdscr, y, "Date", date_str,
|
||||
self.alarm_draft['selected_item'] == 2)
|
||||
|
||||
# Date Selection
|
||||
date_y = form_y + 4
|
||||
if alarm_draft['weekdays']:
|
||||
draw_field(date_y, "Date", "Repeating weekly", selected_item == 2)
|
||||
else:
|
||||
draw_field(date_y, "Date", alarm_draft['date'].strftime("%Y-%m-%d") if alarm_draft['date'] else "None",
|
||||
selected_item == 2)
|
||||
def _draw_weekdays(self, stdscr, y, width):
|
||||
"""Draw the weekday selection field"""
|
||||
label_x = width // 2 - 20
|
||||
stdscr.attron(curses.color_pair(1))
|
||||
stdscr.addstr(y, label_x, "Repeat: ")
|
||||
stdscr.attroff(curses.color_pair(1))
|
||||
|
||||
# Weekday Selection
|
||||
weekday_y = form_y + 6
|
||||
weekday_label = "Repeat: "
|
||||
label_x = width // 2 - 20 # Adjust this value to move the entire "Repeat" section left or right
|
||||
weekday_x = label_x + len("Repeat: ")
|
||||
for i, day in enumerate(self.weekday_names):
|
||||
self._draw_weekday(stdscr, y, weekday_x + i * 4, day, i)
|
||||
|
||||
# Draw the "Repeat: " label once
|
||||
stdscr.attron(curses.color_pair(1))
|
||||
stdscr.addstr(weekday_y, label_x, weekday_label)
|
||||
stdscr.attroff(curses.color_pair(1))
|
||||
|
||||
# Draw weekdays starting right after the label
|
||||
weekday_x = label_x + len(weekday_label)
|
||||
weekday_names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||
for i, day in enumerate(weekday_names):
|
||||
x_pos = weekday_x + i * 4 # Space between weekdays
|
||||
is_selected = (selected_item == 3 and i == alarm_draft.get('current_weekday', 0))
|
||||
is_active = i in alarm_draft.get('weekdays', [])
|
||||
def _draw_weekday(self, stdscr, y, x, day, index):
|
||||
"""Draw a single weekday"""
|
||||
is_selected = (self.alarm_draft['selected_item'] == 3 and
|
||||
index == self.alarm_draft['current_weekday'])
|
||||
is_active = index in self.alarm_draft['weekdays']
|
||||
|
||||
if is_selected:
|
||||
stdscr.attron(curses.color_pair(2)) # Highlight current selection
|
||||
stdscr.attron(curses.color_pair(2))
|
||||
elif is_active:
|
||||
stdscr.attron(curses.color_pair(1) | curses.A_BOLD) # Selected weekday
|
||||
stdscr.attron(curses.color_pair(1) | curses.A_BOLD)
|
||||
else:
|
||||
stdscr.attron(curses.color_pair(1))
|
||||
|
||||
stdscr.addstr(weekday_y, x_pos, day)
|
||||
stdscr.addstr(y, x, day)
|
||||
|
||||
if is_selected:
|
||||
stdscr.attroff(curses.color_pair(2))
|
||||
@ -93,16 +170,129 @@ def draw_add_alarm(stdscr, alarm_draft):
|
||||
else:
|
||||
stdscr.attroff(curses.color_pair(1))
|
||||
|
||||
# Name field
|
||||
draw_field(form_y + 8, "Name", alarm_draft['name'], selected_item == 4)
|
||||
def _draw_name_field(self, stdscr, y, width):
|
||||
"""Draw the name field"""
|
||||
self._draw_field(stdscr, y, "Name", self.alarm_draft['name'],
|
||||
self.alarm_draft['selected_item'] == 4)
|
||||
|
||||
# Enabled Toggle
|
||||
status_y = form_y + 10
|
||||
enabled_str = "● Enabled" if alarm_draft['enabled'] else "○ Disabled"
|
||||
draw_field(status_y, "Status", enabled_str, selected_item == 5)
|
||||
def _draw_status_field(self, stdscr, y, width):
|
||||
"""Draw the enabled/disabled status field"""
|
||||
enabled_str = "● Enabled" if self.alarm_draft['enabled'] else "○ Disabled"
|
||||
self._draw_field(stdscr, y, "Status", enabled_str,
|
||||
self.alarm_draft['selected_item'] == 5)
|
||||
|
||||
# Instructions
|
||||
instructions = "j/k: Change h/l: Move Space: Toggle Enter: Save Esc: Cancel"
|
||||
stdscr.attron(curses.color_pair(1))
|
||||
stdscr.addstr(height - 2, width // 2 - len(instructions) // 2, instructions)
|
||||
stdscr.attroff(curses.color_pair(1))
|
||||
def _draw_instructions(self, stdscr, y, width):
|
||||
"""Draw the instructions at the bottom of the screen"""
|
||||
instructions = "j/k: Change h/l: Move Space: Toggle Enter: Save Esc: Cancel"
|
||||
stdscr.attron(curses.color_pair(1))
|
||||
stdscr.addstr(y, width // 2 - len(instructions) // 2, instructions)
|
||||
stdscr.attroff(curses.color_pair(1))
|
||||
|
||||
def _center_text_x(self, text):
|
||||
"""Calculate x position to center text"""
|
||||
return curses.COLS // 2 - len(text) // 2
|
||||
|
||||
def _adjust_hour(self, increase):
|
||||
"""Adjust the hour value"""
|
||||
delta = 1 if increase else -1
|
||||
self.alarm_draft['hour'] = (self.alarm_draft['hour'] + delta) % 24
|
||||
|
||||
def _adjust_minute(self, increase):
|
||||
"""Adjust the minute value"""
|
||||
delta = 1 if increase else -1
|
||||
self.alarm_draft['minute'] = (self.alarm_draft['minute'] + delta) % 60
|
||||
|
||||
def _adjust_date(self, increase):
|
||||
"""Adjust the date value"""
|
||||
if not self.alarm_draft['date']:
|
||||
self.alarm_draft['date'] = datetime.now().date()
|
||||
return
|
||||
|
||||
delta = 1 if increase else -1
|
||||
current_date = self.alarm_draft['date']
|
||||
|
||||
try:
|
||||
if self.date_edit_pos == 0: # Year
|
||||
new_year = max(current_date.year + delta, datetime.now().year)
|
||||
self.alarm_draft['date'] = current_date.replace(year=new_year)
|
||||
elif self.date_edit_pos == 1: # Month
|
||||
new_month = max(1, min(12, current_date.month + delta))
|
||||
max_day = (datetime(current_date.year, new_month, 1) +
|
||||
timedelta(days=31)).replace(day=1) - timedelta(days=1)
|
||||
self.alarm_draft['date'] = current_date.replace(
|
||||
month=new_month,
|
||||
day=min(current_date.day, max_day.day)
|
||||
)
|
||||
elif self.date_edit_pos == 2: # Day
|
||||
max_day = (datetime(current_date.year, current_date.month, 1) +
|
||||
timedelta(days=31)).replace(day=1) - timedelta(days=1)
|
||||
new_day = max(1, min(max_day.day, current_date.day + delta))
|
||||
self.alarm_draft['date'] = current_date.replace(day=new_day)
|
||||
except ValueError as e:
|
||||
# Handle date validation errors
|
||||
pass
|
||||
|
||||
def _handle_weekday_input(self, key):
|
||||
"""Handle input for weekday selection"""
|
||||
if key in [ord('h'), curses.KEY_LEFT]:
|
||||
self.alarm_draft['current_weekday'] = (self.alarm_draft['current_weekday'] - 1) % 7
|
||||
elif key in [ord('l'), curses.KEY_RIGHT]:
|
||||
self.alarm_draft['current_weekday'] = (self.alarm_draft['current_weekday'] + 1) % 7
|
||||
elif key == 32: # SPACE
|
||||
current_day = self.alarm_draft['current_weekday']
|
||||
if current_day in self.alarm_draft['weekdays']:
|
||||
self.alarm_draft['weekdays'].remove(current_day)
|
||||
else:
|
||||
self.alarm_draft['weekdays'].append(current_day)
|
||||
self.alarm_draft['weekdays'].sort()
|
||||
|
||||
if self.alarm_draft['weekdays']:
|
||||
self.alarm_draft['date'] = None
|
||||
|
||||
def _handle_name_input(self, key):
|
||||
"""Handle input for name editing"""
|
||||
if key == 32: # SPACE
|
||||
if not self.alarm_draft['editing_name']:
|
||||
self.alarm_draft['editing_name'] = True
|
||||
self.alarm_draft['temp_name'] = self.alarm_draft['name']
|
||||
self.alarm_draft['name'] = ''
|
||||
elif self.alarm_draft['editing_name']:
|
||||
if key == curses.KEY_BACKSPACE or key == 127:
|
||||
self.alarm_draft['name'] = self.alarm_draft['name'][:-1]
|
||||
elif 32 <= key <= 126: # Printable ASCII
|
||||
self.alarm_draft['name'] += chr(key)
|
||||
|
||||
def _handle_escape(self):
|
||||
"""Handle escape key press"""
|
||||
if self.alarm_draft['editing_name']:
|
||||
self.alarm_draft['name'] = self.alarm_draft['temp_name']
|
||||
self.alarm_draft['editing_name'] = False
|
||||
return None
|
||||
return 'CLOCK'
|
||||
|
||||
def _handle_enter(self):
|
||||
"""Handle enter key press"""
|
||||
if self.alarm_draft['editing_name']:
|
||||
self.alarm_draft['editing_name'] = False
|
||||
self.alarm_draft['selected_item'] = 0
|
||||
return None
|
||||
|
||||
try:
|
||||
alarm_data = {
|
||||
"name": self.alarm_draft['name'],
|
||||
"time": f"{self.alarm_draft['hour']:02d}:{self.alarm_draft['minute']:02d}:00",
|
||||
"enabled": self.alarm_draft['enabled'],
|
||||
"repeat_rule": {
|
||||
"type": "weekly" if self.alarm_draft['weekdays'] else "once",
|
||||
"days_of_week": [self.weekday_names[day].lower()
|
||||
for day in self.alarm_draft['weekdays']],
|
||||
"at": (self.alarm_draft['date'].strftime("%Y-%m-%d")
|
||||
if self.alarm_draft['date'] and not self.alarm_draft['weekdays']
|
||||
else None)
|
||||
}
|
||||
}
|
||||
self.storage.save_new_alert(alarm_data)
|
||||
return 'CLOCK'
|
||||
except Exception as e:
|
||||
# Handle save errors
|
||||
return None
|
||||
|
@ -98,5 +98,13 @@ BIG_DIGITS = {
|
||||
" ████ ",
|
||||
" ████ ",
|
||||
" "
|
||||
],
|
||||
'?': [
|
||||
" ??? ",
|
||||
" ????? ",
|
||||
"?? ??",
|
||||
" ??? ",
|
||||
" ?? ?? ",
|
||||
" ? "
|
||||
]
|
||||
}
|
||||
|
@ -1,202 +0,0 @@
|
||||
import curses
|
||||
from datetime import datetime, date, timedelta
|
||||
|
||||
class InputHandling:
|
||||
|
||||
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'
|
||||
|
||||
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):
|
||||
"""Handle input for alarm creation"""
|
||||
if not hasattr(self, 'alarm_draft'):
|
||||
self.alarm_draft = {
|
||||
'hour': datetime.now().hour,
|
||||
'minute': datetime.now().minute,
|
||||
'name': 'New Alarm',
|
||||
'enabled': True,
|
||||
'date': None,
|
||||
'weekdays': [],
|
||||
'current_weekday': 0, # Add this to track currently selected weekday
|
||||
'editing_name': False,
|
||||
'temp_name': ''
|
||||
}
|
||||
|
||||
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": alarm['name'],
|
||||
"time": f"{alarm['hour']:02d}:{alarm['minute']:02d}:00",
|
||||
"enabled": alarm['enabled'],
|
||||
"repeat_rule": {
|
||||
"type": "weekly" if alarm['weekdays'] else "once",
|
||||
"days_of_week": [self.weekday_names[day].lower() for day in alarm['weekdays']],
|
||||
"at": alarm['date'].strftime("%Y-%m-%d") if alarm['date'] and not alarm['weekdays'] else None
|
||||
}
|
||||
}
|
||||
self.storage.save_new_alert(alarm_data)
|
||||
self.current_view = 'CLOCK'
|
||||
except Exception as e:
|
||||
self._show_error(str(e))
|
||||
return
|
||||
|
||||
# Move selection using Left (h) and Right (l)
|
||||
if not alarm['editing_name']:
|
||||
if key in [ord('h'), curses.KEY_LEFT]:
|
||||
self.selected_item = (self.selected_item - 1) % 6
|
||||
elif key in [ord('l'), curses.KEY_RIGHT]:
|
||||
self.selected_item = (self.selected_item + 1) % 6
|
||||
|
||||
# Handle numeric values (Hour, Minute, Date)
|
||||
if key in [ord('k'), curses.KEY_UP, ord('j'), curses.KEY_DOWN]:
|
||||
is_up = key in [ord('k'), curses.KEY_UP]
|
||||
delta = 1 if is_up else -1
|
||||
|
||||
if self.selected_item == 0: # Hour
|
||||
alarm['hour'] = (alarm['hour'] + delta) % 24
|
||||
elif self.selected_item == 1: # Minute
|
||||
alarm['minute'] = (alarm['minute'] + delta) % 60
|
||||
elif self.selected_item == 2: # Date
|
||||
if not alarm['date']:
|
||||
alarm['date'] = datetime.now().date()
|
||||
else:
|
||||
try:
|
||||
if not hasattr(self, 'date_edit_pos'):
|
||||
self.date_edit_pos = 2 # Default to editing the day
|
||||
current_date = alarm['date']
|
||||
if self.date_edit_pos == 0: # Year
|
||||
alarm['date'] = current_date.replace(year=max(current_date.year + delta, datetime.now().year))
|
||||
elif self.date_edit_pos == 1: # Month
|
||||
new_month = max(1, min(12, current_date.month + delta))
|
||||
max_day = (datetime(current_date.year, new_month, 1) + timedelta(days=31)).replace(day=1) - timedelta(days=1)
|
||||
alarm['date'] = current_date.replace(month=new_month, day=min(current_date.day, max_day.day))
|
||||
elif self.date_edit_pos == 2: # Day
|
||||
max_day = (datetime(current_date.year, current_date.month, 1) + timedelta(days=31)).replace(day=1) - timedelta(days=1)
|
||||
alarm['date'] = current_date.replace(day=max(1, min(max_day.day, current_date.day + delta)))
|
||||
except ValueError as e:
|
||||
self._show_error(str(e))
|
||||
|
||||
# Weekday Selection
|
||||
elif self.selected_item == 3: # When weekdays are selected
|
||||
if key in [ord('h'), curses.KEY_LEFT]:
|
||||
# Move selection left
|
||||
alarm['current_weekday'] = (alarm['current_weekday'] - 1) % 7
|
||||
elif key in [ord('l'), curses.KEY_RIGHT]:
|
||||
# Move selection right
|
||||
alarm['current_weekday'] = (alarm['current_weekday'] + 1) % 7
|
||||
elif key == 32: # SPACE to toggle selection
|
||||
current_day = alarm['current_weekday']
|
||||
if current_day in alarm['weekdays']:
|
||||
alarm['weekdays'].remove(current_day)
|
||||
else:
|
||||
alarm['weekdays'].append(current_day)
|
||||
alarm['weekdays'].sort()
|
||||
|
||||
# Clear date if weekdays are selected
|
||||
if alarm['weekdays']:
|
||||
alarm['date'] = None
|
||||
|
||||
# Name Editing
|
||||
elif self.selected_item == 4:
|
||||
if key == 32: # SPACE to start editing
|
||||
if not alarm['editing_name']:
|
||||
alarm['editing_name'] = True
|
||||
alarm['temp_name'] = alarm['name']
|
||||
alarm['name'] = ''
|
||||
elif 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)
|
||||
|
||||
# Enabled/Disabled Toggle
|
||||
elif self.selected_item == 5 and key == 32:
|
||||
alarm['enabled'] = not alarm['enabled']
|
||||
|
||||
def _handle_list_alarms_input(self, key):
|
||||
"""Handle input for alarm list view"""
|
||||
total_items = len(self.alarm_list) + 1 # +1 for "Add new alarm" option
|
||||
|
||||
if key == 27: # ESC
|
||||
self.current_view = 'CLOCK'
|
||||
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
|
||||
elif key == ord('d'):
|
||||
# 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:
|
||||
try:
|
||||
alarm_to_delete = self.alarm_list[self.selected_item]
|
||||
self.storage.remove_saved_alert(alarm_to_delete['id'])
|
||||
self.alarm_list = self.storage.get_saved_alerts()
|
||||
# Adjust selected item if needed
|
||||
if self.selected_item >= len(self.alarm_list):
|
||||
self.selected_item = len(self.alarm_list)
|
||||
except Exception as e:
|
||||
self._show_error(f"Failed to delete alarm: {e}")
|
||||
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")
|
@ -1,74 +1,128 @@
|
||||
import curses
|
||||
from datetime import datetime
|
||||
from .utils import init_colors, draw_big_digit
|
||||
|
||||
def draw_list_alarms(stdscr, context):
|
||||
"""Draw the list of alarms screen"""
|
||||
init_colors()
|
||||
height, width = stdscr.getmaxyx()
|
||||
class ListAlarmsView:
|
||||
def __init__(self, storage):
|
||||
"""Initialize the list alarms view"""
|
||||
self.storage = storage
|
||||
self.weekday_names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||
self.selected_index = 0
|
||||
self.alarms = []
|
||||
|
||||
# Get required data from context
|
||||
alarms = context.get('alarms', [])
|
||||
selected_index = context.get('selected_index', 0)
|
||||
weekday_names = context.get('weekday_names', ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'])
|
||||
def reset_state(self):
|
||||
"""Reset the view state"""
|
||||
self.selected_index = 0
|
||||
self.alarms = self.storage.get_saved_alerts()
|
||||
|
||||
# Calculate visible range for scrolling
|
||||
max_visible_items = height - 8 # Leave space for header and footer
|
||||
total_items = len(alarms) + 1 # +1 for "Add new alarm" option
|
||||
def update_alarms(self, alarms):
|
||||
"""Update the list of alarms to display."""
|
||||
self.alarms = alarms
|
||||
self.selected_index = 0
|
||||
|
||||
# Calculate scroll position
|
||||
start_idx = max(0, min(selected_index - max_visible_items // 2,
|
||||
total_items - max_visible_items))
|
||||
if start_idx < 0:
|
||||
start_idx = 0
|
||||
end_idx = min(start_idx + max_visible_items, total_items)
|
||||
def get_selected_alarm(self):
|
||||
"""Get the currently selected alarm."""
|
||||
if 0 <= self.selected_index < len(self.alarms):
|
||||
return self.alarms[self.selected_index]
|
||||
return None
|
||||
|
||||
# Header
|
||||
header_text = "Alarms"
|
||||
stdscr.attron(curses.color_pair(1) | curses.A_BOLD)
|
||||
stdscr.addstr(2, width // 2 - len(header_text) // 2, header_text)
|
||||
stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)
|
||||
def draw(self, stdscr):
|
||||
"""Draw the list of alarms screen"""
|
||||
height, width = stdscr.getmaxyx()
|
||||
|
||||
# Draw alarms
|
||||
for i in range(start_idx, end_idx):
|
||||
y_pos = 4 + (i - start_idx)
|
||||
self._draw_header(stdscr, width)
|
||||
visible_range = self._calculate_visible_range(height)
|
||||
self._draw_alarm_list(stdscr, height, width, visible_range)
|
||||
self._draw_instructions(stdscr, height, width)
|
||||
|
||||
if i == len(alarms): # "Add new alarm" option
|
||||
display_str = "Add new alarm..."
|
||||
else:
|
||||
alarm = alarms[i]
|
||||
# Format time
|
||||
time_str = alarm.get('time', 'Unknown')
|
||||
def handle_input(self, key):
|
||||
"""Handle user input and return the next view name or None to stay"""
|
||||
total_items = len(self.alarms) + 1 # +1 for "Add new alarm" option
|
||||
|
||||
# Format repeat info
|
||||
repeat_info = ""
|
||||
repeat_rule = alarm.get('repeat_rule', {})
|
||||
if repeat_rule:
|
||||
if repeat_rule.get('type') == 'weekly':
|
||||
days = repeat_rule.get('days', [])
|
||||
repeat_info = f" (Every {', '.join(weekday_names[d] for d in days)})"
|
||||
elif repeat_rule.get('type') == 'once' and repeat_rule.get('date'):
|
||||
repeat_info = f" (On {repeat_rule['date']})"
|
||||
if key == 27: # ESC
|
||||
return 'CLOCK'
|
||||
elif key in [ord('j'), curses.KEY_DOWN]:
|
||||
self.selected_index = (self.selected_index + 1) % total_items
|
||||
elif key in [ord('k'), curses.KEY_UP]:
|
||||
self.selected_index = (self.selected_index - 1) % total_items
|
||||
elif key == ord('d'):
|
||||
return self._handle_delete()
|
||||
elif key in [ord('a'), 10]: # 'a' or Enter
|
||||
return self._handle_add_edit()
|
||||
|
||||
# Status indicator (in green)
|
||||
status = "✓" if alarm.get('enabled', True) else "✗"
|
||||
display_str = f"{status} {time_str} {alarm.get('name', 'Unnamed')}{repeat_info}"
|
||||
return None
|
||||
|
||||
# Truncate if too long (leaving space for selection brackets)
|
||||
max_length = width - 6
|
||||
if len(display_str) > max_length:
|
||||
display_str = display_str[:max_length-3] + "..."
|
||||
def _calculate_visible_range(self, height):
|
||||
"""Calculate the visible range for scrolling"""
|
||||
max_visible_items = height - 8 # Space for header and footer
|
||||
total_items = len(self.alarms) + 1 # +1 for "Add new alarm" option
|
||||
|
||||
# Center the item
|
||||
x_pos = width // 2 - len(display_str) // 2
|
||||
# Calculate scroll position
|
||||
start_idx = max(0, min(self.selected_index - max_visible_items // 2,
|
||||
total_items - max_visible_items))
|
||||
if start_idx < 0:
|
||||
start_idx = 0
|
||||
end_idx = min(start_idx + max_visible_items, total_items)
|
||||
|
||||
# Highlight selected item
|
||||
if i == selected_index:
|
||||
return (start_idx, end_idx)
|
||||
|
||||
def _draw_header(self, stdscr, width):
|
||||
"""Draw the header text"""
|
||||
header_text = "Alarms"
|
||||
stdscr.attron(curses.color_pair(1) | curses.A_BOLD)
|
||||
stdscr.addstr(2, self._center_x(width, header_text), header_text)
|
||||
stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)
|
||||
|
||||
def _draw_alarm_list(self, stdscr, height, width, visible_range):
|
||||
"""Draw the list of alarms"""
|
||||
start_idx, end_idx = visible_range
|
||||
|
||||
for i in range(start_idx, end_idx):
|
||||
y_pos = 4 + (i - start_idx)
|
||||
display_str = self._format_alarm_display(i)
|
||||
|
||||
# Truncate if too long
|
||||
max_length = width - 6
|
||||
if len(display_str) > max_length:
|
||||
display_str = display_str[:max_length-3] + "..."
|
||||
|
||||
x_pos = self._center_x(width, display_str)
|
||||
self._draw_alarm_item(stdscr, y_pos, x_pos, display_str, i == self.selected_index)
|
||||
|
||||
def _format_alarm_display(self, index):
|
||||
"""Format the display string for an alarm"""
|
||||
if index == len(self.alarms):
|
||||
return "Add new alarm..."
|
||||
|
||||
alarm = self.alarms[index]
|
||||
time_str = alarm.get('time', 'Unknown')
|
||||
repeat_info = self._format_repeat_info(alarm)
|
||||
status = "✓" if alarm.get('enabled', True) else "✗"
|
||||
|
||||
return f"{status} {time_str} {alarm.get('name', 'Unnamed')}{repeat_info}"
|
||||
|
||||
def _format_repeat_info(self, alarm):
|
||||
"""Format the repeat information for an alarm"""
|
||||
repeat_rule = alarm.get('repeat_rule', {})
|
||||
if not repeat_rule:
|
||||
return ""
|
||||
|
||||
if repeat_rule.get('type') == 'weekly':
|
||||
days = repeat_rule.get('days', [])
|
||||
return f" (Every {', '.join(self.weekday_names[d] for d in days)})"
|
||||
elif repeat_rule.get('type') == 'once' and repeat_rule.get('date'):
|
||||
return f" (On {repeat_rule['date']})"
|
||||
|
||||
return ""
|
||||
|
||||
def _draw_alarm_item(self, stdscr, y_pos, x_pos, display_str, is_selected):
|
||||
"""Draw a single alarm item"""
|
||||
if is_selected:
|
||||
# Draw selection brackets in green
|
||||
stdscr.attron(curses.color_pair(1))
|
||||
stdscr.addstr(y_pos, x_pos - 2, "[ ")
|
||||
stdscr.addstr(y_pos, x_pos + len(display_str), " ]")
|
||||
stdscr.attroff(curses.color_pair(1))
|
||||
|
||||
# Draw text in yellow (highlighted)
|
||||
stdscr.attron(curses.color_pair(2))
|
||||
stdscr.addstr(y_pos, x_pos, display_str)
|
||||
@ -79,8 +133,38 @@ def draw_list_alarms(stdscr, context):
|
||||
stdscr.addstr(y_pos, x_pos, display_str)
|
||||
stdscr.attroff(curses.color_pair(1))
|
||||
|
||||
# Instructions
|
||||
instructions = "j/k: Move d: Delete a: Add/Edit Esc: Back"
|
||||
stdscr.attron(curses.color_pair(1))
|
||||
stdscr.addstr(height - 2, width // 2 - len(instructions) // 2, instructions)
|
||||
stdscr.attroff(curses.color_pair(1))
|
||||
def _draw_instructions(self, stdscr, height, width):
|
||||
"""Draw the instructions at the bottom of the screen"""
|
||||
instructions = "j/k: Move d: Delete a: Add/Edit Esc: Back"
|
||||
stdscr.attron(curses.color_pair(1))
|
||||
stdscr.addstr(height - 2, self._center_x(width, instructions), instructions)
|
||||
stdscr.attroff(curses.color_pair(1))
|
||||
|
||||
def _center_x(self, width, text):
|
||||
"""Calculate x coordinate to center text"""
|
||||
return width // 2 - len(text) // 2
|
||||
|
||||
def _handle_delete(self):
|
||||
"""Handle alarm deletion"""
|
||||
if self.selected_index < len(self.alarms):
|
||||
try:
|
||||
alarm_to_delete = self.alarms[self.selected_index]
|
||||
self.storage.remove_saved_alert(alarm_to_delete['id'])
|
||||
self.alarms = self.storage.get_saved_alerts()
|
||||
# Adjust selected item if needed
|
||||
if self.selected_index >= len(self.alarms):
|
||||
self.selected_index = len(self.alarms)
|
||||
except Exception as e:
|
||||
# You might want to add error handling here
|
||||
pass
|
||||
return None
|
||||
|
||||
def _handle_add_edit(self):
|
||||
"""Handle add/edit action"""
|
||||
if self.selected_index == len(self.alarms):
|
||||
# "Add new alarm" option selected
|
||||
return 'ADD_ALARM'
|
||||
else:
|
||||
# Edit existing alarm
|
||||
selected_alarm = self.alarms[self.selected_index]
|
||||
return ('ADD_ALARM', selected_alarm) # Pass alarm to AddAlarmView
|
||||
|
@ -2,40 +2,70 @@ import curses
|
||||
from datetime import datetime
|
||||
from .utils import init_colors, draw_big_digit
|
||||
from .big_digits import BIG_DIGITS
|
||||
from .add_alarm import draw_add_alarm
|
||||
from .active_alarm import draw_active_alarms
|
||||
from .list_alarms import draw_list_alarms
|
||||
|
||||
class MainClockView:
|
||||
def __init__(self):
|
||||
"""Initialize the main clock view"""
|
||||
self.digit_width = 14 # Width of each digit pattern in the big clock display
|
||||
|
||||
def draw(self, stdscr):
|
||||
"""Draw the main clock screen"""
|
||||
init_colors()
|
||||
height, width = stdscr.getmaxyx()
|
||||
current_time = datetime.now()
|
||||
|
||||
self._draw_big_time(stdscr, current_time, height, width)
|
||||
self._draw_date(stdscr, current_time, height, width)
|
||||
self._draw_menu(stdscr, height, width)
|
||||
|
||||
def draw_main_clock(stdscr, context=None):
|
||||
"""Draw the main clock screen"""
|
||||
init_colors()
|
||||
height, width = stdscr.getmaxyx()
|
||||
current_time = datetime.now()
|
||||
def handle_input(self, key):
|
||||
"""Handle user input and return the next view name or None to stay"""
|
||||
if key == ord('a'):
|
||||
return 'ADD_ALARM'
|
||||
elif key == ord('s'):
|
||||
return 'LIST_ALARMS'
|
||||
elif key == ord('q'):
|
||||
return 'QUIT'
|
||||
return None
|
||||
|
||||
# Big time display
|
||||
time_str = current_time.strftime("%H:%M:%S")
|
||||
digit_width = 14 # Width of each digit pattern
|
||||
total_width = digit_width * len(time_str)
|
||||
start_x = (width - total_width) // 2
|
||||
start_y = (height - 7) // 2 - 4
|
||||
def _draw_big_time(self, stdscr, current_time, height, width):
|
||||
"""Draw the big time display"""
|
||||
time_str = current_time.strftime("%H:%M:%S")
|
||||
total_width = self.digit_width * len(time_str)
|
||||
start_x = self._center_x(width, total_width)
|
||||
start_y = self._center_y(height, 7) - 4 # 7 is the height of digits
|
||||
|
||||
# Green color for big time
|
||||
stdscr.attron(curses.color_pair(1))
|
||||
for i, digit in enumerate(time_str):
|
||||
draw_big_digit(stdscr, start_y, start_x + i * digit_width, digit)
|
||||
stdscr.attroff(curses.color_pair(1))
|
||||
# Draw each digit in green
|
||||
stdscr.attron(curses.color_pair(1))
|
||||
for i, digit in enumerate(time_str):
|
||||
self._draw_digit(stdscr, start_y, start_x + i * self.digit_width, digit)
|
||||
stdscr.attroff(curses.color_pair(1))
|
||||
|
||||
# Date display
|
||||
date_str = current_time.strftime("%Y-%m-%d")
|
||||
date_x = width // 2 - len(date_str) // 2
|
||||
date_y = height // 2 + 4
|
||||
def _draw_date(self, stdscr, current_time, height, width):
|
||||
"""Draw the current date"""
|
||||
date_str = current_time.strftime("%Y-%m-%d")
|
||||
date_x = self._center_x(width, len(date_str))
|
||||
date_y = height // 2 + 4
|
||||
|
||||
stdscr.attron(curses.color_pair(2))
|
||||
stdscr.addstr(date_y, date_x, date_str)
|
||||
stdscr.attroff(curses.color_pair(2))
|
||||
stdscr.attron(curses.color_pair(2))
|
||||
stdscr.addstr(date_y, date_x, date_str)
|
||||
stdscr.attroff(curses.color_pair(2))
|
||||
|
||||
# Menu options
|
||||
menu_str = "A: Add Alarm S: List Alarms Q: Quit"
|
||||
menu_x = width // 2 - len(menu_str) // 2
|
||||
stdscr.addstr(height - 2, menu_x, menu_str)
|
||||
def _draw_menu(self, stdscr, height, width):
|
||||
"""Draw the menu options"""
|
||||
menu_str = "A: Add Alarm S: List Alarms Q: Quit"
|
||||
menu_x = self._center_x(width, len(menu_str))
|
||||
stdscr.addstr(height - 2, menu_x, menu_str)
|
||||
|
||||
def _draw_digit(self, stdscr, y, x, digit):
|
||||
"""Draw a single big digit"""
|
||||
# Delegate to the existing draw_big_digit utility function
|
||||
draw_big_digit(stdscr, y, x, digit)
|
||||
|
||||
def _center_x(self, width, text_width):
|
||||
"""Calculate x coordinate to center text horizontally"""
|
||||
return (width - text_width) // 2
|
||||
|
||||
def _center_y(self, height, text_height):
|
||||
"""Calculate y coordinate to center text vertically"""
|
||||
return (height - text_height) // 2
|
||||
|
@ -1,75 +1,57 @@
|
||||
import curses
|
||||
import time
|
||||
from datetime import datetime, date, timedelta
|
||||
import threading
|
||||
import logging
|
||||
import queue
|
||||
from datetime import datetime
|
||||
|
||||
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
|
||||
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(InputHandling):
|
||||
class UI:
|
||||
def __init__(self, alarm_system_manager, control_queue):
|
||||
# UI State Management
|
||||
"""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
|
||||
|
||||
# Control queue for interacting with AlarmSiren
|
||||
self.control_queue = control_queue
|
||||
|
||||
# Logging
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# Active alarm tracking
|
||||
self.active_alarms = {}
|
||||
# Initialize views
|
||||
self._init_views()
|
||||
|
||||
# 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 _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 a thread to monitor control queue
|
||||
monitor_thread = threading.Thread(target=self._monitor_control_queue, daemon=True)
|
||||
# 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}")
|
||||
@ -88,15 +70,16 @@ class UI(InputHandling):
|
||||
try:
|
||||
control_msg = self.control_queue.get(timeout=1)
|
||||
|
||||
# Handle different types of control messages
|
||||
if control_msg['type'] == 'trigger':
|
||||
# Store triggered alarm
|
||||
# Update active alarms view
|
||||
active_view = self.views['ACTIVE_ALARMS']
|
||||
alarm_id = control_msg['alarm_id']
|
||||
self.active_alarms[alarm_id] = control_msg['info']
|
||||
active_view.update_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'
|
||||
# Switch to active alarms view
|
||||
self.current_view = 'ACTIVE_ALARMS'
|
||||
|
||||
except queue.Empty:
|
||||
pass
|
||||
@ -106,81 +89,91 @@ class UI(InputHandling):
|
||||
self.logger.error(f"Error monitoring control queue: {e}")
|
||||
time.sleep(1)
|
||||
|
||||
def _show_error(self, message, duration=30):
|
||||
def _show_error(self, message, duration=3):
|
||||
"""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
|
||||
|
||||
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()
|
||||
|
@ -1,34 +1,138 @@
|
||||
import curses
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from .big_digits import BIG_DIGITS
|
||||
|
||||
@dataclass
|
||||
class ColorScheme:
|
||||
"""Color scheme configuration for the UI"""
|
||||
PRIMARY = 1 # Green on black
|
||||
HIGHLIGHT = 2 # Yellow on black
|
||||
ERROR = 3 # Red on black
|
||||
|
||||
class ViewUtils:
|
||||
"""Common utility functions for view classes"""
|
||||
@staticmethod
|
||||
def center_x(width: int, text_width: int) -> int:
|
||||
"""Calculate x coordinate to center text horizontally"""
|
||||
return max(0, (width - text_width) // 2)
|
||||
|
||||
@staticmethod
|
||||
def center_y(height: int, text_height: int) -> int:
|
||||
"""Calculate y coordinate to center text vertically"""
|
||||
return max(0, (height - text_height) // 2)
|
||||
|
||||
@staticmethod
|
||||
def draw_centered_text(stdscr, y: int, text: str, color_pair: Optional[int] = None, attrs: int = 0):
|
||||
"""Draw text centered horizontally on the screen with optional color and attributes"""
|
||||
height, width = stdscr.getmaxyx()
|
||||
x = ViewUtils.center_x(width, len(text))
|
||||
|
||||
if color_pair is not None:
|
||||
stdscr.attron(curses.color_pair(color_pair) | attrs)
|
||||
|
||||
try:
|
||||
stdscr.addstr(y, x, text)
|
||||
except curses.error:
|
||||
pass # Ignore errors from writing at invalid positions
|
||||
|
||||
if color_pair is not None:
|
||||
stdscr.attroff(curses.color_pair(color_pair) | attrs)
|
||||
|
||||
def init_colors():
|
||||
"""Initialize color pairs matching specification"""
|
||||
curses.start_color()
|
||||
curses.use_default_colors()
|
||||
# Green text on black background (primary color)
|
||||
curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK)
|
||||
# Highlight color (yellow)
|
||||
curses.init_pair(2, curses.COLOR_YELLOW, curses.COLOR_BLACK)
|
||||
# Error color (red)
|
||||
curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
|
||||
"""Initialize color pairs for the application"""
|
||||
try:
|
||||
curses.start_color()
|
||||
curses.use_default_colors()
|
||||
|
||||
# Primary color (green text on black background)
|
||||
curses.init_pair(ColorScheme.PRIMARY, curses.COLOR_GREEN, curses.COLOR_BLACK)
|
||||
|
||||
# Highlight color (yellow text on black background)
|
||||
curses.init_pair(ColorScheme.HIGHLIGHT, curses.COLOR_YELLOW, curses.COLOR_BLACK)
|
||||
|
||||
# Error color (red text on black background)
|
||||
curses.init_pair(ColorScheme.ERROR, curses.COLOR_RED, curses.COLOR_BLACK)
|
||||
except Exception as e:
|
||||
# Log error or handle gracefully if color initialization fails
|
||||
pass
|
||||
|
||||
def draw_error(stdscr, error_message):
|
||||
"""Draw error message following specification"""
|
||||
def draw_error(stdscr, error_message: str, duration_sec: int = 3):
|
||||
"""
|
||||
Draw error message at the bottom of the screen
|
||||
|
||||
Args:
|
||||
stdscr: Curses window object
|
||||
error_message: Message to display
|
||||
duration_sec: How long the error should be displayed (for reference by caller)
|
||||
"""
|
||||
height, width = stdscr.getmaxyx()
|
||||
|
||||
|
||||
# Truncate message if too long
|
||||
error_message = error_message[:width-4]
|
||||
max_width = width - 4
|
||||
if len(error_message) > max_width:
|
||||
error_message = error_message[:max_width-3] + "..."
|
||||
|
||||
error_x = max(0, width // 2 - len(error_message) // 2)
|
||||
error_y = height - 4 # Show near bottom of screen
|
||||
# Position near bottom of screen
|
||||
error_y = height - 4
|
||||
|
||||
ViewUtils.draw_centered_text(
|
||||
stdscr,
|
||||
error_y,
|
||||
error_message,
|
||||
ColorScheme.ERROR,
|
||||
curses.A_BOLD
|
||||
)
|
||||
|
||||
stdscr.attron(curses.color_pair(3) | curses.A_BOLD)
|
||||
stdscr.addstr(error_y, error_x, error_message)
|
||||
stdscr.attroff(curses.color_pair(3) | curses.A_BOLD)
|
||||
def draw_big_digit(stdscr, y: int, x: int, digit: str):
|
||||
"""
|
||||
Draw a large digit using the predefined patterns
|
||||
|
||||
Args:
|
||||
stdscr: Curses window object
|
||||
y: Starting y coordinate
|
||||
x: Starting x coordinate
|
||||
digit: Character to draw ('0'-'9', ':', etc)
|
||||
"""
|
||||
try:
|
||||
patterns = BIG_DIGITS.get(digit, BIG_DIGITS['?'])
|
||||
for i, line in enumerate(patterns):
|
||||
try:
|
||||
stdscr.addstr(y + i, x, line)
|
||||
except curses.error:
|
||||
continue # Skip lines that would write outside the window
|
||||
except (curses.error, IndexError):
|
||||
pass # Ignore any drawing errors
|
||||
|
||||
def draw_big_digit(stdscr, y, x, digit):
|
||||
"""Draw a big digit using predefined patterns"""
|
||||
patterns = BIG_DIGITS[digit]
|
||||
for i, line in enumerate(patterns):
|
||||
stdscr.addstr(y + i, x, line)
|
||||
def safe_addstr(stdscr, y: int, x: int, text: str, color_pair: Optional[int] = None, attrs: int = 0):
|
||||
"""
|
||||
Safely add a string to the screen, handling boundary conditions
|
||||
|
||||
Args:
|
||||
stdscr: Curses window object
|
||||
y: Y coordinate
|
||||
x: X coordinate
|
||||
text: Text to draw
|
||||
color_pair: Optional color pair number
|
||||
attrs: Additional curses attributes
|
||||
"""
|
||||
height, width = stdscr.getmaxyx()
|
||||
|
||||
# Check if the position is within bounds
|
||||
if y < 0 or y >= height or x < 0 or x >= width:
|
||||
return
|
||||
|
||||
# Truncate text if it would extend beyond screen width
|
||||
if x + len(text) > width:
|
||||
text = text[:width - x]
|
||||
|
||||
try:
|
||||
if color_pair is not None:
|
||||
stdscr.attron(curses.color_pair(color_pair) | attrs)
|
||||
|
||||
stdscr.addstr(y, x, text)
|
||||
|
||||
if color_pair is not None:
|
||||
stdscr.attroff(curses.color_pair(color_pair) | attrs)
|
||||
except curses.error:
|
||||
pass # Ignore any drawing errors
|
||||
|
Loading…
x
Reference in New Issue
Block a user