From 8c3e00b52b7ff846f353926f8605855df6ad15e2 Mon Sep 17 00:00:00 2001 From: pdxlocations Date: Thu, 31 Jul 2025 23:44:14 -0700 Subject: [PATCH] redraw other input types --- contact/utilities/input_handlers.py | 202 +++++++++++++++++----------- 1 file changed, 121 insertions(+), 81 deletions(-) diff --git a/contact/utilities/input_handlers.py b/contact/utilities/input_handlers.py index 58707ff..83adb4f 100644 --- a/contact/utilities/input_handlers.py +++ b/contact/utilities/input_handlers.py @@ -247,6 +247,7 @@ def get_admin_key_input(current_value: List[bytes]) -> Optional[List[str]]: start_x = (curses.COLS - width) // 2 repeated_win = curses.newwin(height, width, start_y, start_x) + repeated_win.timeout(200) repeated_win.bkgd(get_color("background")) repeated_win.attrset(get_color("window_frame")) repeated_win.keypad(True) # Enable keypad for special keys @@ -288,12 +289,14 @@ def get_admin_key_input(current_value: List[bytes]) -> Optional[List[str]]: repeated_win.refresh() curses.noecho() curses.curs_set(0) + menu_state.need_redraw = True 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 curses.noecho() curses.curs_set(0) + menu_state.need_redraw = True return user_values # Return the edited Base64 values else: invalid_input = "Error: Each key must be valid Base64 and 32 bytes long!" @@ -312,6 +315,9 @@ def get_admin_key_input(current_value: List[bytes]) -> Optional[List[str]]: pass # Ignore invalid character inputs +from contact.utilities.singleton import menu_state # Required if not already imported + + def get_repeated_input(current_value: List[str]) -> Optional[str]: height = 9 width = 80 @@ -319,71 +325,82 @@ def get_repeated_input(current_value: List[str]) -> Optional[str]: start_x = (curses.COLS - width) // 2 repeated_win = curses.newwin(height, width, start_y, start_x) + repeated_win.timeout(200) repeated_win.bkgd(get_color("background")) repeated_win.attrset(get_color("window_frame")) - repeated_win.keypad(True) # Enable keypad for special keys + repeated_win.keypad(True) curses.echo() - curses.curs_set(1) # Show the cursor + curses.curs_set(1) - # Editable list of values (max 3 values) - user_values = current_value[:3] - cursor_pos = 0 # Track which value is being edited + user_values = current_value[:3] + [""] * (3 - len(current_value)) # Always 3 fields + cursor_pos = 0 invalid_input = "" - while True: + def redraw(): repeated_win.erase() repeated_win.border() 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 + prefix = "→ " if i == cursor_pos else " " 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, 18, line[: width - 20]) # Prevent overflow - # 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 - - # Show error message if needed if invalid_input: - repeated_win.addstr(7, 2, invalid_input, get_color("settings_default", bold=True)) + repeated_win.addstr(7, 2, invalid_input[: width - 4], get_color("settings_default", bold=True)) + repeated_win.move(3 + cursor_pos, 18 + len(user_values[cursor_pos])) repeated_win.refresh() - key = repeated_win.getch() - if key == 27 or key == curses.KEY_LEFT: # Escape or Left Arrow -> Cancel and return original + while True: + if menu_state.need_redraw: + menu_state.need_redraw = False + redraw() + + redraw() + + try: + key = repeated_win.get_wch() + except curses.error: + continue # ignore timeout or input issues + + if key in (27, curses.KEY_LEFT): # ESC or Left Arrow repeated_win.erase() repeated_win.refresh() curses.noecho() curses.curs_set(0) + menu_state.need_redraw = True return None - - elif key == ord("\n"): # Enter key to save and return + elif key in ("\n", curses.KEY_ENTER): curses.noecho() curses.curs_set(0) - return ", ".join(user_values) - 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 + menu_state.need_redraw = True + return ", ".join(user_values).strip() + elif key == curses.KEY_UP: + cursor_pos = (cursor_pos - 1) % 3 + elif key == curses.KEY_DOWN: + cursor_pos = (cursor_pos + 1) % 3 + elif key in (curses.KEY_BACKSPACE, 127): + user_values[cursor_pos] = user_values[cursor_pos][:-1] else: try: - user_values[cursor_pos] += chr(key) # Append valid character input to the selected field - invalid_input = "" # Clear error if user starts fixing input - except ValueError: - pass # Ignore invalid character inputs + ch = chr(key) if isinstance(key, int) else key + if ch.isprintable(): + user_values[cursor_pos] += ch + invalid_input = "" + except Exception: + pass + + +from contact.utilities.singleton import menu_state # Ensure this is imported def get_fixed32_input(current_value: int) -> int: - cvalue = current_value - current_value = str(ipaddress.IPv4Address(current_value)) + original_value = current_value + ip_string = str(ipaddress.IPv4Address(current_value)) height = 10 width = 80 start_y = (curses.LINES - height) // 2 @@ -393,55 +410,67 @@ def get_fixed32_input(current_value: int) -> int: fixed32_win.bkgd(get_color("background")) fixed32_win.attrset(get_color("window_frame")) fixed32_win.keypad(True) + fixed32_win.timeout(200) curses.echo() curses.curs_set(1) user_input = "" - while True: + def redraw(): 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(3, 2, f"Current: {current_value}") - fixed32_win.addstr(5, 2, f"New value: {user_input}") + fixed32_win.addstr(1, 2, "Enter an IP address (xxx.xxx.xxx.xxx):", get_color("settings_default", bold=True)) + fixed32_win.addstr(3, 2, f"Current: {ip_string}", get_color("settings_default")) + fixed32_win.addstr(5, 2, f"New value: {user_input}", get_color("settings_default")) fixed32_win.refresh() - key = fixed32_win.getch() + while True: + if menu_state.need_redraw: + menu_state.need_redraw = False + redraw() - if key == 27 or key == curses.KEY_LEFT: # Escape or Left Arrow to cancel + redraw() + + try: + key = fixed32_win.get_wch() + except curses.error: + continue # ignore timeout + + if key in (27, curses.KEY_LEFT): # ESC or Left Arrow to cancel fixed32_win.erase() fixed32_win.refresh() curses.noecho() curses.curs_set(0) - return cvalue # Return the current value unchanged - elif key == ord("\n"): # Enter key to validate and save - # Validate IP address + menu_state.need_redraw = True + return original_value + + elif key in ("\n", curses.KEY_ENTER): octets = user_input.split(".") 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 + menu_state.need_redraw = True + return int(ipaddress.ip_address(user_input)) 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.", get_color("settings_default", bold=True)) fixed32_win.refresh() - curses.napms(1500) # Wait for 1.5 seconds before refreshing - user_input = "" # Clear invalid input - elif key == curses.KEY_BACKSPACE or key == 127: # Backspace key + curses.napms(1500) + menu_state.need_redraw = True + user_input = "" + + elif key in (curses.KEY_BACKSPACE, 127): user_input = user_input[:-1] + else: try: - char = chr(key) - if char.isdigit() or char == ".": - user_input += char # Append only valid characters (digits or dots) - except ValueError: - pass # Ignore invalid inputs + ch = chr(key) if isinstance(key, int) else key + if ch.isdigit() or ch == ".": + user_input += ch + except Exception: + pass # Ignore unprintable inputs 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 height = min(len(list_options) + 5, curses.LINES) @@ -450,6 +479,7 @@ def get_list_input(prompt: str, current_option: Optional[str], list_options: Lis start_x = (curses.COLS - width) // 2 list_win = curses.newwin(height, width, start_y, start_x) + list_win.timeout(200) list_win.bkgd(get_color("background")) list_win.attrset(get_color("window_frame")) list_win.keypad(True) @@ -457,36 +487,44 @@ def get_list_input(prompt: str, current_option: Optional[str], list_options: Lis list_pad = curses.newpad(len(list_options) + 1, width - 8) list_pad.bkgd(get_color("background")) - # Render header - list_win.erase() - list_win.border() - list_win.addstr(1, 2, prompt, get_color("settings_default", bold=True)) - - # 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)) - else: - 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, - ) - max_index = len(list_options) - 1 visible_height = list_win.getmaxyx()[0] - 5 - draw_arrows(list_win, visible_height, max_index, [0], show_save_option=False) # Initial call to draw arrows + def redraw_list_ui(): + list_win.erase() + list_win.border() + list_win.addstr(1, 2, prompt, get_color("settings_default", bold=True)) + + 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)) + else: + list_pad.addstr(idx, 0, color.ljust(width - 8), get_color("settings_default")) + + 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, + ) + + draw_arrows(list_win, visible_height, max_index, [0], show_save_option=False) + + # Initial draw + redraw_list_ui() while True: - key = list_win.getch() + if menu_state.need_redraw: + menu_state.need_redraw = False + redraw_list_ui() + + try: + key = list_win.getch() + except curses.error: + continue # Graceful timeout handling if key == curses.KEY_UP: old_selected_index = selected_index @@ -499,8 +537,10 @@ def get_list_input(prompt: str, current_option: Optional[str], list_options: Lis elif key == ord("\n"): # Enter key list_win.clear() list_win.refresh() + menu_state.need_redraw = True return list_options[selected_index] elif key == 27 or key == curses.KEY_LEFT: # ESC or Left Arrow list_win.clear() list_win.refresh() + menu_state.need_redraw = True return current_option