From 18329f01287ae223afe481e253ac0ca6b284dc2f Mon Sep 17 00:00:00 2001 From: pdxlocations Date: Fri, 25 Jul 2025 00:18:51 -0700 Subject: [PATCH] init --- contact/ui/contact_ui.py | 1 - contact/ui/control_ui.py | 22 ++++++----- contact/ui/dialog.py | 29 ++++++++------ contact/utilities/input_handlers.py | 60 ++++++++++++++++++++++++----- 4 files changed, 80 insertions(+), 32 deletions(-) diff --git a/contact/ui/contact_ui.py b/contact/ui/contact_ui.py index 319c835..7ce41c0 100644 --- a/contact/ui/contact_ui.py +++ b/contact/ui/contact_ui.py @@ -335,7 +335,6 @@ def handle_ctrl_t(stdscr: curses.window) -> None: send_traceroute() curses.curs_set(0) # Hide cursor contact.ui.dialog.dialog( - stdscr, f"Traceroute Sent To: {get_name_from_database(ui_state.node_list[ui_state.selected_node])}", "Results will appear in messages window.\nNote: Traceroute is limited to once every 30 seconds.", ) diff --git a/contact/ui/control_ui.py b/contact/ui/control_ui.py index a5045ee..d3f43d2 100644 --- a/contact/ui/control_ui.py +++ b/contact/ui/control_ui.py @@ -268,7 +268,7 @@ def settings_menu(stdscr: object, interface: object) -> None: break elif selected_option == "Export Config File": - filename = get_text_input("Enter a filename for the config file") + filename = get_text_input("Enter a filename for the config file", None) if not filename: logging.info("Export aborted: No filename provided.") menu_state.start_index.pop() @@ -290,7 +290,7 @@ def settings_menu(stdscr: object, interface: object) -> None: with open(yaml_file_path, "w", encoding="utf-8") as file: file.write(config_text) logging.info(f"Config file saved to {yaml_file_path}") - dialog(stdscr, "Config File Saved:", yaml_file_path) + dialog("Config File Saved:", yaml_file_path) menu_state.start_index.pop() continue except PermissionError: @@ -306,14 +306,14 @@ def settings_menu(stdscr: object, interface: object) -> None: # Check if folder exists and is not empty if not os.path.exists(config_folder) or not any(os.listdir(config_folder)): - dialog(stdscr, "", " No config files found. Export a config first.") + dialog("", " No config files found. Export a config first.") continue # Return to menu file_list = [f for f in os.listdir(config_folder) if os.path.isfile(os.path.join(config_folder, f))] # Ensure file_list is not empty before proceeding if not file_list: - dialog(stdscr, "", " No config files found. Export a config first.") + dialog("", " No config files found. Export a config first.") continue filename = get_list_input("Choose a config file", None, file_list) @@ -327,7 +327,7 @@ def settings_menu(stdscr: object, interface: object) -> None: elif selected_option == "Config URL": current_value = interface.localNode.getURL() - new_value = get_text_input(f"Config URL is currently: {current_value}") + new_value = get_text_input(f"Config URL is currently: {current_value}", None) if new_value is not None: current_value = new_value overwrite = get_list_input(f"Are you sure you want to load this config?", None, ["Yes", "No"]) @@ -395,7 +395,9 @@ def settings_menu(stdscr: object, interface: object) -> None: if selected_option in ["longName", "shortName", "isLicensed"]: if selected_option in ["longName", "shortName"]: - new_value = get_text_input(f"{human_readable_name} is currently: {current_value}") + new_value = get_text_input( + f"{human_readable_name} is currently: {current_value}", selected_option + ) new_value = current_value if new_value is None else new_value menu_state.current_menu[selected_option] = (field, new_value) @@ -414,7 +416,7 @@ def settings_menu(stdscr: object, interface: object) -> None: menu_state.start_index.pop() elif selected_option in ["latitude", "longitude", "altitude"]: - new_value = get_text_input(f"{human_readable_name} is currently: {current_value}") + new_value = get_text_input(f"{human_readable_name} is currently: {current_value}", selected_option) new_value = current_value if new_value is None else new_value menu_state.current_menu[selected_option] = (field, new_value) @@ -453,17 +455,17 @@ def settings_menu(stdscr: object, interface: object) -> None: menu_state.start_index.pop() elif field.type == 13: # Field type 13 corresponds to UINT32 - new_value = get_text_input(f"{human_readable_name} is currently: {current_value}") + new_value = get_text_input(f"{human_readable_name} is currently: {current_value}", selected_option) new_value = current_value if new_value is None else int(new_value) menu_state.start_index.pop() elif field.type == 2: # Field type 13 corresponds to INT64 - new_value = get_text_input(f"{human_readable_name} is currently: {current_value}") + new_value = get_text_input(f"{human_readable_name} is currently: {current_value}", selected_option) new_value = current_value if new_value is None else float(new_value) menu_state.start_index.pop() else: # Handle other field types - new_value = get_text_input(f"{human_readable_name} is currently: {current_value}") + new_value = get_text_input(f"{human_readable_name} is currently: {current_value}", selected_option) new_value = current_value if new_value is None else new_value menu_state.start_index.pop() diff --git a/contact/ui/dialog.py b/contact/ui/dialog.py index 06f93e5..3dd09ce 100644 --- a/contact/ui/dialog.py +++ b/contact/ui/dialog.py @@ -2,14 +2,13 @@ import curses from contact.ui.colors import get_color -def dialog(stdscr: curses.window, title: str, message: str) -> None: - height, width = stdscr.getmaxyx() +def dialog(title: str, message: str) -> None: + + height, width = curses.LINES, curses.COLS # Calculate dialog dimensions - max_line_lengh = 0 message_lines = message.splitlines() - for l in message_lines: - max_line_length = max(len(l), max_line_lengh) + max_line_length = max(len(l) for l in message_lines) dialog_width = max(len(title) + 4, max_line_length + 4) dialog_height = len(message_lines) + 4 x = (width - dialog_width) // 2 @@ -24,12 +23,19 @@ def dialog(stdscr: curses.window, title: str, message: str) -> None: # Add title win.addstr(0, 2, title, get_color("settings_default")) - # Add message - for i, l in enumerate(message_lines): - win.addstr(2 + i, 2, l, get_color("settings_default")) + # Add message (centered) + for i, line in enumerate(message_lines): + msg_x = (dialog_width - len(line)) // 2 + win.addstr(2 + i, msg_x, line, get_color("settings_default")) - # Add button - win.addstr(dialog_height - 2, (dialog_width - 4) // 2, " Ok ", get_color("settings_default", reverse=True)) + # Add centered OK button + ok_text = " Ok " + win.addstr( + dialog_height - 2, + (dialog_width - len(ok_text)) // 2, + ok_text, + get_color("settings_default", reverse=True), + ) # Refresh dialog window win.refresh() @@ -37,8 +43,7 @@ def dialog(stdscr: curses.window, title: str, message: str) -> None: # Get user input while True: char = win.getch() - # Close dialog with enter, space, or esc - if char in (curses.KEY_ENTER, 10, 13, 32, 27): + if char in (curses.KEY_ENTER, 10, 13, 32, 27): # Enter, space, or Esc win.erase() win.refresh() return diff --git a/contact/utilities/input_handlers.py b/contact/utilities/input_handlers.py index a4b3994..af446b2 100644 --- a/contact/utilities/input_handlers.py +++ b/contact/utilities/input_handlers.py @@ -6,9 +6,10 @@ from typing import Any, Optional, List from contact.ui.colors import get_color from contact.ui.nav_utils import move_highlight, draw_arrows, wrap_text +from contact.ui.dialog import dialog -def get_text_input(prompt: str) -> Optional[str]: +def get_text_input(prompt: str, selected_config: str) -> Optional[str]: """Handles user input with wrapped text for long prompts.""" height = 8 width = 80 @@ -39,7 +40,22 @@ def get_text_input(prompt: str) -> Optional[str]: input_win.refresh() curses.curs_set(1) - max_length = 4 if "shortName" in prompt else None + max_length = None + fixed_length = None + input_type = str + + if selected_config: + if "shortName" in selected_config: + max_length = 4 + elif "longName" in selected_config: + max_length = 32 + elif "fixed_pin" in selected_config: + fixed_length = 6 + max_length = fixed_length # enforce fixed length + input_type = int + elif "adc_multiplier_override" in selected_config: + input_type = float + user_input = "" # Start user input after the prompt text @@ -55,18 +71,44 @@ def get_text_input(prompt: str) -> Optional[str]: curses.curs_set(0) return None # Exit without saving - elif key in (chr(curses.KEY_ENTER), chr(10), chr(13)): # Enter key - break + elif key in (chr(curses.KEY_ENTER), chr(10), chr(13)): + if fixed_length and len(user_input) != fixed_length: + curses.curs_set(0) + dialog(input_win, "Error", f"Value must be exactly {fixed_length} characters long.") + curses.curs_set(1) + elif input_type is int and not user_input.isdigit(): + curses.curs_set(0) + dialog(input_win, "Error", "Only numeric digits (0–9) allowed.") + curses.curs_set(1) + elif input_type is float: + try: + float(user_input) + except ValueError: + curses.curs_set(0) + dialog(input_win, "Error", "Must be a valid floating point number.") + curses.curs_set(1) + else: + break + else: + break elif key in (curses.KEY_BACKSPACE, chr(127)): # Handle Backspace if user_input: user_input = user_input[:-1] # Remove last character - elif max_length is None or len(user_input) < max_length: # Enforce max length - if isinstance(key, str): - user_input += key - else: - user_input += chr(key) + elif max_length is None or len(user_input) < max_length: + try: + char = chr(key) if not isinstance(key, str) else key + if input_type is int: + if char.isdigit(): + user_input += char + elif input_type is float: + if char.isdigit() or (char == "." and "." not in user_input): + user_input += char + else: + user_input += char + except ValueError: + pass # Ignore invalid input # First line must be manually handled before using wrap_text() first_line = user_input[:first_line_width] # Cut to max first line width