diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..da9568b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.formatOnSave": true + } +} \ No newline at end of file diff --git a/contact/ui/control_ui.py b/contact/ui/control_ui.py index 8b2b38b..6f312a8 100644 --- a/contact/ui/control_ui.py +++ b/contact/ui/control_ui.py @@ -7,7 +7,13 @@ import sys from contact.utilities.save_to_radio import save_changes from contact.utilities.config_io import config_export, config_import -from contact.utilities.input_handlers import get_repeated_input, get_text_input, get_fixed32_input, get_list_input, get_admin_key_input +from contact.utilities.input_handlers import ( + get_repeated_input, + get_text_input, + get_fixed32_input, + get_list_input, + get_admin_key_input, +) from contact.ui.menus import generate_menu_from_protobuf from contact.ui.colors import get_color from contact.ui.dialog import dialog @@ -42,14 +48,17 @@ field_mapping, help_text = parse_ini_file(translation_file) Segment = tuple[str, str, bool, bool] WrappedLine = list[Segment] -def display_menu(menu_state: MenuState) -> tuple[object, object]: # curses.window or pad types + +def display_menu( + menu_state: MenuState, +) -> tuple[object, object]: # curses.window or pad types min_help_window_height = 6 num_items = len(menu_state.current_menu) + (1 if menu_state.show_save_option else 0) # Determine the available height for the menu - max_menu_height = curses.LINES - menu_height = min(max_menu_height - min_help_window_height, num_items + 5) + max_menu_height = curses.LINES + menu_height = min(max_menu_height - min_help_window_height, num_items + 5) start_y = (curses.LINES - menu_height) // 2 - (min_help_window_height // 2) start_x = (curses.COLS - width) // 2 @@ -70,7 +79,7 @@ def display_menu(menu_state: MenuState) -> tuple[object, object]: # curses.wind header = " > ".join(word.title() for word in menu_state.menu_path) if len(header) > width - 4: - header = header[:width - 7] + "..." + header = header[: width - 7] + "..." menu_win.addstr(1, 2, header, get_color("settings_breadcrumbs", bold=True)) transformed_path = transform_menu_path(menu_state.menu_path) @@ -78,35 +87,65 @@ def display_menu(menu_state: MenuState) -> tuple[object, object]: # curses.wind for idx, option in enumerate(menu_state.current_menu): field_info = menu_state.current_menu[option] current_value = field_info[1] if isinstance(field_info, tuple) else "" - full_key = '.'.join(transformed_path + [option]) + full_key = ".".join(transformed_path + [option]) display_name = field_mapping.get(full_key, option) - display_option = f"{display_name}"[:width // 2 - 2] - display_value = f"{current_value}"[:width // 2 - 4] + display_option = f"{display_name}"[: width // 2 - 2] + display_value = f"{current_value}"[: width // 2 - 4] try: - color = get_color("settings_sensitive" if option in sensitive_settings else "settings_default", reverse=(idx == menu_state.selected_index)) - menu_pad.addstr(idx, 0, f"{display_option:<{width // 2 - 2}} {display_value}".ljust(width - 8), color) + color = get_color( + ( + "settings_sensitive" + if option in sensitive_settings + else "settings_default" + ), + reverse=(idx == menu_state.selected_index), + ) + menu_pad.addstr( + idx, + 0, + f"{display_option:<{width // 2 - 2}} {display_value}".ljust(width - 8), + color, + ) except curses.error: pass if menu_state.show_save_option: save_position = menu_height - 2 - menu_win.addstr(save_position, (width - len(save_option)) // 2, save_option, get_color("settings_save", reverse=(menu_state.selected_index == len(menu_state.current_menu)))) + menu_win.addstr( + save_position, + (width - len(save_option)) // 2, + save_option, + get_color( + "settings_save", + reverse=(menu_state.selected_index == len(menu_state.current_menu)), + ), + ) # Draw help window with dynamically updated max_help_lines - draw_help_window(start_y, start_x, menu_height, max_help_lines, transformed_path, menu_state) + draw_help_window( + start_y, start_x, menu_height, max_help_lines, transformed_path, menu_state + ) menu_win.refresh() menu_pad.refresh( - menu_state.start_index[-1], 0, - menu_win.getbegyx()[0] + 3, menu_win.getbegyx()[1] + 4, - menu_win.getbegyx()[0] + 3 + menu_win.getmaxyx()[0] - 5 - (2 if menu_state.show_save_option else 0), - menu_win.getbegyx()[1] + menu_win.getmaxyx()[1] - 4 + menu_state.start_index[-1], + 0, + menu_win.getbegyx()[0] + 3, + menu_win.getbegyx()[1] + 4, + menu_win.getbegyx()[0] + + 3 + + menu_win.getmaxyx()[0] + - 5 + - (2 if menu_state.show_save_option else 0), + menu_win.getbegyx()[1] + menu_win.getmaxyx()[1] - 4, ) max_index = num_items + (1 if menu_state.show_save_option else 0) - 1 - visible_height = menu_win.getmaxyx()[0] - 5 - (2 if menu_state.show_save_option else 0) + visible_height = ( + menu_win.getmaxyx()[0] - 5 - (2 if menu_state.show_save_option else 0) + ) draw_arrows(menu_win, visible_height, max_index, menu_state) @@ -119,18 +158,32 @@ def draw_help_window( menu_height: int, max_help_lines: int, transformed_path: list[str], - menu_state: MenuState + menu_state: MenuState, ) -> None: global help_win - if 'help_win' not in globals(): + if "help_win" not in globals(): help_win = None # Initialize if it does not exist - selected_option = list(menu_state.current_menu.keys())[menu_state.selected_index] if menu_state.current_menu else None + selected_option = ( + list(menu_state.current_menu.keys())[menu_state.selected_index] + if menu_state.current_menu + else None + ) help_y = menu_start_y + menu_height - help_win = update_help_window(help_win, help_text, transformed_path, selected_option, max_help_lines, width, help_y, menu_start_x) + help_win = update_help_window( + help_win, + help_text, + transformed_path, + selected_option, + max_help_lines, + width, + help_y, + menu_start_x, + ) + def update_help_window( help_win: object, # curses window or None @@ -140,11 +193,12 @@ def update_help_window( max_help_lines: int, width: int, help_y: int, - help_x: int + help_x: int, ) -> object: # returns a curses window - """Handles rendering the help window consistently.""" - wrapped_help = get_wrapped_help_text(help_text, transformed_path, selected_option, width, max_help_lines) + wrapped_help = get_wrapped_help_text( + help_text, transformed_path, selected_option, width, max_help_lines + ) help_height = min(len(wrapped_help) + 2, max_help_lines + 2) # +2 for border help_height = max(help_height, 3) # Ensure at least 3 rows (1 text + border) @@ -185,24 +239,33 @@ def get_wrapped_help_text( transformed_path: list[str], selected_option: str | None, width: int, - max_lines: int + max_lines: int, ) -> list[WrappedLine]: """Fetches and formats help text for display, ensuring it fits within the allowed lines.""" - - full_help_key = '.'.join(transformed_path + [selected_option]) if selected_option else None + + full_help_key = ( + ".".join(transformed_path + [selected_option]) if selected_option else None + ) help_content = help_text.get(full_help_key, "No help available.") wrap_width = max(width - 6, 10) # Ensure a valid wrapping width # Color replacements color_mappings = { - r'\[warning\](.*?)\[/warning\]': ('settings_warning', True, False), # Red for warnings - r'\[note\](.*?)\[/note\]': ('settings_note', True, False), # Green for notes - r'\[underline\](.*?)\[/underline\]': ('settings_default', False, True), # Underline - - r'\\033\[31m(.*?)\\033\[0m': ('settings_warning', True, False), # Red text - r'\\033\[32m(.*?)\\033\[0m': ('settings_note', True, False), # Green text - r'\\033\[4m(.*?)\\033\[0m': ('settings_default', False, True) # Underline + r"\[warning\](.*?)\[/warning\]": ( + "settings_warning", + True, + False, + ), # Red for warnings + r"\[note\](.*?)\[/note\]": ("settings_note", True, False), # Green for notes + r"\[underline\](.*?)\[/underline\]": ( + "settings_default", + False, + True, + ), # Underline + r"\\033\[31m(.*?)\\033\[0m": ("settings_warning", True, False), # Red text + r"\\033\[32m(.*?)\\033\[0m": ("settings_note", True, False), # Green text + r"\\033\[4m(.*?)\\033\[0m": ("settings_default", False, True), # Underline } def extract_ansi_segments(text: str) -> list[Segment]: @@ -214,7 +277,9 @@ def get_wrapped_help_text( # Find all matches and store their positions for pattern, (color, bold, underline) in color_mappings.items(): for match in re.finditer(pattern, text): - pattern_matches.append((match.start(), match.end(), match.group(1), color, bold, underline)) + pattern_matches.append( + (match.start(), match.end(), match.group(1), color, bold, underline) + ) # Sort matches by start position to process sequentially pattern_matches.sort(key=lambda x: x[0]) @@ -224,7 +289,7 @@ def get_wrapped_help_text( if last_pos < start: segment = text[last_pos:start] matches.append((segment, "settings_default", False, False)) - + # Append the colored segment matches.append((content, color, bold, underline)) last_pos = end @@ -242,7 +307,7 @@ def get_wrapped_help_text( line_length = 0 for text, color, bold, underline in segments: - words = re.findall(r'\S+|\s+', text) # Capture words and spaces separately + words = re.findall(r"\S+|\s+", text) # Capture words and spaces separately for word in words: word_length = len(word) @@ -272,8 +337,8 @@ def get_wrapped_help_text( # Trim and add ellipsis if needed if len(wrapped_help) > max_lines: - wrapped_help = wrapped_help[:max_lines] - wrapped_help[-1].append(("...", "settings_default", False, False)) + wrapped_help = wrapped_help[:max_lines] + wrapped_help[-1].append(("...", "settings_default", False, False)) return wrapped_help @@ -288,7 +353,7 @@ def get_wrapped_help_text( # max_help_lines: int, # menu_state: MenuState # ) -> None: - + # if old_idx == menu_state.selected_index: # No-op # return @@ -320,11 +385,11 @@ def get_wrapped_help_text( # menu_pad.chgat(menu_state.selected_index, 0, menu_pad.getmaxyx()[1], get_color("settings_sensitive", reverse=True) if options[menu_state.selected_index] in sensitive_settings else get_color("settings_default", reverse=True)) # menu_win.refresh() - + # # Refresh pad only if scrolling is needed # menu_pad.refresh(menu_state.start_index[-1], 0, # menu_win.getbegyx()[0] + 3, menu_win.getbegyx()[1] + 4, -# menu_win.getbegyx()[0] + 3 + visible_height, +# menu_win.getbegyx()[0] + 3 + visible_height, # menu_win.getbegyx()[1] + menu_win.getmaxyx()[1] - 4) # # Update help window @@ -337,14 +402,11 @@ def get_wrapped_help_text( def draw_arrows( - win: object, - visible_height: int, - max_index: int, - menu_state: MenuState + win: object, visible_height: int, max_index: int, menu_state: MenuState ) -> None: # vh = visible_height + (1 if show_save_option else 0) - mi = max_index - (2 if menu_state.show_save_option else 0) + mi = max_index - (2 if menu_state.show_save_option else 0) if visible_height < mi: if menu_state.start_index[-1] > 0: @@ -352,11 +414,13 @@ def draw_arrows( else: win.addstr(3, 2, " ", get_color("settings_default")) - if mi - menu_state.start_index[-1] >= visible_height + (0 if menu_state.show_save_option else 1) : + if mi - menu_state.start_index[-1] >= visible_height + ( + 0 if menu_state.show_save_option else 1 + ): win.addstr(visible_height + 3, 2, "▼", get_color("settings_default")) else: win.addstr(visible_height + 3, 2, " ", get_color("settings_default")) - + def settings_menu(stdscr: object, interface: object) -> None: curses.update_lines_cols() @@ -365,22 +429,31 @@ def settings_menu(stdscr: object, interface: object) -> None: menu_state.current_menu = menu["Main Menu"] menu_state.menu_path = ["Main Menu"] - modified_settings = {} - + need_redraw = True menu_state.show_save_option = False while True: - if(need_redraw): + if need_redraw: options = list(menu_state.current_menu.keys()) menu_state.show_save_option = ( - len(menu_state.menu_path) > 2 and ("Radio Settings" in menu_state.menu_path or "Module Settings" in menu_state.menu_path) - ) or ( - len(menu_state.menu_path) == 2 and "User Settings" in menu_state.menu_path - ) or ( - len(menu_state.menu_path) == 3 and "Channels" in menu_state.menu_path + ( + len(menu_state.menu_path) > 2 + and ( + "Radio Settings" in menu_state.menu_path + or "Module Settings" in menu_state.menu_path + ) + ) + or ( + len(menu_state.menu_path) == 2 + and "User Settings" in menu_state.menu_path + ) + or ( + len(menu_state.menu_path) == 3 + and "Channels" in menu_state.menu_path + ) ) # Display the menu @@ -396,9 +469,17 @@ def settings_menu(stdscr: object, interface: object) -> None: if key == curses.KEY_UP: old_idx = menu_state.selected_index - menu_state.selected_index = max_index if menu_state.selected_index == 0 else menu_state.selected_index - 1 + menu_state.selected_index = ( + max_index + if menu_state.selected_index == 0 + else menu_state.selected_index - 1 + ) move_highlight( - old_idx, menu_state.selected_index, options, menu_win, menu_pad, + old_idx, + menu_state.selected_index, + options, + menu_win, + menu_pad, start_index_ref=menu_state.start_index[-1:], selected_index=menu_state.selected_index, show_save=menu_state.show_save_option, @@ -407,14 +488,22 @@ def settings_menu(stdscr: object, interface: object) -> None: field_mapping=help_text, menu_path=transform_menu_path(menu_state.menu_path), max_help_lines=max_help_lines, - sensitive_mode=True + sensitive_mode=True, ) - + elif key == curses.KEY_DOWN: old_idx = menu_state.selected_index - menu_state.selected_index = 0 if menu_state.selected_index == max_index else menu_state.selected_index + 1 + menu_state.selected_index = ( + 0 + if menu_state.selected_index == max_index + else menu_state.selected_index + 1 + ) move_highlight( - old_idx, menu_state.selected_index, options, menu_win, menu_pad, + old_idx, + menu_state.selected_index, + options, + menu_win, + menu_pad, start_index_ref=menu_state.start_index[-1:], selected_index=menu_state.selected_index, show_save=menu_state.show_save_option, @@ -423,7 +512,7 @@ def settings_menu(stdscr: object, interface: object) -> None: field_mapping=help_text, menu_path=transform_menu_path(menu_state.menu_path), max_help_lines=max_help_lines, - sensitive_mode=True + sensitive_mode=True, ) elif key == curses.KEY_RESIZE: @@ -440,7 +529,11 @@ def settings_menu(stdscr: object, interface: object) -> None: old_idx = menu_state.selected_index menu_state.selected_index = max_index move_highlight( - old_idx, menu_state.selected_index, options, menu_win, menu_pad, + old_idx, + menu_state.selected_index, + options, + menu_win, + menu_pad, start_index_ref=menu_state.start_index[-1:], selected_index=menu_state.selected_index, show_save=menu_state.show_save_option, @@ -449,9 +542,9 @@ def settings_menu(stdscr: object, interface: object) -> None: field_mapping=help_text, menu_path=transform_menu_path(menu_state.menu_path), max_help_lines=max_help_lines, - sensitive_mode=True + sensitive_mode=True, ) - elif key == curses.KEY_RIGHT or key == ord('\n'): + elif key == curses.KEY_RIGHT or key == ord("\n"): need_redraw = True menu_state.start_index.append(0) menu_win.erase() @@ -462,7 +555,9 @@ def settings_menu(stdscr: object, interface: object) -> None: menu_win.refresh() help_win.refresh() - if menu_state.show_save_option and menu_state.selected_index == len(options): + if menu_state.show_save_option and menu_state.selected_index == len( + options + ): save_changes(interface, modified_settings, menu_state) modified_settings.clear() logging.info("Changes Saved") @@ -494,9 +589,15 @@ def settings_menu(stdscr: object, interface: object) -> None: yaml_file_path = os.path.join(config_folder, filename) if os.path.exists(yaml_file_path): - overwrite = get_list_input(f"{filename} already exists. Overwrite?", None, ["Yes", "No"]) + overwrite = get_list_input( + f"{filename} already exists. Overwrite?", + None, + ["Yes", "No"], + ) if overwrite == "No": - logging.info("Export cancelled: User chose not to overwrite.") + logging.info( + "Export cancelled: User chose not to overwrite." + ) menu_state.start_index.pop() continue # Return to menu os.makedirs(os.path.dirname(yaml_file_path), exist_ok=True) @@ -507,22 +608,30 @@ def settings_menu(stdscr: object, interface: object) -> None: menu_state.start_index.pop() continue except PermissionError: - logging.error(f"Permission denied: Unable to write to {yaml_file_path}") + logging.error( + f"Permission denied: Unable to write to {yaml_file_path}" + ) except OSError as e: logging.error(f"OS error while saving config: {e}") except Exception as e: logging.error(f"Unexpected error: {e}") menu_state.start_index.pop() continue - + elif selected_option == "Load Config File": # Check if folder exists and is not empty - if not os.path.exists(config_folder) or not any(os.listdir(config_folder)): + if not os.path.exists(config_folder) or not any( + os.listdir(config_folder) + ): dialog(stdscr, "", " 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))] + 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: @@ -532,7 +641,11 @@ def settings_menu(stdscr: object, interface: object) -> None: filename = get_list_input("Choose a config file", None, file_list) if filename: file_path = os.path.join(config_folder, filename) - overwrite = get_list_input(f"Are you sure you want to load {filename}?", None, ["Yes", "No"]) + overwrite = get_list_input( + f"Are you sure you want to load {filename}?", + None, + ["Yes", "No"], + ) if overwrite == "Yes": config_import(interface, file_path) menu_state.start_index.pop() @@ -543,7 +656,11 @@ def settings_menu(stdscr: object, interface: object) -> None: new_value = get_text_input(f"Config URL is currently: {current_value}") 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"]) + overwrite = get_list_input( + f"Are you sure you want to load this config?", + None, + ["Yes", "No"], + ) if overwrite == "Yes": interface.localNode.setURL(new_value) logging.info(f"New Config URL sent to node") @@ -551,7 +668,9 @@ def settings_menu(stdscr: object, interface: object) -> None: continue elif selected_option == "Reboot": - confirmation = get_list_input("Are you sure you want to Reboot?", None, ["Yes", "No"]) + confirmation = get_list_input( + "Are you sure you want to Reboot?", None, ["Yes", "No"] + ) if confirmation == "Yes": interface.localNode.reboot() logging.info(f"Node Reboot Requested by menu") @@ -559,7 +678,9 @@ def settings_menu(stdscr: object, interface: object) -> None: continue elif selected_option == "Reset Node DB": - confirmation = get_list_input("Are you sure you want to Reset Node DB?", None, ["Yes", "No"]) + confirmation = get_list_input( + "Are you sure you want to Reset Node DB?", None, ["Yes", "No"] + ) if confirmation == "Yes": interface.localNode.resetNodeDb() logging.info(f"Node DB Reset Requested by menu") @@ -567,7 +688,9 @@ def settings_menu(stdscr: object, interface: object) -> None: continue elif selected_option == "Shutdown": - confirmation = get_list_input("Are you sure you want to Shutdown?", None, ["Yes", "No"]) + confirmation = get_list_input( + "Are you sure you want to Shutdown?", None, ["Yes", "No"] + ) if confirmation == "Yes": interface.localNode.shutdown() logging.info(f"Node Shutdown Requested by menu") @@ -575,7 +698,9 @@ def settings_menu(stdscr: object, interface: object) -> None: continue elif selected_option == "Factory Reset": - confirmation = get_list_input("Are you sure you want to Factory Reset?", None, ["Yes", "No"]) + confirmation = get_list_input( + "Are you sure you want to Factory Reset?", None, ["Yes", "No"] + ) if confirmation == "Yes": interface.localNode.factoryReset() logging.info(f"Factory Reset Requested by menu") @@ -594,26 +719,32 @@ def settings_menu(stdscr: object, interface: object) -> None: menu_state.selected_index = 4 continue # need_redraw = True - + field_info = menu_state.current_menu.get(selected_option) if isinstance(field_info, tuple): field, current_value = field_info # Transform the menu path to get the full key transformed_path = transform_menu_path(menu_state.menu_path) - full_key = '.'.join(transformed_path + [selected_option]) + full_key = ".".join(transformed_path + [selected_option]) # Fetch human-readable name from field_mapping human_readable_name = field_mapping.get(full_key, selected_option) - 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}") + 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 = current_value if new_value is None else new_value menu_state.current_menu[selected_option] = (field, new_value) - elif selected_option == 'isLicensed': - new_value = get_list_input(f"{human_readable_name} is currently: {current_value}", str(current_value), ["True", "False"]) + elif selected_option == "isLicensed": + new_value = get_list_input( + f"{human_readable_name} is currently: {current_value}", + str(current_value), + ["True", "False"], + ) new_value = new_value == "True" menu_state.current_menu[selected_option] = (field, new_value) @@ -622,60 +753,82 @@ 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}") + elif selected_option in ["latitude", "longitude", "altitude"]: + new_value = get_text_input( + f"{human_readable_name} is currently: {current_value}" + ) new_value = current_value if new_value is None else new_value menu_state.current_menu[selected_option] = (field, new_value) - for option in ['latitude', 'longitude', 'altitude']: + for option in ["latitude", "longitude", "altitude"]: if option in menu_state.current_menu: - modified_settings[option] = menu_state.current_menu[option][1] + modified_settings[option] = menu_state.current_menu[option][ + 1 + ] menu_state.start_index.pop() elif selected_option == "admin_key": new_values = get_admin_key_input(current_value) - new_value = current_value if new_values is None else [base64.b64decode(key) for key in new_values] + new_value = ( + current_value + if new_values is None + else [base64.b64decode(key) for key in new_values] + ) menu_state.start_index.pop() elif field.type == 8: # Handle boolean type - new_value = get_list_input(human_readable_name, str(current_value), ["True", "False"]) + new_value = get_list_input( + human_readable_name, str(current_value), ["True", "False"] + ) if new_value == "Not Set": pass # Leave it as-is else: new_value = new_value == "True" or new_value is True menu_state.start_index.pop() - elif field.label == field.LABEL_REPEATED: # Handle repeated field - Not currently used + elif ( + field.label == field.LABEL_REPEATED + ): # Handle repeated field - Not currently used new_value = get_repeated_input(current_value) - new_value = current_value if new_value is None else new_value.split(", ") + new_value = ( + current_value if new_value is None else new_value.split(", ") + ) menu_state.start_index.pop() elif field.enum_type: # Enum field enum_options = {v.name: v.number for v in field.enum_type.values} - new_value_name = get_list_input(human_readable_name, current_value, list(enum_options.keys())) + new_value_name = get_list_input( + human_readable_name, current_value, list(enum_options.keys()) + ) new_value = enum_options.get(new_value_name, current_value) menu_state.start_index.pop() - elif field.type == 7: # Field type 7 corresponds to FIXED32 + elif field.type == 7: # Field type 7 corresponds to FIXED32 new_value = get_fixed32_input(current_value) 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}") + 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 = 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}") + 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 = 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}" + ) new_value = current_value if new_value is None else new_value menu_state.start_index.pop() - + for key in menu_state.menu_path[3:]: # Skip "Main Menu" modified_settings = modified_settings.setdefault(key, {}) @@ -684,8 +837,14 @@ def settings_menu(stdscr: object, interface: object) -> None: # Convert enum string to int if field and field.enum_type: - enum_value_descriptor = field.enum_type.values_by_number.get(new_value) - new_value = enum_value_descriptor.name if enum_value_descriptor else new_value + enum_value_descriptor = field.enum_type.values_by_number.get( + new_value + ) + new_value = ( + enum_value_descriptor.name + if enum_value_descriptor + else new_value + ) menu_state.current_menu[selected_option] = (field, new_value) else: @@ -694,7 +853,6 @@ def settings_menu(stdscr: object, interface: object) -> None: menu_state.menu_index.append(menu_state.selected_index) menu_state.selected_index = 0 - elif key == curses.KEY_LEFT: need_redraw = True @@ -718,14 +876,15 @@ def settings_menu(stdscr: object, interface: object) -> None: menu_state.current_menu = menu_state.current_menu.get(step, {}) menu_state.selected_index = menu_state.menu_index.pop() menu_state.start_index.pop() - + elif key == 27: # Escape key menu_win.erase() menu_win.refresh() break + def set_region(interface: object) -> None: - node = interface.getNode('^local') + node = interface.getNode("^local") device_config = node.localConfig lora_descriptor = device_config.lora.DESCRIPTOR @@ -735,10 +894,12 @@ def set_region(interface: object) -> None: regions = list(region_name_to_number.keys()) - new_region_name = get_list_input('Select your region:', 'UNSET', regions) + new_region_name = get_list_input("Select your region:", "UNSET", regions) # Convert region name to corresponding enum number - new_region_number = region_name_to_number.get(new_region_name, 0) # Default to 0 if not found + new_region_number = region_name_to_number.get( + new_region_name, 0 + ) # Default to 0 if not found node.localConfig.lora.region = new_region_number - node.writeConfig("lora") \ No newline at end of file + node.writeConfig("lora") diff --git a/contact/ui/navigation_utils.py b/contact/ui/navigation_utils.py index 2a05ec2..f8caf3d 100644 --- a/contact/ui/navigation_utils.py +++ b/contact/ui/navigation_utils.py @@ -2,7 +2,10 @@ import curses from contact.ui.colors import get_color -from contact.ui.user_config import save_option, sensitive_settings + +save_option = "Save Changes" +sensitive_settings = ["Reboot", "Reset Node DB", "Shutdown", "Factory Reset"] + def move_highlight( old_idx: int, @@ -20,7 +23,7 @@ def move_highlight( field_mapping=None, menu_path: list[str] = None, width: int = 80, - sensitive_mode: bool = False + sensitive_mode: bool = False, ): if old_idx == new_idx: return @@ -36,32 +39,82 @@ def move_highlight( start_index_ref[0] = new_idx elif new_idx >= start_index_ref[0] + visible_height: start_index_ref[0] = new_idx - visible_height - start_index_ref[0] = max(0, min(start_index_ref[0], max_index - visible_height + 1)) + start_index_ref[0] = max( + 0, min(start_index_ref[0], max_index - visible_height + 1) + ) scroll = start_index_ref[0] if start_index_ref else 0 # Clear previous highlight if show_save and old_idx == max_index: - win.chgat(win.getmaxyx()[0] - 2, (width - len(save_option)) // 2, len(save_option), get_color("settings_save")) + win.chgat( + win.getmaxyx()[0] - 2, + (width - len(save_option)) // 2, + len(save_option), + get_color("settings_save"), + ) else: - color = "settings_sensitive" if sensitive_mode and options[old_idx] in sensitive_settings else "settings_default" + color = ( + "settings_sensitive" + if sensitive_mode and options[old_idx] in sensitive_settings + else "settings_default" + ) pad.chgat(old_idx, 0, pad.getmaxyx()[1], get_color(color)) # Apply new highlight if show_save and new_idx == max_index: - win.chgat(win.getmaxyx()[0] - 2, (width - len(save_option)) // 2, len(save_option), get_color("settings_save", reverse=True)) + win.chgat( + win.getmaxyx()[0] - 2, + (width - len(save_option)) // 2, + len(save_option), + get_color("settings_save", reverse=True), + ) else: - color = "settings_sensitive" if sensitive_mode and options[new_idx] in sensitive_settings else "settings_default" + color = ( + "settings_sensitive" + if sensitive_mode and options[new_idx] in sensitive_settings + else "settings_default" + ) pad.chgat(new_idx, 0, pad.getmaxyx()[1], get_color(color, reverse=True)) win.refresh() - pad.refresh(scroll, 0, - win.getbegyx()[0] + 3, win.getbegyx()[1] + 4, - win.getbegyx()[0] + 3 + visible_height, - win.getbegyx()[1] + win.getmaxyx()[1] - 4) + pad.refresh( + scroll, + 0, + win.getbegyx()[0] + 3, + win.getbegyx()[1] + 4, + win.getbegyx()[0] + 3 + visible_height, + win.getbegyx()[1] + win.getmaxyx()[1] - 4, + ) # Optional help update if help_win and help_updater and menu_path and selected_index is not None: - selected_option = options[selected_index] if selected_index < len(options) else None + selected_option = ( + options[selected_index] if selected_index < len(options) else None + ) help_y = win.getbegyx()[0] + win.getmaxyx()[0] - help_updater(help_win, field_mapping, menu_path, selected_option, max_help_lines, width, help_y, win.getbegyx()[1]) \ No newline at end of file + help_updater( + help_win, + field_mapping, + menu_path, + selected_option, + max_help_lines, + width, + help_y, + win.getbegyx()[1], + ) + + +def draw_arrows( + win: curses.window, visible_height: int, max_index: int, start_index: int +) -> None: + if visible_height < max_index: + if start_index > 0: + win.addstr(3, 2, "▲", get_color("settings_default")) + else: + win.addstr(3, 2, " ", get_color("settings_default")) + + if max_index - start_index > visible_height: + win.addstr(visible_height + 3, 2, "▼", get_color("settings_default")) + else: + win.addstr(visible_height + 3, 2, " ", get_color("settings_default")) diff --git a/contact/ui/user_config.py b/contact/ui/user_config.py index 31c4ded..0c3a362 100644 --- a/contact/ui/user_config.py +++ b/contact/ui/user_config.py @@ -11,16 +11,22 @@ width = 80 save_option = "Save Changes" sensitive_settings = [] + def edit_color_pair(key: str, current_value: list[str]) -> list[str]: """ Allows the user to select a foreground and background color for a key. """ color_list = [" "] + list(COLOR_MAP.keys()) - fg_color = get_list_input(f"Select Foreground Color for {key}", current_value[0], color_list) - bg_color = get_list_input(f"Select Background Color for {key}", current_value[1], color_list) + fg_color = get_list_input( + f"Select Foreground Color for {key}", current_value[0], color_list + ) + bg_color = get_list_input( + f"Select Background Color for {key}", current_value[1], color_list + ) return [fg_color, bg_color] + def edit_value(key: str, current_value: str) -> str: height = 10 @@ -39,9 +45,14 @@ def edit_value(key: str, current_value: str) -> str: edit_win.addstr(3, 2, "Current Value:", get_color("settings_default")) wrap_width = width - 4 # Account for border and padding - wrapped_lines = [current_value[i:i+wrap_width] for i in range(0, len(current_value), wrap_width)] + wrapped_lines = [ + current_value[i : i + wrap_width] + for i in range(0, len(current_value), wrap_width) + ] - for i, line in enumerate(wrapped_lines[:4]): # Limit display to fit the window height + for i, line in enumerate( + wrapped_lines[:4] + ): # Limit display to fit the window height edit_win.addstr(4 + i, 2, line, get_color("settings_default")) edit_win.refresh() @@ -49,10 +60,14 @@ def edit_value(key: str, current_value: str) -> str: # Handle theme selection dynamically if key == "theme": # Load theme names dynamically from the JSON - theme_options = [k.split("_", 2)[2].lower() for k in loaded_config.keys() if k.startswith("COLOR_CONFIG")] + theme_options = [ + k.split("_", 2)[2].lower() + for k in loaded_config.keys() + if k.startswith("COLOR_CONFIG") + ] return get_list_input("Select Theme", current_value, theme_options) elif key == "node_sort": - sort_options = ['lastHeard', 'name', 'hops'] + sort_options = ["lastHeard", "name", "hops"] return get_list_input("Sort By", current_value, sort_options) # Standard Input Mode (Scrollable) @@ -64,18 +79,26 @@ def edit_value(key: str, current_value: str) -> str: input_position = (7, 13) # Tuple for row and column row, col = input_position # Unpack tuple while True: - visible_text = user_input[scroll_offset:scroll_offset + input_width] # Only show what fits - edit_win.addstr(row, col, " " * input_width, get_color("settings_default")) # Clear previous text - edit_win.addstr(row, col, visible_text, get_color("settings_default")) # Display text + visible_text = user_input[ + scroll_offset : scroll_offset + input_width + ] # Only show what fits + edit_win.addstr( + row, col, " " * input_width, get_color("settings_default") + ) # Clear previous text + edit_win.addstr( + row, col, visible_text, get_color("settings_default") + ) # Display text edit_win.refresh() - edit_win.move(row, col + min(len(user_input) - scroll_offset, input_width)) # Adjust cursor position + edit_win.move( + row, col + min(len(user_input) - scroll_offset, input_width) + ) # Adjust cursor position key = edit_win.get_wch() if key in (chr(27), curses.KEY_LEFT): # ESC or Left Arrow curses.curs_set(0) return current_value # Exit without returning a value - + elif key in (chr(curses.KEY_ENTER), chr(10), chr(13)): break @@ -83,7 +106,9 @@ def edit_value(key: str, current_value: str) -> str: if user_input: # Only process if there's something to delete user_input = user_input[:-1] if scroll_offset > 0 and len(user_input) < scroll_offset + input_width: - scroll_offset -= 1 # Move back if text is shorter than scrolled area + scroll_offset -= ( + 1 # Move back if text is shorter than scrolled area + ) else: if isinstance(key, str): user_input += key @@ -113,7 +138,7 @@ def display_menu(menu_state: Any) -> tuple[Any, Any, list[str]]: # Calculate dynamic dimensions for the menu max_menu_height = curses.LINES - menu_height = min(max_menu_height, num_items + 5) + menu_height = min(max_menu_height, num_items + 5) num_items = len(options) start_y = (curses.LINES - menu_height) // 2 start_x = (curses.COLS - width) // 2 @@ -133,35 +158,60 @@ def display_menu(menu_state: Any) -> tuple[Any, Any, list[str]]: # Display the menu path header = " > ".join(menu_state.menu_path) if len(header) > width - 4: - header = header[:width - 7] + "..." + header = header[: width - 7] + "..." menu_win.addstr(1, 2, header, get_color("settings_breadcrumbs", bold=True)) # Populate the pad with menu options for idx, key in enumerate(options): - value = menu_state.current_menu[key] if isinstance(menu_state.current_menu, dict) else menu_state.current_menu[int(key.strip("[]"))] - display_key = f"{key}"[:width // 2 - 2] - display_value = ( - f"{value}"[:width // 2 - 8] + value = ( + menu_state.current_menu[key] + if isinstance(menu_state.current_menu, dict) + else menu_state.current_menu[int(key.strip("[]"))] ) + display_key = f"{key}"[: width // 2 - 2] + display_value = f"{value}"[: width // 2 - 8] - color = get_color("settings_default", reverse=(idx == menu_state.selected_index)) - menu_pad.addstr(idx, 0, f"{display_key:<{width // 2 - 2}} {display_value}".ljust(width - 8), color) + color = get_color( + "settings_default", reverse=(idx == menu_state.selected_index) + ) + menu_pad.addstr( + idx, + 0, + f"{display_key:<{width // 2 - 2}} {display_value}".ljust(width - 8), + color, + ) # Add Save button to the main window if menu_state.show_save_option: save_position = menu_height - 2 - menu_win.addstr(save_position, (width - len(save_option)) // 2, save_option, get_color("settings_save", reverse=(menu_state.selected_index == len(menu_state.current_menu)))) + menu_win.addstr( + save_position, + (width - len(save_option)) // 2, + save_option, + get_color( + "settings_save", + reverse=(menu_state.selected_index == len(menu_state.current_menu)), + ), + ) menu_win.refresh() menu_pad.refresh( - menu_state.start_index[-1], 0, - menu_win.getbegyx()[0] + 3, menu_win.getbegyx()[1] + 4, - menu_win.getbegyx()[0] + 3 + menu_win.getmaxyx()[0] - 5 - (2 if menu_state.show_save_option else 0), - menu_win.getbegyx()[1] + menu_win.getmaxyx()[1] - 4 + menu_state.start_index[-1], + 0, + menu_win.getbegyx()[0] + 3, + menu_win.getbegyx()[1] + 4, + menu_win.getbegyx()[0] + + 3 + + menu_win.getmaxyx()[0] + - 5 + - (2 if menu_state.show_save_option else 0), + menu_win.getbegyx()[1] + menu_win.getmaxyx()[1] - 4, ) max_index = num_items + (1 if menu_state.show_save_option else 0) - 1 - visible_height = menu_win.getmaxyx()[0] - 5 - (2 if menu_state.show_save_option else 0) + visible_height = ( + menu_win.getmaxyx()[0] - 5 - (2 if menu_state.show_save_option else 0) + ) draw_arrows(menu_win, visible_height, max_index, menu_state) @@ -175,7 +225,7 @@ def display_menu(menu_state: Any) -> tuple[Any, Any, list[str]]: # menu_pad: curses.window, # menu_state: Any # ) -> None: - + # if old_idx == menu_state.selected_index: # No-op # return @@ -207,24 +257,21 @@ def display_menu(menu_state: Any) -> tuple[Any, Any, list[str]]: # menu_pad.chgat(menu_state.selected_index, 0, menu_pad.getmaxyx()[1], get_color("settings_sensitive", reverse=True) if options[menu_state.selected_index] in sensitive_settings else get_color("settings_default", reverse=True)) # menu_win.refresh() - + # # Refresh pad only if scrolling is needed # menu_pad.refresh(menu_state.start_index[-1], 0, # menu_win.getbegyx()[0] + 3, menu_win.getbegyx()[1] + 4, -# menu_win.getbegyx()[0] + 3 + visible_height, +# menu_win.getbegyx()[0] + 3 + visible_height, # menu_win.getbegyx()[1] + menu_win.getmaxyx()[1] - 4) # draw_arrows(menu_win, visible_height, max_index, menu_state) def draw_arrows( - win: curses.window, - visible_height: int, - max_index: int, - menu_state: any + win: curses.window, visible_height: int, max_index: int, menu_state: any ) -> None: - mi = max_index - (2 if menu_state.show_save_option else 0) + mi = max_index - (2 if menu_state.show_save_option else 0) if visible_height < mi: if menu_state.start_index[-1] > 0: @@ -232,7 +279,9 @@ def draw_arrows( else: win.addstr(3, 2, " ", get_color("settings_default")) - if mi - menu_state.start_index[-1] >= visible_height + (0 if menu_state.show_save_option else 1) : + if mi - menu_state.start_index[-1] >= visible_height + ( + 0 if menu_state.show_save_option else 1 + ): win.addstr(visible_height + 3, 2, "▼", get_color("settings_default")) else: win.addstr(visible_height + 3, 2, " ", get_color("settings_default")) @@ -265,47 +314,67 @@ def json_editor(stdscr: curses.window, menu_state: Any) -> None: need_redraw = True while True: - if(need_redraw): + if need_redraw: menu_win, menu_pad, options = display_menu(menu_state) menu_win.refresh() need_redraw = False - + max_index = len(options) + (1 if menu_state.show_save_option else 0) - 1 key = menu_win.getch() if key == curses.KEY_UP: old_idx = menu_state.selected_index - menu_state.selected_index = max_index if menu_state.selected_index == 0 else menu_state.selected_index - 1 + menu_state.selected_index = ( + max_index + if menu_state.selected_index == 0 + else menu_state.selected_index - 1 + ) move_highlight( - old_idx, menu_state.selected_index, options, menu_win, menu_pad, + old_idx, + menu_state.selected_index, + options, + menu_win, + menu_pad, start_index_ref=menu_state.start_index[-1:], selected_index=menu_state.selected_index, show_save=menu_state.show_save_option, - sensitive_mode=True + sensitive_mode=True, ) elif key == curses.KEY_DOWN: old_idx = menu_state.selected_index - menu_state.selected_index = 0 if menu_state.selected_index == max_index else menu_state.selected_index + 1 + menu_state.selected_index = ( + 0 + if menu_state.selected_index == max_index + else menu_state.selected_index + 1 + ) move_highlight( - old_idx, menu_state.selected_index, options, menu_win, menu_pad, + old_idx, + menu_state.selected_index, + options, + menu_win, + menu_pad, start_index_ref=menu_state.start_index[-1:], selected_index=menu_state.selected_index, show_save=menu_state.show_save_option, - sensitive_mode=True + sensitive_mode=True, ) elif key == ord("\t") and menu_state.show_save_option: old_idx = menu_state.selected_index menu_state.selected_index = max_index move_highlight( - old_idx, menu_state.selected_index, options, menu_win, menu_pad, + old_idx, + menu_state.selected_index, + options, + menu_win, + menu_pad, start_index_ref=menu_state.start_index[-1:], selected_index=menu_state.selected_index, show_save=menu_state.show_save_option, - sensitive_mode=True + sensitive_mode=True, ) elif key in (curses.KEY_RIGHT, 10, 13): # 10 = \n, 13 = carriage return @@ -314,12 +383,14 @@ def json_editor(stdscr: curses.window, menu_state: Any) -> None: menu_win.erase() menu_win.refresh() - if menu_state.selected_index < len(options): # Handle selection of a menu item + if menu_state.selected_index < len( + options + ): # Handle selection of a menu item selected_key = options[menu_state.selected_index] menu_state.menu_path.append(str(selected_key)) menu_state.start_index.append(0) menu_state.menu_index.append(menu_state.selected_index) - + # Handle nested data if isinstance(menu_state.current_menu, dict): if selected_key in menu_state.current_menu: @@ -327,7 +398,9 @@ def json_editor(stdscr: curses.window, menu_state: Any) -> None: else: continue # Skip invalid key elif isinstance(menu_state.current_menu, list): - selected_data = menu_state.current_menu[int(selected_key.strip("[]"))] + selected_data = menu_state.current_menu[ + int(selected_key.strip("[]")) + ] if isinstance(selected_data, list) and len(selected_data) == 2: # Edit color pair @@ -350,7 +423,7 @@ def json_editor(stdscr: curses.window, menu_state: Any) -> None: menu_state.start_index.pop() menu_state.current_menu[selected_key] = new_value need_redraw = True - + else: # Save button selected save_json(file_path, data) @@ -372,13 +445,17 @@ def json_editor(stdscr: curses.window, menu_state: Any) -> None: menu_state.current_menu = data for path in menu_state.menu_path[2:]: - menu_state.current_menu = menu_state.current_menu[path] if isinstance(menu_state.current_menu, dict) else menu_state.current_menu[int(path.strip("[]"))] + menu_state.current_menu = ( + menu_state.current_menu[path] + if isinstance(menu_state.current_menu, dict) + else menu_state.current_menu[int(path.strip("[]"))] + ) else: # Exit the editor menu_win.clear() menu_win.refresh() - + break @@ -388,6 +465,7 @@ def save_json(file_path: str, data: dict[str, Any]) -> None: f.write(formatted_json) setup_colors(reinit=True) + def main(stdscr: curses.window) -> None: from contact.ui.ui_state import MenuState @@ -400,5 +478,6 @@ def main(stdscr: curses.window) -> None: setup_colors() json_editor(stdscr, menu_state) + if __name__ == "__main__": - curses.wrapper(main) \ No newline at end of file + curses.wrapper(main) diff --git a/contact/utilities/input_handlers.py b/contact/utilities/input_handlers.py index fa8595b..d73aa66 100644 --- a/contact/utilities/input_handlers.py +++ b/contact/utilities/input_handlers.py @@ -7,9 +7,10 @@ from typing import Any, Optional from contact.ui.colors import get_color from contact.ui.navigation_utils import move_highlight + def wrap_text(text: str, wrap_width: int) -> list[str]: """Wraps text while preserving spaces and breaking long words.""" - words = re.findall(r'\S+|\s+', text) # Capture words and spaces separately + words = re.findall(r"\S+|\s+", text) # Capture words and spaces separately wrapped_lines = [] line_buffer = "" line_length = 0 @@ -25,7 +26,7 @@ def wrap_text(text: str, wrap_width: int) -> list[str]: line_buffer = "" line_length = 0 for i in range(0, word_length, wrap_width): - wrapped_lines.append(word[i:i+wrap_width]) + wrapped_lines.append(word[i : i + wrap_width]) continue if line_length + word_length > wrap_width and word.strip(): @@ -40,7 +41,7 @@ def wrap_text(text: str, wrap_width: int) -> list[str]: wrapped_lines.append(line_buffer) return wrapped_lines - + def get_text_input(prompt: str) -> Optional[str]: """Handles user input with wrapped text for long prompts.""" @@ -62,14 +63,16 @@ def get_text_input(prompt: str) -> Optional[str]: wrapped_prompt = wrap_text(prompt, wrap_width=input_width) row = 1 for line in wrapped_prompt: - input_win.addstr(row, margin, line[:input_width], get_color("settings_default", bold=True)) + input_win.addstr( + row, margin, line[:input_width], get_color("settings_default", bold=True) + ) row += 1 if row >= height - 3: # Prevent overflow break prompt_text = "Enter new value: " input_win.addstr(row + 1, margin, prompt_text, get_color("settings_default")) - + input_win.refresh() curses.curs_set(1) @@ -106,21 +109,35 @@ def get_text_input(prompt: str) -> Optional[str]: first_line = user_input[:first_line_width] # Cut to max first line width remaining_text = user_input[first_line_width:] # Remaining text for wrapping - wrapped_lines = wrap_text(remaining_text, wrap_width=input_width) if remaining_text else [] - + wrapped_lines = ( + wrap_text(remaining_text, wrap_width=input_width) if remaining_text else [] + ) + # Clear only the input area (without touching prompt text) for i in range(max_input_rows): if row + 1 + i < height - 1: - input_win.addstr(row + 1 + i, margin, " " * min(input_width, width - margin - 1), get_color("settings_default")) + input_win.addstr( + row + 1 + i, + margin, + " " * min(input_width, width - margin - 1), + get_color("settings_default"), + ) # Redraw the prompt text so it never disappears input_win.addstr(row + 1, margin, prompt_text, get_color("settings_default")) # Redraw wrapped input - input_win.addstr(row + 1, col_start, first_line, get_color("settings_default")) # First line next to prompt + input_win.addstr( + row + 1, col_start, first_line, get_color("settings_default") + ) # First line next to prompt for i, line in enumerate(wrapped_lines): if row + 2 + i < height - 1: - input_win.addstr(row + 2 + i, margin, line[:input_width], get_color("settings_default")) + input_win.addstr( + row + 2 + i, + margin, + line[:input_width], + get_color("settings_default"), + ) input_win.refresh() @@ -165,55 +182,75 @@ def get_admin_key_input(current_value: list[bytes]) -> Optional[list[str]]: while True: repeated_win.erase() repeated_win.border() - repeated_win.addstr(1, 2, "Edit up to 3 Admin Keys:", get_color("settings_default", bold=True)) + repeated_win.addstr( + 1, 2, "Edit up to 3 Admin Keys:", get_color("settings_default", bold=True) + ) # Display current values, allowing editing for i, line in enumerate(user_values): prefix = "→ " if i == cursor_pos else " " # Highlight the current line - repeated_win.addstr(3 + i, 2, f"{prefix}Admin Key {i + 1}: ", get_color("settings_default", bold=(i == cursor_pos))) + repeated_win.addstr( + 3 + i, + 2, + f"{prefix}Admin Key {i + 1}: ", + get_color("settings_default", bold=(i == cursor_pos)), + ) repeated_win.addstr(3 + i, 18, line) # Align text for easier editing # Move cursor to the correct position inside the field curses.curs_set(1) - repeated_win.move(3 + cursor_pos, 18 + len(user_values[cursor_pos])) # Position cursor at end of text + repeated_win.move( + 3 + cursor_pos, 18 + len(user_values[cursor_pos]) + ) # Position cursor at end of text # Show error message if needed if error_message: - repeated_win.addstr(7, 2, error_message, get_color("settings_default", bold=True)) + repeated_win.addstr( + 7, 2, error_message, get_color("settings_default", bold=True) + ) repeated_win.refresh() key = repeated_win.getch() - if key == 27 or key == curses.KEY_LEFT: # Escape or Left Arrow -> Cancel and return original + if ( + key == 27 or key == curses.KEY_LEFT + ): # Escape or Left Arrow -> Cancel and return original repeated_win.erase() repeated_win.refresh() curses.noecho() curses.curs_set(0) return None - - elif key == ord('\n'): # Enter key to save and return - if all(is_valid_base64(val) for val in user_values): # Ensure all values are valid Base64 and 32 bytes + + elif key == ord("\n"): # Enter key to save and return + if all( + is_valid_base64(val) for val in user_values + ): # Ensure all values are valid Base64 and 32 bytes curses.noecho() curses.curs_set(0) return user_values # Return the edited Base64 values else: - error_message = "Error: Each key must be valid Base64 and 32 bytes long!" + error_message = ( + "Error: Each key must be valid Base64 and 32 bytes long!" + ) elif key == curses.KEY_UP: # Move cursor up cursor_pos = (cursor_pos - 1) % len(user_values) elif key == curses.KEY_DOWN: # Move cursor down cursor_pos = (cursor_pos + 1) % len(user_values) elif key == curses.KEY_BACKSPACE or key == 127: # Backspace key if len(user_values[cursor_pos]) > 0: - user_values[cursor_pos] = user_values[cursor_pos][:-1] # Remove last character + user_values[cursor_pos] = user_values[cursor_pos][ + :-1 + ] # Remove last character else: try: - user_values[cursor_pos] += chr(key) # Append valid character input to the selected field + user_values[cursor_pos] += chr( + key + ) # Append valid character input to the selected field error_message = "" # Clear error if user starts fixing input except ValueError: pass # Ignore invalid character inputs - def get_repeated_input(current_value: list[str]) -> Optional[str]: height = 9 width = 80 @@ -236,33 +273,46 @@ def get_repeated_input(current_value: list[str]) -> Optional[str]: while True: repeated_win.erase() repeated_win.border() - repeated_win.addstr(1, 2, "Edit up to 3 Values:", get_color("settings_default", bold=True)) + repeated_win.addstr( + 1, 2, "Edit up to 3 Values:", get_color("settings_default", bold=True) + ) # Display current values, allowing editing for i, line in enumerate(user_values): prefix = "→ " if i == cursor_pos else " " # Highlight the current line - repeated_win.addstr(3 + i, 2, f"{prefix}Value{i + 1}: ", get_color("settings_default", bold=(i == cursor_pos))) - repeated_win.addstr(3 + i, 18, line) + repeated_win.addstr( + 3 + i, + 2, + f"{prefix}Value{i + 1}: ", + get_color("settings_default", bold=(i == cursor_pos)), + ) + repeated_win.addstr(3 + i, 18, line) # Move cursor to the correct position inside the field curses.curs_set(1) - repeated_win.move(3 + cursor_pos, 18 + len(user_values[cursor_pos])) # Position cursor at end of text + repeated_win.move( + 3 + cursor_pos, 18 + len(user_values[cursor_pos]) + ) # Position cursor at end of text # Show error message if needed if error_message: - repeated_win.addstr(7, 2, error_message, get_color("settings_default", bold=True)) + repeated_win.addstr( + 7, 2, error_message, get_color("settings_default", bold=True) + ) repeated_win.refresh() key = repeated_win.getch() - if key == 27 or key == curses.KEY_LEFT: # Escape or Left Arrow -> Cancel and return original + if ( + key == 27 or key == curses.KEY_LEFT + ): # Escape or Left Arrow -> Cancel and return original repeated_win.erase() repeated_win.refresh() curses.noecho() curses.curs_set(0) return None - - elif key == ord('\n'): # Enter key to save and return + + elif key == ord("\n"): # Enter key to save and return curses.noecho() curses.curs_set(0) return ", ".join(user_values) @@ -272,10 +322,14 @@ def get_repeated_input(current_value: list[str]) -> Optional[str]: cursor_pos = (cursor_pos + 1) % len(user_values) elif key == curses.KEY_BACKSPACE or key == 127: # Backspace key if len(user_values[cursor_pos]) > 0: - user_values[cursor_pos] = user_values[cursor_pos][:-1] # Remove last character + user_values[cursor_pos] = user_values[cursor_pos][ + :-1 + ] # Remove last character else: try: - user_values[cursor_pos] += chr(key) # Append valid character input to the selected field + user_values[cursor_pos] += chr( + key + ) # Append valid character input to the selected field error_message = "" # Clear error if user starts fixing input except ValueError: pass # Ignore invalid character inputs @@ -301,7 +355,9 @@ def get_fixed32_input(current_value: int) -> int: while True: fixed32_win.erase() fixed32_win.border() - fixed32_win.addstr(1, 2, "Enter an IP address (xxx.xxx.xxx.xxx):", curses.A_BOLD) + fixed32_win.addstr( + 1, 2, "Enter an IP address (xxx.xxx.xxx.xxx):", curses.A_BOLD + ) fixed32_win.addstr(3, 2, f"Current: {current_value}") fixed32_win.addstr(5, 2, f"New value: {user_input}") fixed32_win.refresh() @@ -314,16 +370,23 @@ def get_fixed32_input(current_value: int) -> int: curses.noecho() curses.curs_set(0) return cvalue # Return the current value unchanged - elif key == ord('\n'): # Enter key to validate and save + elif key == ord("\n"): # Enter key to validate and save # Validate IP address octets = user_input.split(".") - if len(octets) == 4 and all(octet.isdigit() and 0 <= int(octet) <= 255 for octet in octets): + if len(octets) == 4 and all( + octet.isdigit() and 0 <= int(octet) <= 255 for octet in octets + ): curses.noecho() curses.curs_set(0) fixed32_address = ipaddress.ip_address(user_input) return int(fixed32_address) # Return the valid IP address else: - fixed32_win.addstr(7, 2, "Invalid IP address. Try again.", curses.A_BOLD | curses.color_pair(5)) + fixed32_win.addstr( + 7, + 2, + "Invalid IP address. Try again.", + curses.A_BOLD | curses.color_pair(5), + ) fixed32_win.refresh() curses.napms(1500) # Wait for 1.5 seconds before refreshing user_input = "" # Clear invalid input @@ -338,11 +401,16 @@ def get_fixed32_input(current_value: int) -> int: pass # Ignore invalid inputs -def get_list_input(prompt: str, current_option: Optional[str], list_options: list[str]) -> Optional[str]: +def get_list_input( + prompt: str, current_option: Optional[str], list_options: list[str] +) -> Optional[str]: """ Displays a scrollable list of list_options for the user to choose from. """ - selected_index = list_options.index(current_option) if current_option in list_options else 0 + selected_index = ( + list_options.index(current_option) if current_option in list_options else 0 + ) + scroll_offset = 0 height = min(len(list_options) + 5, curses.LINES) width = 80 @@ -365,16 +433,28 @@ def get_list_input(prompt: str, current_option: Optional[str], list_options: lis # Render options on the pad for idx, color in enumerate(list_options): if idx == selected_index: - list_pad.addstr(idx, 0, color.ljust(width - 8), get_color("settings_default", reverse=True)) + list_pad.addstr( + idx, + 0, + color.ljust(width - 8), + get_color("settings_default", reverse=True), + ) else: - list_pad.addstr(idx, 0, color.ljust(width - 8), get_color("settings_default")) + list_pad.addstr( + idx, 0, color.ljust(width - 8), get_color("settings_default") + ) # Initial refresh list_win.refresh() - list_pad.refresh(0, 0, - list_win.getbegyx()[0] + 3, list_win.getbegyx()[1] + 4, - list_win.getbegyx()[0] + list_win.getmaxyx()[0] - 2, list_win.getbegyx()[1] + list_win.getmaxyx()[1] - 4) - + list_pad.refresh( + 0, + 0, + list_win.getbegyx()[0] + 3, + list_win.getbegyx()[1] + 4, + list_win.getbegyx()[0] + list_win.getmaxyx()[0] - 2, + list_win.getbegyx()[1] + list_win.getmaxyx()[1] - 4, + ) + max_index = len(list_options) - 1 visible_height = list_win.getmaxyx()[0] - 5 @@ -388,8 +468,12 @@ def get_list_input(prompt: str, current_option: Optional[str], list_options: lis selected_index = max(0, selected_index - 1) scroll_ref = [scroll_offset] move_highlight( - old_idx, selected_index, list_options, list_win, list_pad, - start_index_ref=scroll_ref + old_idx, + selected_index, + list_options, + list_win, + list_pad, + start_index_ref=scroll_ref, ) scroll_offset = scroll_ref[0] elif key == curses.KEY_DOWN: @@ -397,11 +481,15 @@ def get_list_input(prompt: str, current_option: Optional[str], list_options: lis selected_index = min(len(list_options) - 1, selected_index + 1) scroll_ref = [scroll_offset] move_highlight( - old_idx, selected_index, list_options, list_win, list_pad, - start_index_ref=scroll_ref + old_idx, + selected_index, + list_options, + list_win, + list_pad, + start_index_ref=scroll_ref, ) scroll_offset = scroll_ref[0] - elif key == ord('\n'): # Enter key + elif key == ord("\n"): # Enter key list_win.clear() list_win.refresh() return list_options[selected_index] @@ -445,23 +533,20 @@ def get_list_input(prompt: str, current_option: Optional[str], list_options: lis # list_pad.chgat(new_idx, 0, list_pad.getmaxyx()[1], get_color("settings_default", reverse=True)) # list_win.refresh() - + # # Refresh pad only if scrolling is needed # list_pad.refresh(scroll_offset, 0, # list_win.getbegyx()[0] + 3, list_win.getbegyx()[1] + 4, -# list_win.getbegyx()[0] + 3 + visible_height, +# list_win.getbegyx()[0] + 3 + visible_height, # list_win.getbegyx()[1] + list_win.getmaxyx()[1] - 4) - + # draw_arrows(list_win, visible_height, max_index, scroll_offset) # return scroll_offset # Return updated scroll_offset to be stored externally def draw_arrows( - win: curses.window, - visible_height: int, - max_index: int, - start_index: int + win: curses.window, visible_height: int, max_index: int, start_index: int ) -> None: if visible_height < max_index: @@ -474,4 +559,3 @@ def draw_arrows( win.addstr(visible_height + 3, 2, "▼", get_color("settings_default")) else: win.addstr(visible_height + 3, 2, " ", get_color("settings_default")) - \ No newline at end of file