Some nice UI upgrades.
This commit is contained in:
		| @@ -10,13 +10,13 @@ BIG_DIGITS = { | |||||||
|         "  █████████  " |         "  █████████  " | ||||||
|     ], |     ], | ||||||
|     '1': [ |     '1': [ | ||||||
|         "     ███    ", |         "     ████    ", | ||||||
|         "   █████    ", |         "   ██████    ", | ||||||
|         "     ███    ", |         "     ████    ", | ||||||
|         "     ███    ", |         "     ████    ", | ||||||
|         "     ███    ", |         "     ████    ", | ||||||
|         "     ███    ", |         "     ████    ", | ||||||
|         "   ███████  " |         "  █████████  " | ||||||
|     ], |     ], | ||||||
|     '2': [ |     '2': [ | ||||||
|         " ██████████ ", |         " ██████████ ", | ||||||
|   | |||||||
| @@ -225,17 +225,33 @@ class UI: | |||||||
|  |  | ||||||
|     def _handle_list_alarms_input(self, key): |     def _handle_list_alarms_input(self, key): | ||||||
|         """Handle input for alarm list view""" |         """Handle input for alarm list view""" | ||||||
|  |         total_items = len(self.alarm_list) + 1  # +1 for "Add new alarm" option | ||||||
|  |  | ||||||
|         if key == 27:  # ESC |         if key == 27:  # ESC | ||||||
|             self.current_view = 'CLOCK' |             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'): |         elif key == ord('d'): | ||||||
|             # Delete last alarm |             # Only delete if a real alarm is selected (not the "Add new" option) | ||||||
|             if self.alarm_list: |             if self.selected_item < len(self.alarm_list) and self.alarm_list: | ||||||
|                 last_alarm = self.alarm_list[-1] |  | ||||||
|                 try: |                 try: | ||||||
|                     self.storage.remove_saved_alert(last_alarm['id']) |                     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() |                     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: |                 except Exception as e: | ||||||
|                     self._show_error(f"Failed to delete alarm: {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") | ||||||
|  |  | ||||||
|     def _show_error(self, message, duration=30): |     def _show_error(self, message, duration=30): | ||||||
|         """Display an error message""" |         """Display an error message""" | ||||||
| @@ -271,7 +287,8 @@ class UI: | |||||||
|             elif self.current_view == 'LIST_ALARMS': |             elif self.current_view == 'LIST_ALARMS': | ||||||
|                 _draw_list_alarms(stdscr, { |                 _draw_list_alarms(stdscr, { | ||||||
|                     'alarms': self.alarm_list or [], |                     'alarms': self.alarm_list or [], | ||||||
|                     'weekday_names': self.weekday_names |                     'weekday_names': self.weekday_names, | ||||||
|  |                     'selected_index': self.selected_item | ||||||
|                 }) |                 }) | ||||||
|             elif self.current_view == 'ACTIVE_ALARMS': |             elif self.current_view == 'ACTIVE_ALARMS': | ||||||
|                 # Draw active alarm view |                 # Draw active alarm view | ||||||
|   | |||||||
| @@ -133,7 +133,7 @@ def _draw_main_clock(stdscr, context=None): | |||||||
|     stdscr.addstr(height - 2, menu_x, menu_str) |     stdscr.addstr(height - 2, menu_x, menu_str) | ||||||
|  |  | ||||||
| def _draw_add_alarm(stdscr, context): | def _draw_add_alarm(stdscr, context): | ||||||
|     """Draw the add alarm screen following specification""" |     """Draw the add alarm screen""" | ||||||
|     # Ensure context is a dictionary with default values |     # Ensure context is a dictionary with default values | ||||||
|     if context is None: |     if context is None: | ||||||
|         context = {} |         context = {} | ||||||
| @@ -152,89 +152,164 @@ def _draw_add_alarm(stdscr, context): | |||||||
|  |  | ||||||
|     _init_colors() |     _init_colors() | ||||||
|     height, width = stdscr.getmaxyx() |     height, width = stdscr.getmaxyx() | ||||||
|     form_y = height // 2 - 3 |  | ||||||
|      |      | ||||||
|     # Title |     # Center the form vertically with good spacing | ||||||
|     stdscr.addstr(form_y - 1, width // 2 - 10, "Add New Alarm") |     form_y = height // 2 - 6 | ||||||
|  |  | ||||||
|     # Name input |     # Title with green color and bold | ||||||
|     name_str = str(context['new_alarm_name']) |     title = "Add New Alarm" | ||||||
|     if context['new_alarm_selected'] == 4: |     stdscr.attron(curses.color_pair(1) | curses.A_BOLD) | ||||||
|         stdscr.attron(curses.color_pair(2)) |     stdscr.addstr(form_y, width // 2 - len(title) // 2, title) | ||||||
|     stdscr.addstr(form_y + 1, width // 2 - 10, f"Name: {name_str}") |     stdscr.attroff(curses.color_pair(1) | curses.A_BOLD) | ||||||
|     if context['new_alarm_selected'] == 4: |  | ||||||
|  |     # 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)) |             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)) | ||||||
|  |  | ||||||
|     # Time selection |     # Name field with proper spacing | ||||||
|  |     name_str = str(context['new_alarm_name']) | ||||||
|  |     draw_field(form_y + 2, "Name", name_str, context['new_alarm_selected'] == 4) | ||||||
|  |  | ||||||
|  |     # Time selection with centered layout | ||||||
|  |     time_y = form_y + 4 | ||||||
|  |     time_label = "Time: " | ||||||
|     hour_str = f"{int(context['new_alarm_hour']):02d}" |     hour_str = f"{int(context['new_alarm_hour']):02d}" | ||||||
|     minute_str = f"{int(context['new_alarm_minute']):02d}" |     minute_str = f"{int(context['new_alarm_minute']):02d}" | ||||||
|      |      | ||||||
|     # Highlight time components |     # 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 | ||||||
|     if context['new_alarm_selected'] == 0: |     if context['new_alarm_selected'] == 0: | ||||||
|         stdscr.attron(curses.color_pair(2)) |         stdscr.attron(curses.color_pair(2)) | ||||||
|     stdscr.addstr(form_y + 2, width // 2 - 2, hour_str) |     else: | ||||||
|  |         stdscr.attron(curses.color_pair(1)) | ||||||
|  |     stdscr.addstr(time_y, time_x + len(time_label), hour_str) | ||||||
|     if context['new_alarm_selected'] == 0: |     if context['new_alarm_selected'] == 0: | ||||||
|         stdscr.attroff(curses.color_pair(2)) |         stdscr.attroff(curses.color_pair(2)) | ||||||
|  |     else: | ||||||
|  |         stdscr.attroff(curses.color_pair(1)) | ||||||
|  |  | ||||||
|     stdscr.addstr(form_y + 2, width // 2, ":") |     # Draw colon | ||||||
|  |     stdscr.attron(curses.color_pair(1)) | ||||||
|  |     stdscr.addstr(time_y, time_x + len(time_label) + 2, ":") | ||||||
|  |     stdscr.attroff(curses.color_pair(1)) | ||||||
|  |  | ||||||
|  |     # Draw minute | ||||||
|     if context['new_alarm_selected'] == 1: |     if context['new_alarm_selected'] == 1: | ||||||
|         stdscr.attron(curses.color_pair(2)) |         stdscr.attron(curses.color_pair(2)) | ||||||
|     stdscr.addstr(form_y + 2, width // 2 + 1, minute_str) |     else: | ||||||
|  |         stdscr.attron(curses.color_pair(1)) | ||||||
|  |     stdscr.addstr(time_y, time_x + len(time_label) + 3, minute_str) | ||||||
|     if context['new_alarm_selected'] == 1: |     if context['new_alarm_selected'] == 1: | ||||||
|         stdscr.attroff(curses.color_pair(2)) |         stdscr.attroff(curses.color_pair(2)) | ||||||
|  |     else: | ||||||
|     # Enabled toggle |         stdscr.attroff(curses.color_pair(1)) | ||||||
|     enabled_str = "Enabled" if context['new_alarm_enabled'] else "Disabled" |  | ||||||
|     if context['new_alarm_selected'] == 5: |  | ||||||
|         stdscr.attron(curses.color_pair(2)) |  | ||||||
|     stdscr.addstr(form_y + 4, width // 2 - len(enabled_str)//2, enabled_str) |  | ||||||
|     if context['new_alarm_selected'] == 5: |  | ||||||
|         stdscr.attroff(curses.color_pair(2)) |  | ||||||
|  |  | ||||||
|     # Date selection |     # Date selection | ||||||
|     if context['new_alarm_date'] and hasattr(context['new_alarm_date'], 'strftime'): |     if context['new_alarm_date'] and hasattr(context['new_alarm_date'], 'strftime'): | ||||||
|         date_str = context['new_alarm_date'].strftime("%Y-%m-%d") |         date_str = context['new_alarm_date'].strftime("%Y-%m-%d") | ||||||
|     else: |     else: | ||||||
|         date_str = 'No specific date' |         date_str = 'No specific date' | ||||||
|  |     draw_field(form_y + 6, "Date", date_str, context['new_alarm_selected'] == 2) | ||||||
|  |  | ||||||
|     if context['new_alarm_selected'] == 2: |     # Weekday selection with improved visual style | ||||||
|         stdscr.attron(curses.color_pair(2)) |     weekday_y = form_y + 8 | ||||||
|     stdscr.addstr(form_y + 3, width // 2 - len(date_str) // 2, date_str) |     weekday_label = "Repeat: " | ||||||
|     if context['new_alarm_selected'] == 2: |     weekdays_str = " ".join( | ||||||
|         stdscr.attroff(curses.color_pair(2)) |         f"[{context['weekday_names'][i]}]" if i in context['new_alarm_weekdays'] | ||||||
|  |         else f" {context['weekday_names'][i]} " | ||||||
|     # Weekday selection |         for i in range(len(context['weekday_names'])) | ||||||
|     weekday_names = context['weekday_names'] |  | ||||||
|     weekday_str = "Repeat: " + " ".join( |  | ||||||
|         weekday_names[i] if i in context['new_alarm_weekdays'] else "___" |  | ||||||
|         for i in range(len(weekday_names)) |  | ||||||
|     ) |     ) | ||||||
|      |      | ||||||
|  |     # 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)) | ||||||
|  |      | ||||||
|     if context['new_alarm_selected'] == 3: |     if context['new_alarm_selected'] == 3: | ||||||
|         stdscr.attron(curses.color_pair(2)) |         stdscr.attron(curses.color_pair(2)) | ||||||
|     stdscr.addstr(form_y + 5, width // 2 - len(weekday_str) // 2, weekday_str) |     else: | ||||||
|  |         stdscr.attron(curses.color_pair(1)) | ||||||
|  |     stdscr.addstr(weekday_y, weekday_x + len(weekday_label), weekdays_str) | ||||||
|     if context['new_alarm_selected'] == 3: |     if context['new_alarm_selected'] == 3: | ||||||
|         stdscr.attroff(curses.color_pair(2)) |         stdscr.attroff(curses.color_pair(2)) | ||||||
|  |     else: | ||||||
|  |         stdscr.attroff(curses.color_pair(1)) | ||||||
|  |  | ||||||
|     # Instructions |     # Enabled/Disabled toggle with visual indicator | ||||||
|     stdscr.addstr(height - 2, 2, |     status_y = form_y + 10 | ||||||
|         "jk: Change  hl: Switch  Space: Toggle  Enter: Save  Esc: Cancel") |     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)) | ||||||
|  |  | ||||||
| def _draw_list_alarms(stdscr, context): | def _draw_list_alarms(stdscr, context): | ||||||
|     """Draw the list of alarms screen""" |     """Draw the list of alarms screen""" | ||||||
|     _init_colors() |     _init_colors() | ||||||
|     height, width = stdscr.getmaxyx() |     height, width = stdscr.getmaxyx() | ||||||
|      |      | ||||||
|     # Header |     # Get required data from context | ||||||
|     stdscr.addstr(2, width // 2 - 5, "Alarms") |     alarms = context.get('alarms', []) | ||||||
|  |     selected_index = context.get('selected_index', 0) | ||||||
|     if not context.get('alarms'): |  | ||||||
|         stdscr.addstr(4, width // 2 - 10, "No alarms set") |  | ||||||
|     else: |  | ||||||
|     weekday_names = context.get('weekday_names', ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']) |     weekday_names = context.get('weekday_names', ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']) | ||||||
|      |      | ||||||
|         for i, alarm in enumerate(context['alarms'][:height-6]): |     # 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) | ||||||
|  |      | ||||||
|  |     # 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) | ||||||
|  |      | ||||||
|  |     # 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] | ||||||
|             # Format time |             # Format time | ||||||
|             time_str = alarm.get('time', 'Unknown') |             time_str = alarm.get('time', 'Unknown') | ||||||
|              |              | ||||||
| @@ -248,14 +323,37 @@ def _draw_list_alarms(stdscr, context): | |||||||
|                 elif repeat_rule.get('type') == 'once' and repeat_rule.get('date'): |                 elif repeat_rule.get('type') == 'once' and repeat_rule.get('date'): | ||||||
|                     repeat_info = f" (On {repeat_rule['date']})" |                     repeat_info = f" (On {repeat_rule['date']})" | ||||||
|              |              | ||||||
|             # Status indicator |             # Status indicator (in green) | ||||||
|             status = "✓" if alarm.get('enabled', True) else "✗" |             status = "✓" if alarm.get('enabled', True) else "✗" | ||||||
|             display_str = f"{status} {time_str} {alarm.get('name', 'Unnamed')}{repeat_info}" |             display_str = f"{status} {time_str} {alarm.get('name', 'Unnamed')}{repeat_info}" | ||||||
|          |          | ||||||
|             # Truncate if too long |         # Truncate if too long (leaving space for selection brackets) | ||||||
|             display_str = display_str[:width-4] |         max_length = width - 6 | ||||||
|  |         if len(display_str) > max_length: | ||||||
|  |             display_str = display_str[:max_length-3] + "..." | ||||||
|          |          | ||||||
|             stdscr.addstr(4 + i, 2, display_str) |         # 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)) | ||||||
|      |      | ||||||
|     # Instructions |     # Instructions | ||||||
|     stdscr.addstr(height - 2, 2, "D: Delete  Enter: Edit  Esc: Back") |     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)) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Kalzu Rekku
					Kalzu Rekku