From 3a3813e389f2860dc7577ea2e36e54f93aeb6dae Mon Sep 17 00:00:00 2001 From: Kalzu Rekku Date: Tue, 28 Jan 2025 10:55:14 +0200 Subject: [PATCH] Some nice UI upgrades. --- alert_api/big_digits.py | 14 +-- alert_api/ncurses_ui.py | 27 ++++- alert_api/ncurses_ui_draw.py | 216 +++++++++++++++++++++++++---------- 3 files changed, 186 insertions(+), 71 deletions(-) diff --git a/alert_api/big_digits.py b/alert_api/big_digits.py index c86cf72..df5d452 100644 --- a/alert_api/big_digits.py +++ b/alert_api/big_digits.py @@ -10,13 +10,13 @@ BIG_DIGITS = { " █████████ " ], '1': [ - " ███ ", - " █████ ", - " ███ ", - " ███ ", - " ███ ", - " ███ ", - " ███████ " + " ████ ", + " ██████ ", + " ████ ", + " ████ ", + " ████ ", + " ████ ", + " █████████ " ], '2': [ " ██████████ ", diff --git a/alert_api/ncurses_ui.py b/alert_api/ncurses_ui.py index cdf7d35..9252c4b 100644 --- a/alert_api/ncurses_ui.py +++ b/alert_api/ncurses_ui.py @@ -225,17 +225,33 @@ class UI: 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'): - # Delete last alarm - if self.alarm_list: - last_alarm = self.alarm_list[-1] + # 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: - 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() + # 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") def _show_error(self, message, duration=30): """Display an error message""" @@ -271,7 +287,8 @@ class UI: elif self.current_view == 'LIST_ALARMS': _draw_list_alarms(stdscr, { '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': # Draw active alarm view diff --git a/alert_api/ncurses_ui_draw.py b/alert_api/ncurses_ui_draw.py index 409f8f3..ad742c6 100644 --- a/alert_api/ncurses_ui_draw.py +++ b/alert_api/ncurses_ui_draw.py @@ -133,7 +133,7 @@ def _draw_main_clock(stdscr, context=None): stdscr.addstr(height - 2, menu_x, menu_str) 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 if context is None: context = {} @@ -152,92 +152,167 @@ def _draw_add_alarm(stdscr, context): _init_colors() height, width = stdscr.getmaxyx() - form_y = height // 2 - 3 + + # Center the form vertically with good spacing + form_y = height // 2 - 6 - # Title - stdscr.addstr(form_y - 1, width // 2 - 10, "Add New Alarm") + # 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) - # Name input + # 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 name_str = str(context['new_alarm_name']) - if context['new_alarm_selected'] == 4: - stdscr.attron(curses.color_pair(2)) - stdscr.addstr(form_y + 1, width // 2 - 10, f"Name: {name_str}") - if context['new_alarm_selected'] == 4: - stdscr.attroff(curses.color_pair(2)) + draw_field(form_y + 2, "Name", name_str, context['new_alarm_selected'] == 4) - # Time selection + # Time selection with centered layout + time_y = form_y + 4 + time_label = "Time: " hour_str = f"{int(context['new_alarm_hour']):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: 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: 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: 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: stdscr.attroff(curses.color_pair(2)) - - # Enabled toggle - 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)) + else: + stdscr.attroff(curses.color_pair(1)) # Date selection 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' + draw_field(form_y + 6, "Date", date_str, context['new_alarm_selected'] == 2) - if context['new_alarm_selected'] == 2: - stdscr.attron(curses.color_pair(2)) - stdscr.addstr(form_y + 3, width // 2 - len(date_str) // 2, date_str) - if context['new_alarm_selected'] == 2: - stdscr.attroff(curses.color_pair(2)) - - # Weekday selection - 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)) + # 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'])) ) - + + # 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: 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: stdscr.attroff(curses.color_pair(2)) + else: + stdscr.attroff(curses.color_pair(1)) - # Instructions - stdscr.addstr(height - 2, 2, - "jk: Change hl: Switch Space: Toggle Enter: Save Esc: Cancel") + # 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)) def _draw_list_alarms(stdscr, context): """Draw the list of alarms screen""" _init_colors() height, width = stdscr.getmaxyx() - + + # 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) + # Header - stdscr.addstr(2, width // 2 - 5, "Alarms") - - 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']) - - for i, alarm in enumerate(context['alarms'][:height-6]): + 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 time_str = alarm.get('time', 'Unknown') - + # Format repeat info repeat_info = "" repeat_rule = alarm.get('repeat_rule', {}) @@ -247,15 +322,38 @@ def _draw_list_alarms(stdscr, context): 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']})" - - # Status indicator + + # Status indicator (in green) status = "✓" if alarm.get('enabled', True) else "✗" display_str = f"{status} {time_str} {alarm.get('name', 'Unnamed')}{repeat_info}" - - # Truncate if too long - display_str = display_str[:width-4] - - stdscr.addstr(4 + i, 2, display_str) - + + # 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)) + # 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))