From 211f5819218685b04636246f4150d7f78a2835cc Mon Sep 17 00:00:00 2001 From: Kalzu Rekku Date: Thu, 23 Jan 2025 23:12:52 +0200 Subject: [PATCH] python import problems. --- alert_clients/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 172 bytes alert_clients/alarm_api_client/__init__.py | 0 .../alarm_api_client.py | 0 .../cli_client.py} | 3 +- alert_clients/ncurses_client/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 187 bytes .../__pycache__/main.cpython-312.pyc | Bin 0 -> 927 bytes .../ncurses_handlers.cpython-312.pyc | Bin 0 -> 11940 bytes alert_clients/ncurses_client/alarm-logger.py | 78 +++++++ alert_clients/ncurses_client/alert_logic.py | 88 ++++++++ alert_clients/ncurses_client/main.py | 18 ++ .../ncurses_client/ncurses_handlers.py | 212 ++++++++++++++++++ .../ncurses_client/ncurses_threads.py | 30 +++ alert_clients/ncurses_client/ncurses_ui.py | 166 ++++++++++++++ 15 files changed, 593 insertions(+), 2 deletions(-) create mode 100644 alert_clients/__init__.py create mode 100644 alert_clients/__pycache__/__init__.cpython-312.pyc create mode 100644 alert_clients/alarm_api_client/__init__.py rename alert_clients/{ => alarm_api_client}/alarm_api_client.py (100%) rename alert_clients/{alarm_client_cli.py => cli_client/cli_client.py} (98%) create mode 100644 alert_clients/ncurses_client/__init__.py create mode 100644 alert_clients/ncurses_client/__pycache__/__init__.cpython-312.pyc create mode 100644 alert_clients/ncurses_client/__pycache__/main.cpython-312.pyc create mode 100644 alert_clients/ncurses_client/__pycache__/ncurses_handlers.cpython-312.pyc create mode 100644 alert_clients/ncurses_client/alarm-logger.py create mode 100644 alert_clients/ncurses_client/alert_logic.py create mode 100755 alert_clients/ncurses_client/main.py create mode 100644 alert_clients/ncurses_client/ncurses_handlers.py create mode 100644 alert_clients/ncurses_client/ncurses_threads.py create mode 100644 alert_clients/ncurses_client/ncurses_ui.py diff --git a/alert_clients/__init__.py b/alert_clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/alert_clients/__pycache__/__init__.cpython-312.pyc b/alert_clients/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ac403a522f816125539d1e05e2607da9129d2097 GIT binary patch literal 172 zcmX@j%ge<81k*Q6N(a%8K?FMZ%mNgd&QQsq$>_I|p@<2{`wUX^%UeGqKQ~oBJ29uK zRKK_=SwBBFu|&V1vLquvFTS{>G%Za(H8nLpF(_I|p@<2{`wUX^D@s2jKQ~oBJ29uK zRKK_=SwBBFu|&V1vLquvFTS{>G%Za(H8nLpF(XDo!ni^7Z56GxIV_;^XxSDsOSv!4P1ma>4<0CU8BV!RWkOcsM CJTrO# literal 0 HcmV?d00001 diff --git a/alert_clients/ncurses_client/__pycache__/main.cpython-312.pyc b/alert_clients/ncurses_client/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..af87dfb2e8f88da9af263480f939211acfcbfbb9 GIT binary patch literal 927 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 self.snooze_until: + self.current_alarm = {"time": self.snooze_until.strftime("%H:%M:%S")} + self.snooze_until = None + + if not self.current_alarm: + self.refresh_alarms() + for alarm in self.alarms: + alarm_time = datetime.strptime(alarm["time"], "%H:%M:%S").time() + if (alarm_time.hour == current_time.hour and + alarm_time.minute == current_time.minute and + alarm["enabled"]): + self.current_alarm = alarm + break diff --git a/alert_clients/ncurses_client/main.py b/alert_clients/ncurses_client/main.py new file mode 100755 index 0000000..d97b417 --- /dev/null +++ b/alert_clients/ncurses_client/main.py @@ -0,0 +1,18 @@ +#!/usr/bin/python3 + +import curses +from ncurses_handlers import AlarmClock +from alarm_api_client.alarm_api_client import AlarmApiClient + +def main(stdscr): + """Main entry point for the ncurses alarm clock client.""" + try: + alarm_clock = AlarmClock(stdscr) + alarm_clock.run() + except Exception as e: + # Fallback error handling + curses.endwin() + print(f"An error occurred: {e}") + +if __name__ == "__main__": + curses.wrapper(main) diff --git a/alert_clients/ncurses_client/ncurses_handlers.py b/alert_clients/ncurses_client/ncurses_handlers.py new file mode 100644 index 0000000..a922a06 --- /dev/null +++ b/alert_clients/ncurses_client/ncurses_handlers.py @@ -0,0 +1,212 @@ +import curses +from datetime import datetime, date, timedelta +import time +from threading import Thread +import sys +import os +from pathlib import Path + +from alert_clients.alarm_api_client.alarm_api_client import AlarmApiClient +#from alarm_api_client.alarm_api_client import AlarmApiClient +from ncurses_ui import NcursesUI +from alert_logic import AlarmLogic +from ncurses_threads import NcursesThreads +from alarm_logger import AlarmLogger + +class AlarmClock: + def __init__(self, stdscr): + # Initialize logging first + self.logger = AlarmLogger() + + try: + self.stdscr = stdscr + self.api_client = AlarmApiClient("http://localhost:8000") + self.alarm_logic = AlarmLogic(self.api_client) + self.ncurses_ui = NcursesUI(stdscr) + self.ncurses_threads = NcursesThreads(self.alarm_logic) + + self.alarms = [] + self.running = True + self.selected_menu = 0 + self.new_alarm_hour = 0 + self.new_alarm_minute = 0 + self.new_alarm_selected = 0 + self.new_alarm_date = None + self.new_alarm_weekdays = [] + self.weekday_names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + self.current_alarm = None + self.current_alarm_process = False + self.snooze_minutes = 5 + self.snooze_until = None + + # Initialize curses + self.stdscr.keypad(1) + self.stdscr.timeout(100) + + self.logger.log_system_error("Alarm Clock Initialized Successfully") + except Exception as e: + self.logger.log_system_error(f"Initialization Error: {str(e)}", exc_info=True) + raise + + def show_error(self, message): + self.logger.log_system_error(message) + self.ncurses_ui.show_error(message) + + def handle_add_alarm_input(self, key): + try: + if key == 27: # Escape + self.selected_menu = 0 + self.logger.log_system_error("Add Alarm menu cancelled") + return + + if key == 10: # Enter + try: + alarm_details = { + "hour": self.new_alarm_hour, + "minute": self.new_alarm_minute, + "date": self.new_alarm_date, + "weekdays": self.new_alarm_weekdays if self.new_alarm_weekdays else None + } + self.alarm_logic.create_alarm( + self.new_alarm_hour, + self.new_alarm_minute, + self.new_alarm_date, + self.new_alarm_weekdays if self.new_alarm_weekdays else None + ) + self.logger.log_alarm_created(alarm_details) + self.alarms = self.api_client.get_alarms() + self.selected_menu = 0 + except Exception as e: + self.show_error(f"Failed to create alarm: {str(e)}") + return + + if key == curses.KEY_LEFT: + self.new_alarm_selected = (self.new_alarm_selected - 1) % 4 + self.logger.log_system_error(f"Selected field changed to {self.new_alarm_selected}") + elif key == curses.KEY_RIGHT: + self.new_alarm_selected = (self.new_alarm_selected + 1) % 4 + self.logger.log_system_error(f"Selected field changed to {self.new_alarm_selected}") + elif key == 32: # Space + if self.new_alarm_selected == 2: # Date + self.new_alarm_date = None + self.logger.log_system_error(f"Selected field changed to DATE") + elif self.new_alarm_selected == 3: # Weekdays + current_day = len(self.new_alarm_weekdays) + if current_day < 7: + if current_day in self.new_alarm_weekdays: + self.new_alarm_weekdays.remove(current_day) + else: + self.new_alarm_weekdays.append(current_day) + self.new_alarm_weekdays.sort() + + elif key == curses.KEY_UP: + if self.new_alarm_selected == 0: + self.new_alarm_hour = (self.new_alarm_hour + 1) % 24 + elif self.new_alarm_selected == 1: + self.new_alarm_minute = (self.new_alarm_minute + 1) % 60 + elif self.new_alarm_selected == 2: + if not self.new_alarm_date: + self.new_alarm_date = date.today() + else: + self.new_alarm_date += timedelta(days=1) + + elif key == curses.KEY_DOWN: + if self.new_alarm_selected == 0: + self.new_alarm_hour = (self.new_alarm_hour - 1) % 24 + elif self.new_alarm_selected == 1: + self.new_alarm_minute = (self.new_alarm_minute - 1) % 60 + elif self.new_alarm_selected == 2 and self.new_alarm_date: + self.new_alarm_date -= timedelta(days=1) + + except Exception as e: + self.logger.log_system_error(f"Error in add alarm input: {str(e)}", exc_info=True) + + def delete_selected_alarm(self): + try: + if self.alarms: + alarm = self.alarms[-1] # Get the last alarm + if self.api_client.delete_alarm(alarm["id"]): + self.logger.log_system_error(f"Deleted Alarm ID: {alarm['id']}") + self.alarm_logic.refresh_alarms() + else: + self.show_error("Failed to delete alarm") + except Exception as e: + self.logger.log_system_error(f"Delete alarm error: {str(e)}", exc_info=True) + self.show_error(f"Delete error: {str(e)}") + + def handle_list_alarms_input(self, key): + try: + if key == 27: # Escape + self.selected_menu = 0 + self.logger.log_system_error("List Alarms menu cancelled") + elif key == ord('d'): + self.logger.log_system_error("Attempting to delete last alarm") + self.delete_selected_alarm() + except Exception as e: + self.logger.log_system_error(f"Error in list alarms input: {str(e)}", exc_info=True) + + def draw_main_clock(self): + self.ncurses_ui.draw_main_clock() + + def draw_add_alarm(self): + self.ncurses_ui.draw_add_alarm() + + def draw_list_alarms(self): + self.ncurses_ui.draw_list_alarms() + + def run(self): + try: + # Start alarm checking thread + self.ncurses_threads.start_threads() + self.logger.log_system_error("Alarm checking threads started") + + while self.running: + try: + # Clear and redraw the screen + self.stdscr.erase() + + if self.selected_menu == 0: + self.draw_main_clock() + elif self.selected_menu == 1: + self.draw_add_alarm() + elif self.selected_menu == 2: + self.draw_list_alarms() + + self.stdscr.refresh() + + # Handle input with timeout + key = self.stdscr.getch() + if key != -1: # Key was pressed + if self.selected_menu == 0: + if key == ord('q'): + self.logger.log_system_error("Application quit requested") + self.alarm_logic.stop_alarm() + break + elif key == ord('a'): + self.selected_menu = 1 + self.logger.log_system_error("Switched to Add Alarm menu") + elif key == ord('l'): + self.selected_menu = 2 + self.logger.log_system_error("Switched to List Alarms menu") + elif key == ord('s'): + self.logger.log_system_error("Alarm stopped") + self.alarm_logic.stop_alarm() + elif key == ord('z'): + self.logger.log_alarm_snoozed( + "current_alarm", + self.snooze_minutes + ) + self.alarm_logic.snooze_alarm() + elif self.selected_menu == 1: + self.handle_add_alarm_input(key) + elif self.selected_menu == 2: + self.handle_list_alarms_input(key) + + except curses.error as e: + self.logger.log_system_error(f"Curses error: {str(e)}", exc_info=True) + self.running = False + raise Exception(f"Curses error: {str(e)}") + + except Exception as e: + self.logger.log_system_error(f"Unhandled error in main run loop: {str(e)}", exc_info=True) + raise diff --git a/alert_clients/ncurses_client/ncurses_threads.py b/alert_clients/ncurses_client/ncurses_threads.py new file mode 100644 index 0000000..1c231d1 --- /dev/null +++ b/alert_clients/ncurses_client/ncurses_threads.py @@ -0,0 +1,30 @@ +import time +from threading import Thread + +class NcursesThreads: + def __init__(self, alarm_logic): + self.alarm_logic = alarm_logic + self.running = True + + def alarm_check_thread(self): + """Thread for continuously checking alarms.""" + while self.running: + try: + self.alarm_logic.check_alarms() + time.sleep(1) + except Exception as e: + # Handle any errors during alarm checking + print(f"Alarm check error: {str(e)}") + time.sleep(5) + + def start_threads(self): + """Starts the necessary threads for ncurses client.""" + self.alarm_thread = Thread(target=self.alarm_check_thread) + self.alarm_thread.daemon = True + self.alarm_thread.start() + + def stop_threads(self): + """Stops all running threads.""" + self.running = False + if self.alarm_thread.is_alive(): + self.alarm_thread.join() diff --git a/alert_clients/ncurses_client/ncurses_ui.py b/alert_clients/ncurses_client/ncurses_ui.py new file mode 100644 index 0000000..ccf7c81 --- /dev/null +++ b/alert_clients/ncurses_client/ncurses_ui.py @@ -0,0 +1,166 @@ +import curses +import os +from datetime import datetime +from big_digits import BIG_DIGITS + +class NcursesUI: + def __init__(self, stdscr): + # Initialize curses + curses.start_color() + curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK) + curses.init_pair(2, curses.COLOR_YELLOW, curses.COLOR_BLACK) + curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK) + curses.curs_set(0) + self.stdscr.keypad(1) + self.stdscr.timeout(100) + + def show_error(self, message): + height, width = self.stdscr.getmaxyx() + self.stdscr.attron(curses.color_pair(3)) + self.stdscr.addstr(height-1, 0, f"ERROR: {message}"[:width-1]) + self.stdscr.attroff(curses.color_pair(3)) + self.stdscr.refresh() + time.sleep(2) + + def draw_big_digit(self, y, x, digit): + patterns = BIG_DIGITS[digit] + for i, line in enumerate(patterns): + self.stdscr.addstr(y + i, x, line) + + def draw_big_time(self, current_time): + height, width = self.stdscr.getmaxyx() + time_str = current_time.strftime("%H:%M:%S") + + # Calculate starting position to center the big clock + digit_width = 14 # Width of each digit pattern including spacing + total_width = digit_width * len(time_str) + start_x = (width - total_width) // 2 + start_y = (height - 7) // 2 - 4 # Move up a bit to make room for date + + self.stdscr.attron(curses.color_pair(1)) + for i, digit in enumerate(time_str): + self.draw_big_digit(start_y, start_x + i * digit_width, digit) + self.stdscr.attroff(curses.color_pair(1)) + + def draw_main_clock(self): + current_time = datetime.now() + time_str = current_time.strftime("%H:%M:%S") + date_str = current_time.strftime("%Y-%m-%d") + + # Get terminal dimensions + height, width = self.stdscr.getmaxyx() + + # Draw big time + self.draw_big_time(current_time) + + # Draw date + date_x = width // 2 - len(date_str) // 2 + date_y = height // 2 + 4 # Below the big clock + + self.stdscr.attron(curses.color_pair(2)) + self.stdscr.addstr(date_y, date_x, date_str) + self.stdscr.attroff(curses.color_pair(2)) + + # Draw menu options + menu_str = "A: Add Alarm L: List Alarms S: Stop Z: Snooze Q: Quit" + menu_x = width // 2 - len(menu_str) // 2 + self.stdscr.addstr(height - 2, menu_x, menu_str) + + # Show alarm/snooze status + if self.current_alarm_process: + status_str = "⏰ ALARM ACTIVE - Press 'S' to stop or 'Z' to snooze" + status_x = width // 2 - len(status_str) // 2 + self.stdscr.attron(curses.color_pair(3)) + self.stdscr.addstr(height - 4, status_x, status_str) + self.stdscr.attroff(curses.color_pair(3)) + elif self.snooze_until: + snooze_str = f"💤 Snoozed until {self.snooze_until.strftime('%H:%M')}" + snooze_x = width // 2 - len(snooze_str) // 2 + self.stdscr.attron(curses.color_pair(2)) + self.stdscr.addstr(height - 4, snooze_x, snooze_str) + self.stdscr.attroff(curses.color_pair(2)) + + def draw_add_alarm(self): + height, width = self.stdscr.getmaxyx() + + form_y = height // 2 - 3 + self.stdscr.addstr(form_y, width // 2 - 10, "New Alarm") + + # Time selection + hour_str = f"{self.new_alarm_hour:02d}" + minute_str = f"{self.new_alarm_minute:02d}" + + # Highlight selected field + if self.new_alarm_selected == 0: + self.stdscr.attron(curses.color_pair(2)) + self.stdscr.addstr(form_y + 1, width // 2 - 2, hour_str) + if self.new_alarm_selected == 0: + self.stdscr.attroff(curses.color_pair(2)) + + self.stdscr.addstr(form_y + 1, width // 2, ":") + + if self.new_alarm_selected == 1: + self.stdscr.attron(curses.color_pair(2)) + self.stdscr.addstr(form_y + 1, width // 2 + 1, minute_str) + if self.new_alarm_selected == 1: + self.stdscr.attroff(curses.color_pair(2)) + + # Date selection + date_str = "No specific date" if not self.new_alarm_date else self.new_alarm_date.strftime("%Y-%m-%d") + if self.new_alarm_selected == 2: + self.stdscr.attron(curses.color_pair(2)) + self.stdscr.addstr(form_y + 3, width // 2 - len(date_str) // 2, date_str) + if self.new_alarm_selected == 2: + self.stdscr.attroff(curses.color_pair(2)) + + # Weekday selection + weekday_str = "Repeat: " + " ".join( + self.weekday_names[i] if i in self.new_alarm_weekdays else "___" + for i in range(7) + ) + if self.new_alarm_selected == 3: + self.stdscr.attron(curses.color_pair(2)) + self.stdscr.addstr(form_y + 4, width // 2 - len(weekday_str) // 2, weekday_str) + if self.new_alarm_selected == 3: + self.stdscr.attroff(curses.color_pair(2)) + + # Instructions + if self.new_alarm_selected < 2: + self.stdscr.addstr(height - 2, 2, "↑↓: Change value ←→: Switch field Enter: Save Esc: Cancel") + elif self.new_alarm_selected == 2: + self.stdscr.addstr(height - 2, 2, "↑↓: Change date Space: Clear date Enter: Save Esc: Cancel") + else: # weekday selection + self.stdscr.addstr(height - 2, 2, "←→: Select day Space: Toggle Enter: Save Esc: Cancel") + + def draw_list_alarms(self): + height, width = self.stdscr.getmaxyx() + + self.stdscr.addstr(2, width // 2 - 5, "Alarms") + + try: + self.alarms = self.api_client.get_alarms() + for i, alarm in enumerate(self.alarms): + # Parse time from the time string + time_parts = alarm['time'].split(':') + time_str = f"{time_parts[0]}:{time_parts[1]}" + + # Add repeat rule information + if 'repeat_rule' in alarm: + if alarm['repeat_rule']['type'] == 'once' and 'date' in alarm['repeat_rule']: + time_str += f" on {alarm['repeat_rule']['date']}" + elif alarm['repeat_rule']['type'] == 'weekly' and 'days' in alarm['repeat_rule']: + weekdays = [self.weekday_names[d] for d in alarm['repeat_rule']['days']] + time_str += f" every {', '.join(weekdays)}" + + status = "✓" if alarm['enabled'] else "✗" + display_str = f"{status} {time_str}" + + self.stdscr.addstr(4 + i, width // 2 - len(display_str) // 2, display_str) + + if not self.alarms: + self.stdscr.addstr(4, width // 2 - 7, "No alarms set") + + except Exception as e: + self.show_error(f"Failed to display alarms: {str(e)}") + + self.stdscr.addstr(height - 2, 2, "D: Delete alarm Esc: Back")