2025-01-24 23:08:45 +02:00
|
|
|
import curses
|
2025-01-25 23:24:27 +02:00
|
|
|
from datetime import datetime
|
2025-01-24 23:08:45 +02:00
|
|
|
from big_digits import BIG_DIGITS
|
|
|
|
|
2025-01-25 23:24:27 +02:00
|
|
|
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)
|
|
|
|
|
2025-01-24 23:08:45 +02:00
|
|
|
def _draw_error(stdscr, error_message):
|
2025-01-25 23:24:27 +02:00
|
|
|
"""Draw error message following specification"""
|
|
|
|
height, width = stdscr.getmaxyx()
|
|
|
|
|
|
|
|
# Truncate message if too long
|
|
|
|
error_message = error_message[:width-4]
|
|
|
|
|
|
|
|
error_x = max(0, width // 2 - len(error_message) // 2)
|
|
|
|
error_y = height - 4 # Show near bottom of screen
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
2025-01-26 22:02:23 +02:00
|
|
|
def _draw_active_alarms(stdscr, context):
|
2025-01-27 12:10:57 +02:00
|
|
|
"""Draw the active alarms"""
|
2025-01-27 11:26:45 +02:00
|
|
|
_init_colors()
|
2025-01-26 22:02:23 +02:00
|
|
|
height, width = stdscr.getmaxyx()
|
2025-01-27 12:10:57 +02:00
|
|
|
current_time = datetime.now()
|
2025-01-26 22:02:23 +02:00
|
|
|
|
2025-01-27 12:10:57 +02:00
|
|
|
# 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
|
|
|
|
|
|
|
|
# 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))
|
|
|
|
|
|
|
|
# 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 (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))
|
2025-01-26 22:02:23 +02:00
|
|
|
|
2025-01-27 12:10:57 +02:00
|
|
|
# Get active alarm info
|
|
|
|
active_alarms = context.get('active_alarms', {})
|
2025-01-26 22:02:23 +02:00
|
|
|
if not active_alarms:
|
|
|
|
return
|
|
|
|
|
|
|
|
# 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']
|
2025-01-27 12:10:57 +02:00
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
# Center the alarm info
|
|
|
|
info_x = max(0, width // 2 - len(alarm_str) // 2)
|
|
|
|
|
|
|
|
# Draw with green color
|
|
|
|
stdscr.attron(curses.color_pair(1))
|
|
|
|
stdscr.addstr(alarm_y, alarm_x, alarm_str)
|
|
|
|
stdscr.attroff(curses.color_pair(1))
|
2025-01-26 22:02:23 +02:00
|
|
|
|
|
|
|
# Instructions
|
|
|
|
stdscr.addstr(height - 2, width // 2 - 15, "S: Snooze D: Dismiss")
|
|
|
|
|
2025-01-25 23:24:27 +02:00
|
|
|
def _draw_big_digit(stdscr, y, x, digit):
|
|
|
|
"""Draw a big digit using predefined patterns"""
|
|
|
|
patterns = BIG_DIGITS[digit]
|
2025-01-24 23:08:45 +02:00
|
|
|
for i, line in enumerate(patterns):
|
|
|
|
stdscr.addstr(y + i, x, line)
|
|
|
|
|
2025-01-25 23:24:27 +02:00
|
|
|
def _draw_main_clock(stdscr, context=None):
|
|
|
|
"""Draw the main clock screen"""
|
|
|
|
_init_colors()
|
2025-01-24 23:08:45 +02:00
|
|
|
height, width = stdscr.getmaxyx()
|
2025-01-25 23:24:27 +02:00
|
|
|
current_time = datetime.now()
|
2025-01-24 23:08:45 +02:00
|
|
|
|
2025-01-25 23:24:27 +02:00
|
|
|
# Big time display
|
|
|
|
time_str = current_time.strftime("%H:%M:%S")
|
|
|
|
digit_width = 14 # Width of each digit pattern
|
2025-01-24 23:08:45 +02:00
|
|
|
total_width = digit_width * len(time_str)
|
|
|
|
start_x = (width - total_width) // 2
|
2025-01-25 23:24:27 +02:00
|
|
|
start_y = (height - 7) // 2 - 4
|
2025-01-24 23:08:45 +02:00
|
|
|
|
2025-01-25 23:24:27 +02:00
|
|
|
# Green color for big time
|
2025-01-24 23:08:45 +02:00
|
|
|
stdscr.attron(curses.color_pair(1))
|
|
|
|
for i, digit in enumerate(time_str):
|
2025-01-25 23:24:27 +02:00
|
|
|
_draw_big_digit(stdscr, start_y, start_x + i * digit_width, digit)
|
2025-01-24 23:08:45 +02:00
|
|
|
stdscr.attroff(curses.color_pair(1))
|
|
|
|
|
2025-01-25 23:24:27 +02:00
|
|
|
# Date display
|
2025-01-24 23:08:45 +02:00
|
|
|
date_str = current_time.strftime("%Y-%m-%d")
|
|
|
|
date_x = width // 2 - len(date_str) // 2
|
2025-01-25 23:24:27 +02:00
|
|
|
date_y = height // 2 + 4
|
2025-01-24 23:08:45 +02:00
|
|
|
|
|
|
|
stdscr.attron(curses.color_pair(2))
|
|
|
|
stdscr.addstr(date_y, date_x, date_str)
|
|
|
|
stdscr.attroff(curses.color_pair(2))
|
|
|
|
|
2025-01-25 23:24:27 +02:00
|
|
|
# Menu options
|
|
|
|
menu_str = "A: Add Alarm S: List Alarms Q: Quit"
|
2025-01-24 23:08:45 +02:00
|
|
|
menu_x = width // 2 - len(menu_str) // 2
|
|
|
|
stdscr.addstr(height - 2, menu_x, menu_str)
|
|
|
|
|
|
|
|
def _draw_add_alarm(stdscr, context):
|
2025-01-28 10:55:14 +02:00
|
|
|
"""Draw the add alarm screen"""
|
2025-01-25 23:24:27 +02:00
|
|
|
# Ensure context is a dictionary with default values
|
|
|
|
if context is None:
|
|
|
|
context = {}
|
|
|
|
|
|
|
|
# Provide default values with more explicit checks
|
|
|
|
context = {
|
|
|
|
'new_alarm_selected': context.get('new_alarm_selected', 0),
|
|
|
|
'new_alarm_name': context.get('new_alarm_name', 'New Alarm'),
|
|
|
|
'new_alarm_hour': context.get('new_alarm_hour', datetime.now().hour),
|
|
|
|
'new_alarm_minute': context.get('new_alarm_minute', datetime.now().minute),
|
|
|
|
'new_alarm_enabled': context.get('new_alarm_enabled', True),
|
|
|
|
'new_alarm_date': context.get('new_alarm_date') or None,
|
|
|
|
'new_alarm_weekdays': context.get('new_alarm_weekdays', []) or [],
|
|
|
|
'weekday_names': context.get('weekday_names', ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'])
|
|
|
|
}
|
|
|
|
|
|
|
|
_init_colors()
|
2025-01-24 23:08:45 +02:00
|
|
|
height, width = stdscr.getmaxyx()
|
2025-01-28 10:55:14 +02:00
|
|
|
|
|
|
|
# Center the form vertically with good spacing
|
|
|
|
form_y = height // 2 - 6
|
|
|
|
|
|
|
|
# Title with green color and bold
|
|
|
|
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)
|
|
|
|
|
|
|
|
# Helper function to draw a labeled field
|
|
|
|
def draw_field(y, label, value, is_selected, center_offset=0):
|
|
|
|
label_str = f"{label}: "
|
|
|
|
total_width = len(label_str) + len(str(value))
|
|
|
|
x = width // 2 - total_width // 2 + center_offset
|
|
|
|
|
|
|
|
# Draw label in green
|
|
|
|
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)) # Yellow for selected
|
|
|
|
stdscr.addstr(y, x + len(label_str), str(value))
|
|
|
|
stdscr.attroff(curses.color_pair(2))
|
|
|
|
else:
|
|
|
|
stdscr.attron(curses.color_pair(1)) # Green for normal
|
|
|
|
stdscr.addstr(y, x + len(label_str), str(value))
|
|
|
|
stdscr.attroff(curses.color_pair(1))
|
|
|
|
|
|
|
|
# Name field with proper spacing
|
2025-01-25 23:24:27 +02:00
|
|
|
name_str = str(context['new_alarm_name'])
|
2025-01-28 10:55:14 +02:00
|
|
|
draw_field(form_y + 2, "Name", name_str, context['new_alarm_selected'] == 4)
|
2025-01-24 23:08:45 +02:00
|
|
|
|
2025-01-28 10:55:14 +02:00
|
|
|
# Time selection with centered layout
|
|
|
|
time_y = form_y + 4
|
|
|
|
time_label = "Time: "
|
2025-01-25 23:24:27 +02:00
|
|
|
hour_str = f"{int(context['new_alarm_hour']):02d}"
|
|
|
|
minute_str = f"{int(context['new_alarm_minute']):02d}"
|
2025-01-28 10:55:14 +02:00
|
|
|
|
|
|
|
# Calculate center position for time
|
|
|
|
time_total_width = len(time_label) + 5 # 5 = HH:MM
|
|
|
|
time_x = width // 2 - time_total_width // 2
|
|
|
|
|
|
|
|
# Draw time label
|
|
|
|
stdscr.attron(curses.color_pair(1))
|
|
|
|
stdscr.addstr(time_y, time_x, time_label)
|
|
|
|
stdscr.attroff(curses.color_pair(1))
|
|
|
|
|
|
|
|
# Draw hour
|
2025-01-24 23:08:45 +02:00
|
|
|
if context['new_alarm_selected'] == 0:
|
|
|
|
stdscr.attron(curses.color_pair(2))
|
2025-01-28 10:55:14 +02:00
|
|
|
else:
|
|
|
|
stdscr.attron(curses.color_pair(1))
|
|
|
|
stdscr.addstr(time_y, time_x + len(time_label), hour_str)
|
2025-01-24 23:08:45 +02:00
|
|
|
if context['new_alarm_selected'] == 0:
|
|
|
|
stdscr.attroff(curses.color_pair(2))
|
2025-01-28 10:55:14 +02:00
|
|
|
else:
|
|
|
|
stdscr.attroff(curses.color_pair(1))
|
2025-01-24 23:08:45 +02:00
|
|
|
|
2025-01-28 10:55:14 +02:00
|
|
|
# Draw colon
|
|
|
|
stdscr.attron(curses.color_pair(1))
|
|
|
|
stdscr.addstr(time_y, time_x + len(time_label) + 2, ":")
|
|
|
|
stdscr.attroff(curses.color_pair(1))
|
2025-01-24 23:08:45 +02:00
|
|
|
|
2025-01-28 10:55:14 +02:00
|
|
|
# Draw minute
|
2025-01-24 23:08:45 +02:00
|
|
|
if context['new_alarm_selected'] == 1:
|
|
|
|
stdscr.attron(curses.color_pair(2))
|
2025-01-28 10:55:14 +02:00
|
|
|
else:
|
|
|
|
stdscr.attron(curses.color_pair(1))
|
|
|
|
stdscr.addstr(time_y, time_x + len(time_label) + 3, minute_str)
|
2025-01-24 23:08:45 +02:00
|
|
|
if context['new_alarm_selected'] == 1:
|
|
|
|
stdscr.attroff(curses.color_pair(2))
|
2025-01-28 10:55:14 +02:00
|
|
|
else:
|
|
|
|
stdscr.attroff(curses.color_pair(1))
|
2025-01-24 23:08:45 +02:00
|
|
|
|
|
|
|
# Date selection
|
2025-01-25 23:24:27 +02:00
|
|
|
if context['new_alarm_date'] and hasattr(context['new_alarm_date'], 'strftime'):
|
|
|
|
date_str = context['new_alarm_date'].strftime("%Y-%m-%d")
|
|
|
|
else:
|
|
|
|
date_str = 'No specific date'
|
2025-01-28 10:55:14 +02:00
|
|
|
draw_field(form_y + 6, "Date", date_str, context['new_alarm_selected'] == 2)
|
|
|
|
|
|
|
|
# Weekday selection with improved visual style
|
|
|
|
weekday_y = form_y + 8
|
|
|
|
weekday_label = "Repeat: "
|
|
|
|
weekdays_str = " ".join(
|
|
|
|
f"[{context['weekday_names'][i]}]" if i in context['new_alarm_weekdays']
|
|
|
|
else f" {context['weekday_names'][i]} "
|
|
|
|
for i in range(len(context['weekday_names']))
|
2025-01-24 23:08:45 +02:00
|
|
|
)
|
2025-01-28 10:55:14 +02:00
|
|
|
|
|
|
|
# Center weekdays
|
|
|
|
total_width = len(weekday_label) + len(weekdays_str)
|
|
|
|
weekday_x = width // 2 - total_width // 2
|
|
|
|
|
|
|
|
# Draw weekday selection
|
|
|
|
stdscr.attron(curses.color_pair(1))
|
|
|
|
stdscr.addstr(weekday_y, weekday_x, weekday_label)
|
|
|
|
stdscr.attroff(curses.color_pair(1))
|
|
|
|
|
2025-01-24 23:08:45 +02:00
|
|
|
if context['new_alarm_selected'] == 3:
|
|
|
|
stdscr.attron(curses.color_pair(2))
|
2025-01-28 10:55:14 +02:00
|
|
|
else:
|
|
|
|
stdscr.attron(curses.color_pair(1))
|
|
|
|
stdscr.addstr(weekday_y, weekday_x + len(weekday_label), weekdays_str)
|
2025-01-24 23:08:45 +02:00
|
|
|
if context['new_alarm_selected'] == 3:
|
|
|
|
stdscr.attroff(curses.color_pair(2))
|
2025-01-28 10:55:14 +02:00
|
|
|
else:
|
|
|
|
stdscr.attroff(curses.color_pair(1))
|
2025-01-24 23:08:45 +02:00
|
|
|
|
2025-01-28 10:55:14 +02:00
|
|
|
# Enabled/Disabled toggle with visual indicator
|
|
|
|
status_y = form_y + 10
|
|
|
|
enabled_str = "● Enabled" if context['new_alarm_enabled'] else "○ Disabled"
|
|
|
|
draw_field(status_y, "Status", enabled_str, context['new_alarm_selected'] == 5, -2)
|
|
|
|
|
|
|
|
# Instructions in green at the bottom
|
|
|
|
instructions = "j/k: Change h/l: Switch 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))
|
2025-01-24 23:08:45 +02:00
|
|
|
|
|
|
|
def _draw_list_alarms(stdscr, context):
|
2025-01-25 23:24:27 +02:00
|
|
|
"""Draw the list of alarms screen"""
|
|
|
|
_init_colors()
|
2025-01-24 23:08:45 +02:00
|
|
|
height, width = stdscr.getmaxyx()
|
2025-01-28 10:55:14 +02:00
|
|
|
|
|
|
|
# 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'])
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
2025-01-24 23:08:45 +02:00
|
|
|
# Header
|
2025-01-28 10:55:14 +02:00
|
|
|
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)
|
|
|
|
|
|
|
|
# Draw alarms
|
|
|
|
for i in range(start_idx, end_idx):
|
|
|
|
y_pos = 4 + (i - start_idx)
|
|
|
|
|
|
|
|
if i == len(alarms): # "Add new alarm" option
|
|
|
|
display_str = "Add new alarm..."
|
|
|
|
else:
|
|
|
|
alarm = alarms[i]
|
2025-01-25 23:24:27 +02:00
|
|
|
# Format time
|
2025-01-24 23:08:45 +02:00
|
|
|
time_str = alarm.get('time', 'Unknown')
|
2025-01-28 10:55:14 +02:00
|
|
|
|
2025-01-24 23:08:45 +02:00
|
|
|
# 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', [])
|
2025-01-25 23:24:27 +02:00
|
|
|
repeat_info = f" (Every {', '.join(weekday_names[d] for d in days)})"
|
2025-01-24 23:08:45 +02:00
|
|
|
elif repeat_rule.get('type') == 'once' and repeat_rule.get('date'):
|
|
|
|
repeat_info = f" (On {repeat_rule['date']})"
|
2025-01-28 10:55:14 +02:00
|
|
|
|
|
|
|
# Status indicator (in green)
|
2025-01-24 23:08:45 +02:00
|
|
|
status = "✓" if alarm.get('enabled', True) else "✗"
|
2025-01-25 23:24:27 +02:00
|
|
|
display_str = f"{status} {time_str} {alarm.get('name', 'Unnamed')}{repeat_info}"
|
2025-01-28 10:55:14 +02:00
|
|
|
|
|
|
|
# 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] + "..."
|
|
|
|
|
|
|
|
# Center the item
|
|
|
|
x_pos = width // 2 - len(display_str) // 2
|
|
|
|
|
|
|
|
# Highlight selected item
|
|
|
|
if i == selected_index:
|
|
|
|
# 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)
|
|
|
|
stdscr.attroff(curses.color_pair(2))
|
|
|
|
else:
|
|
|
|
# Draw normal items in green
|
|
|
|
stdscr.attron(curses.color_pair(1))
|
|
|
|
stdscr.addstr(y_pos, x_pos, display_str)
|
|
|
|
stdscr.attroff(curses.color_pair(1))
|
|
|
|
|
2025-01-25 23:24:27 +02:00
|
|
|
# Instructions
|
2025-01-28 10:55:14 +02:00
|
|
|
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))
|