From 4f64131d2eb6b9fec69f3035ba0dc6e581aa61fe Mon Sep 17 00:00:00 2001 From: pdxlocations <117498748+pdxlocations@users.noreply.github.com> Date: Fri, 4 Apr 2025 22:49:40 -0700 Subject: [PATCH] Scroll Arrows for User Config (#161) * almost working * likely working changes * fix width and launch * unused UI state --- .vscode/launch.json | 2 +- contact/ui/control_ui.py | 135 ++++++++++----------- contact/ui/ui_state.py | 8 ++ contact/ui/user_config.py | 238 +++++++++++++++++++++++++------------- 4 files changed, 233 insertions(+), 150 deletions(-) create mode 100644 contact/ui/ui_state.py diff --git a/.vscode/launch.json b/.vscode/launch.json index d6dff23..fe2dc5b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,7 +7,7 @@ "request": "launch", "cwd": "${workspaceFolder}", "module": "contact.__main__", - "args": ["-c"] + "args": [] } ] } diff --git a/contact/ui/control_ui.py b/contact/ui/control_ui.py index b3c5b9e..28c446d 100644 --- a/contact/ui/control_ui.py +++ b/contact/ui/control_ui.py @@ -13,6 +13,9 @@ from contact.ui.colors import get_color from contact.ui.dialog import dialog from contact.utilities.control_utils import parse_ini_file, transform_menu_path from contact.ui.user_config import json_editor +from contact.ui.ui_state import UIState + +state = UIState() import contact.localisations @@ -37,13 +40,10 @@ config_folder = os.path.join(locals_dir, "node-configs") field_mapping, help_text = parse_ini_file(translation_file) -def display_menu(current_menu, menu_path, selected_index, show_save_option, help_text): +def display_menu(current_menu, selected_index, show_save_option, help_text): + min_help_window_height = 6 num_items = len(current_menu) + (1 if show_save_option else 0) - # Track visible range - global start_index - if 'start_index' not in globals(): - start_index = [0] # Initialize if not set # Determine the available height for the menu max_menu_height = curses.LINES @@ -66,12 +66,12 @@ def display_menu(current_menu, menu_path, selected_index, show_save_option, help menu_pad = curses.newpad(len(current_menu) + 1, width - 8) menu_pad.bkgd(get_color("background")) - header = " > ".join(word.title() for word in menu_path) + header = " > ".join(word.title() for word in state.menu_path) if len(header) > width - 4: header = header[:width - 7] + "..." menu_win.addstr(1, 2, header, get_color("settings_breadcrumbs", bold=True)) - transformed_path = transform_menu_path(menu_path) + transformed_path = transform_menu_path(state.menu_path) for idx, option in enumerate(current_menu): field_info = current_menu[option] @@ -97,7 +97,7 @@ def display_menu(current_menu, menu_path, selected_index, show_save_option, help menu_win.refresh() menu_pad.refresh( - start_index[-1], 0, + 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 show_save_option else 0), menu_win.getbegyx()[1] + menu_win.getmaxyx()[1] - 8 @@ -106,7 +106,7 @@ def display_menu(current_menu, menu_path, selected_index, show_save_option, help max_index = num_items + (1 if show_save_option else 0) - 1 visible_height = menu_win.getmaxyx()[0] - 5 - (2 if show_save_option else 0) - draw_arrows(menu_win, visible_height, max_index, start_index, show_save_option) + draw_arrows(menu_win, visible_height, max_index, state, show_save_option) return menu_win, menu_pad @@ -252,24 +252,24 @@ def get_wrapped_help_text(help_text, transformed_path, selected_option, width, m return wrapped_help -def move_highlight(old_idx, new_idx, options, show_save_option, menu_win, menu_pad, help_win, help_text, menu_path, max_help_lines): +def move_highlight(old_idx, new_idx, options, show_save_option, menu_win, menu_pad, help_win, help_text, max_help_lines): if old_idx == new_idx: # No-op return max_index = len(options) + (1 if show_save_option else 0) - 1 visible_height = menu_win.getmaxyx()[0] - 5 - (2 if show_save_option else 0) - # Adjust start_index only when moving out of visible range + # Adjust state.start_index only when moving out of visible range if new_idx == max_index and show_save_option: pass - elif new_idx < start_index[-1]: # Moving above the visible area - start_index[-1] = new_idx - elif new_idx >= start_index[-1] + visible_height: # Moving below the visible area - start_index[-1] = new_idx - visible_height + elif new_idx < state.start_index[-1]: # Moving above the visible area + state.start_index[-1] = new_idx + elif new_idx >= state.start_index[-1] + visible_height: # Moving below the visible area + state.start_index[-1] = new_idx - visible_height pass - # Ensure start_index is within bounds - start_index[-1] = max(0, min(start_index[-1], max_index - visible_height + 1)) + # Ensure state.start_index is within bounds + state.start_index[-1] = max(0, min(state.start_index[-1], max_index - visible_height + 1)) # Clear old selection if show_save_option and old_idx == max_index: @@ -286,18 +286,18 @@ def move_highlight(old_idx, new_idx, options, show_save_option, menu_win, menu_p menu_win.refresh() # Refresh pad only if scrolling is needed - menu_pad.refresh(start_index[-1], 0, + menu_pad.refresh(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()[1] + menu_win.getmaxyx()[1] - 4) # Update help window - transformed_path = transform_menu_path(menu_path) + transformed_path = transform_menu_path(state.menu_path) selected_option = options[new_idx] if new_idx < len(options) else None help_y = menu_win.getbegyx()[0] + menu_win.getmaxyx()[0] help_win = update_help_window(help_win, help_text, transformed_path, selected_option, max_help_lines, width, help_y, menu_win.getbegyx()[1]) - draw_arrows(menu_win, visible_height, max_index, start_index, show_save_option) + draw_arrows(menu_win, visible_height, max_index, state, show_save_option) def draw_arrows(win, visible_height, max_index, start_index, show_save_option): @@ -306,12 +306,12 @@ def draw_arrows(win, visible_height, max_index, start_index, show_save_option): mi = max_index - (2 if show_save_option else 0) if visible_height < mi: - if start_index[-1] > 0: + if state.start_index[-1] > 0: win.addstr(3, 2, "▲", get_color("settings_default")) else: win.addstr(3, 2, " ", get_color("settings_default")) - if mi - start_index[-1] >= visible_height + (0 if show_save_option else 1) : + if mi - state.start_index[-1] >= visible_height + (0 if 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")) @@ -322,7 +322,7 @@ def settings_menu(stdscr, interface): menu = generate_menu_from_protobuf(interface) current_menu = menu["Main Menu"] - menu_path = ["Main Menu"] + state.menu_path = ["Main Menu"] menu_index = [] selected_index = 0 modified_settings = {} @@ -335,15 +335,15 @@ def settings_menu(stdscr, interface): options = list(current_menu.keys()) show_save_option = ( - len(menu_path) > 2 and ("Radio Settings" in menu_path or "Module Settings" in menu_path) + len(state.menu_path) > 2 and ("Radio Settings" in state.menu_path or "Module Settings" in state.menu_path) ) or ( - len(menu_path) == 2 and "User Settings" in menu_path + len(state.menu_path) == 2 and "User Settings" in state.menu_path ) or ( - len(menu_path) == 3 and "Channels" in menu_path + len(state.menu_path) == 3 and "Channels" in state.menu_path ) # Display the menu - menu_win, menu_pad = display_menu(current_menu, menu_path, selected_index, show_save_option, help_text) + menu_win, menu_pad = display_menu(current_menu, selected_index, show_save_option, help_text) need_redraw = False @@ -356,12 +356,12 @@ def settings_menu(stdscr, interface): if key == curses.KEY_UP: old_selected_index = selected_index selected_index = max_index if selected_index == 0 else selected_index - 1 - move_highlight(old_selected_index, selected_index, options, show_save_option, menu_win, menu_pad, help_win, help_text, menu_path,max_help_lines) + move_highlight(old_selected_index, selected_index, options, show_save_option, menu_win, menu_pad, help_win, help_text, max_help_lines) elif key == curses.KEY_DOWN: old_selected_index = selected_index selected_index = 0 if selected_index == max_index else selected_index + 1 - move_highlight(old_selected_index, selected_index, options, show_save_option, menu_win, menu_pad, help_win, help_text, menu_path, max_help_lines) + move_highlight(old_selected_index, selected_index, options, show_save_option, menu_win, menu_pad, help_win, help_text, max_help_lines) elif key == curses.KEY_RESIZE: need_redraw = True @@ -376,28 +376,28 @@ def settings_menu(stdscr, interface): elif key == ord("\t") and show_save_option: old_selected_index = selected_index selected_index = max_index - move_highlight(old_selected_index, selected_index, options, show_save_option, menu_win, menu_pad, help_win, help_text, menu_path, max_help_lines) + move_highlight(old_selected_index, selected_index, options, show_save_option, menu_win, menu_pad, help_win, help_text, max_help_lines) elif key == curses.KEY_RIGHT or key == ord('\n'): need_redraw = True - start_index.append(0) + state.start_index.append(0) menu_win.erase() help_win.erase() - # draw_help_window(menu_win.getbegyx()[0], menu_win.getbegyx()[1], menu_win.getmaxyx()[0], max_help_lines, current_menu, selected_index, transform_menu_path(menu_path)) + # draw_help_window(menu_win.getbegyx()[0], menu_win.getbegyx()[1], menu_win.getmaxyx()[0], max_help_lines, current_menu, selected_index, transform_menu_path(state.menu_path)) menu_win.refresh() help_win.refresh() if show_save_option and selected_index == len(options): - save_changes(interface, menu_path, modified_settings) + save_changes(interface, modified_settings) modified_settings.clear() logging.info("Changes Saved") - if len(menu_path) > 1: - menu_path.pop() + if len(state.menu_path) > 1: + state.menu_path.pop() current_menu = menu["Main Menu"] - for step in menu_path[1:]: + for step in state.menu_path[1:]: current_menu = current_menu.get(step, {}) selected_index = 0 continue @@ -411,7 +411,7 @@ def settings_menu(stdscr, interface): filename = get_text_input("Enter a filename for the config file") if not filename: logging.info("Export aborted: No filename provided.") - start_index.pop() + state.start_index.pop() continue # Go back to the menu if not filename.lower().endswith(".yaml"): filename += ".yaml" @@ -424,14 +424,14 @@ def settings_menu(stdscr, interface): overwrite = get_list_input(f"{filename} already exists. Overwrite?", None, ["Yes", "No"]) if overwrite == "No": logging.info("Export cancelled: User chose not to overwrite.") - start_index.pop() + state.start_index.pop() continue # Return to menu os.makedirs(os.path.dirname(yaml_file_path), exist_ok=True) 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) - start_index.pop() + state.start_index.pop() continue except PermissionError: logging.error(f"Permission denied: Unable to write to {yaml_file_path}") @@ -439,7 +439,7 @@ def settings_menu(stdscr, interface): logging.error(f"OS error while saving config: {e}") except Exception as e: logging.error(f"Unexpected error: {e}") - start_index.pop() + state.start_index.pop() continue elif selected_option == "Load Config File": @@ -462,7 +462,7 @@ def settings_menu(stdscr, interface): overwrite = get_list_input(f"Are you sure you want to load {filename}?", None, ["Yes", "No"]) if overwrite == "Yes": config_import(interface, file_path) - start_index.pop() + state.start_index.pop() continue elif selected_option == "Config URL": @@ -474,7 +474,7 @@ def settings_menu(stdscr, interface): if overwrite == "Yes": interface.localNode.setURL(new_value) logging.info(f"New Config URL sent to node") - start_index.pop() + state.start_index.pop() continue elif selected_option == "Reboot": @@ -482,7 +482,7 @@ def settings_menu(stdscr, interface): if confirmation == "Yes": interface.localNode.reboot() logging.info(f"Node Reboot Requested by menu") - start_index.pop() + state.start_index.pop() continue elif selected_option == "Reset Node DB": @@ -490,7 +490,7 @@ def settings_menu(stdscr, interface): if confirmation == "Yes": interface.localNode.resetNodeDb() logging.info(f"Node DB Reset Requested by menu") - start_index.pop() + state.start_index.pop() continue elif selected_option == "Shutdown": @@ -498,7 +498,7 @@ def settings_menu(stdscr, interface): if confirmation == "Yes": interface.localNode.shutdown() logging.info(f"Node Shutdown Requested by menu") - start_index.pop() + state.start_index.pop() continue elif selected_option == "Factory Reset": @@ -506,13 +506,16 @@ def settings_menu(stdscr, interface): if confirmation == "Yes": interface.localNode.factoryReset() logging.info(f"Factory Reset Requested by menu") - start_index.pop() + state.start_index.pop() continue elif selected_option == "App Settings": menu_win.clear() menu_win.refresh() - json_editor(stdscr) # Open the App Settings menu + state.menu_path.append("App Settings") + json_editor(stdscr, state) # Open the App Settings menu + state.start_index.pop() + state.menu_path.pop() continue # need_redraw = True @@ -521,7 +524,7 @@ def settings_menu(stdscr, interface): field, current_value = field_info # Transform the menu path to get the full key - transformed_path = transform_menu_path(menu_path) + transformed_path = transform_menu_path(state.menu_path) full_key = '.'.join(transformed_path + [selected_option]) # Fetch human-readable name from field_mapping @@ -541,7 +544,7 @@ def settings_menu(stdscr, interface): for option, (field, value) in current_menu.items(): modified_settings[option] = value - start_index.pop() + state.start_index.pop() elif selected_option in ['latitude', 'longitude', 'altitude']: new_value = get_text_input(f"{human_readable_name} is currently: {current_value}") @@ -552,49 +555,49 @@ def settings_menu(stdscr, interface): if option in current_menu: modified_settings[option] = current_menu[option][1] - start_index.pop() + 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] - start_index.pop() + 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 = new_value == "True" or new_value is True - start_index.pop() + state.start_index.pop() 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(", ") - start_index.pop() + 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 = enum_options.get(new_value_name, current_value) - start_index.pop() + state.start_index.pop() elif field.type == 7: # Field type 7 corresponds to FIXED32 new_value = get_fixed32_input(current_value) - start_index.pop() + 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 = current_value if new_value is None else int(new_value) - start_index.pop() + 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 = current_value if new_value is None else float(new_value) - start_index.pop() + state.start_index.pop() else: # Handle other field types 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 - start_index.pop() + state.start_index.pop() - for key in menu_path[3:]: # Skip "Main Menu" + for key in state.menu_path[3:]: # Skip "Main Menu" modified_settings = modified_settings.setdefault(key, {}) # Add the new value to the appropriate level @@ -608,7 +611,7 @@ def settings_menu(stdscr, interface): current_menu[selected_option] = (field, new_value) else: current_menu = current_menu[selected_option] - menu_path.append(selected_option) + state.menu_path.append(selected_option) menu_index.append(selected_index) selected_index = 0 @@ -620,22 +623,22 @@ def settings_menu(stdscr, interface): help_win.erase() # max_help_lines = 4 - # draw_help_window(menu_win.getbegyx()[0], menu_win.getbegyx()[1], menu_win.getmaxyx()[0], max_help_lines, current_menu, selected_index, transform_menu_path(menu_path)) + # draw_help_window(menu_win.getbegyx()[0], menu_win.getbegyx()[1], menu_win.getmaxyx()[0], max_help_lines, current_menu, selected_index, transform_menu_path(state.menu_path)) menu_win.refresh() help_win.refresh() - if len(menu_path) < 2: + if len(state.menu_path) < 2: modified_settings.clear() # Navigate back to the previous menu - if len(menu_path) > 1: - menu_path.pop() + if len(state.menu_path) > 1: + state.menu_path.pop() current_menu = menu["Main Menu"] - for step in menu_path[1:]: + for step in state.menu_path[1:]: current_menu = current_menu.get(step, {}) selected_index = menu_index.pop() - start_index.pop() + state.start_index.pop() elif key == 27: # Escape key menu_win.erase() diff --git a/contact/ui/ui_state.py b/contact/ui/ui_state.py new file mode 100644 index 0000000..c23fa74 --- /dev/null +++ b/contact/ui/ui_state.py @@ -0,0 +1,8 @@ +class UIState: + def __init__(self): + self.start_index = [0] + self.menu_path = [] + # self.menu_index = [] + # self.current_menu = "" + # self.selected_index = 0 + # self.show_save_option = False \ No newline at end of file diff --git a/contact/ui/user_config.py b/contact/ui/user_config.py index eb4e9b6..9890ce0 100644 --- a/contact/ui/user_config.py +++ b/contact/ui/user_config.py @@ -5,8 +5,11 @@ from contact.ui.colors import get_color, setup_colors, COLOR_MAP from contact.ui.default_config import format_json_single_line_arrays, loaded_config from contact.utilities.input_handlers import get_list_input -width = 60 -save_option_text = "Save Changes" + +width = 80 +save_option = "Save Changes" +sensitive_settings = [] + def edit_color_pair(key, current_value): @@ -19,8 +22,8 @@ def edit_color_pair(key, current_value): return [fg_color, bg_color] -def edit_value(key, current_value): - width = 60 +def edit_value(key, current_value, state): + height = 10 input_width = width - 16 # Allow space for "New Value: " start_y = (curses.LINES - height) // 2 @@ -73,8 +76,10 @@ def edit_value(key, current_value): 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 + elif key in (curses.KEY_BACKSPACE, chr(127)): # Backspace if user_input: # Only process if there's something to delete user_input = user_input[:-1] @@ -93,45 +98,48 @@ def edit_value(key, current_value): return user_input if user_input else current_value -def render_menu(current_data, menu_path, selected_index): +def display_menu(current_menu, selected_index, show_save_option, state): """ Render the configuration menu with a Save button directly added to the window. """ - # Determine menu items based on the type of current_data - if isinstance(current_data, dict): - options = list(current_data.keys()) - elif isinstance(current_data, list): - options = [f"[{i}]" for i in range(len(current_data))] + num_items = len(current_menu) + (1 if show_save_option else 0) + + # Determine menu items based on the type of current_menu + if isinstance(current_menu, dict): + options = list(current_menu.keys()) + elif isinstance(current_menu, list): + options = [f"[{i}]" for i in range(len(current_menu))] else: options = [] # Fallback in case of unexpected data types # Calculate dynamic dimensions for the menu + max_menu_height = curses.LINES + menu_height = min(max_menu_height, num_items + 5) num_items = len(options) - height = min(curses.LINES - 2, num_items + 6) # Include space for borders and Save button - start_y = (curses.LINES - height) // 2 + start_y = (curses.LINES - menu_height) // 2 start_x = (curses.COLS - width) // 2 # Create the window - menu_win = curses.newwin(height, width, start_y, start_x) - menu_win.clear() + menu_win = curses.newwin(menu_height, width, start_y, start_x) + menu_win.erase() menu_win.bkgd(get_color("background")) menu_win.attrset(get_color("window_frame")) menu_win.border() menu_win.keypad(True) - # Display the menu path - header = " > ".join(menu_path) - if len(header) > width - 4: - header = header[:width - 7] + "..." - menu_win.addstr(1, 2, header, get_color("settings_breadcrumbs", bold=True)) - # Create the pad for scrolling menu_pad = curses.newpad(num_items + 1, width - 8) menu_pad.bkgd(get_color("background")) + # Display the menu path + header = " > ".join(state.menu_path) + if len(header) > width - 4: + 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 = current_data[key] if isinstance(current_data, dict) else current_data[int(key.strip("[]"))] + value = current_menu[key] if isinstance(current_menu, dict) else current_menu[int(key.strip("[]"))] display_key = f"{key}"[:width // 2 - 2] display_value = ( f"{value}"[:width // 2 - 8] @@ -141,66 +149,97 @@ def render_menu(current_data, menu_path, 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 - save_button_position = height - 2 - menu_win.addstr( - save_button_position, - (width - len(save_option_text)) // 2, - save_option_text, - get_color("settings_save", reverse=(selected_index == len(options))), - ) + if 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=(selected_index == len(current_menu)))) + - # Refresh menu and pad menu_win.refresh() menu_pad.refresh( - 0, - 0, - menu_win.getbegyx()[0] + 3, - menu_win.getbegyx()[1] + 4, - - menu_win.getbegyx()[0] + menu_win.getmaxyx()[0] - 3, - menu_win.getbegyx()[1] + menu_win.getmaxyx()[1] - 4, + 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 show_save_option else 0), + menu_win.getbegyx()[1] + menu_win.getmaxyx()[1] - 4 ) + max_index = num_items + (1 if show_save_option else 0) - 1 + visible_height = menu_win.getmaxyx()[0] - 5 - (2 if show_save_option else 0) + + draw_arrows(menu_win, visible_height, max_index, state, show_save_option) + return menu_win, menu_pad, options -def move_highlight(old_idx, new_idx, options, menu_win, menu_pad): - if old_idx == new_idx: - return # no-op - - show_save_option = True +def move_highlight(old_idx, new_idx, options, show_save_option, menu_win, menu_pad, state): + if old_idx == new_idx: # No-op + return max_index = len(options) + (1 if show_save_option else 0) - 1 + visible_height = menu_win.getmaxyx()[0] - 5 - (2 if show_save_option else 0) - if show_save_option and old_idx == max_index: # special case un-highlight "Save" option - menu_win.chgat(menu_win.getmaxyx()[0] - 2, (width - len(save_option_text)) // 2, len(save_option_text), get_color("settings_save")) + # Adjust state.start_index only when moving out of visible range + if new_idx == max_index and show_save_option: + pass + elif new_idx < state.start_index[-1]: # Moving above the visible area + state.start_index[-1] = new_idx + elif new_idx >= state.start_index[-1] + visible_height: # Moving below the visible area + state.start_index[-1] = new_idx - visible_height + pass + + # Ensure state.start_index is within bounds + state.start_index[-1] = max(0, min(state.start_index[-1], max_index - visible_height + 1)) + + # Clear old selection + if show_save_option and old_idx == max_index: + menu_win.chgat(menu_win.getmaxyx()[0] - 2, (width - len(save_option)) // 2, len(save_option), get_color("settings_save")) else: - menu_pad.chgat(old_idx, 0, menu_pad.getmaxyx()[1], get_color("settings_default")) + menu_pad.chgat(old_idx, 0, menu_pad.getmaxyx()[1], get_color("settings_sensitive") if options[old_idx] in sensitive_settings else get_color("settings_default")) - if show_save_option and new_idx == max_index: # special case highlight "Save" option - menu_win.chgat(menu_win.getmaxyx()[0] - 2, (width - len(save_option_text)) // 2, len(save_option_text), get_color("settings_save", reverse = True)) + # Highlight new selection + if show_save_option and new_idx == max_index: + menu_win.chgat(menu_win.getmaxyx()[0] - 2, (width - len(save_option)) // 2, len(save_option), get_color("settings_save", reverse=True)) else: - menu_pad.chgat(new_idx, 0,menu_pad.getmaxyx()[1], get_color("settings_default", reverse = True)) - - start_index = max(0, new_idx - (menu_win.getmaxyx()[0] - 6)) + menu_pad.chgat(new_idx, 0, menu_pad.getmaxyx()[1], get_color("settings_sensitive", reverse=True) if options[new_idx] in sensitive_settings else get_color("settings_default", reverse=True)) menu_win.refresh() - menu_pad.refresh(start_index, 0, - menu_win.getbegyx()[0] + 3, - menu_win.getbegyx()[1] + 4, - menu_win.getbegyx()[0] + menu_win.getmaxyx()[0] - 3, - menu_win.getbegyx()[1] + 4 + menu_win.getmaxyx()[1] - 4) + + # Refresh pad only if scrolling is needed + menu_pad.refresh(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()[1] + menu_win.getmaxyx()[1] - 4) -def json_editor(stdscr): - menu_path = ["App Settings"] + draw_arrows(menu_win, visible_height, max_index, state, show_save_option) + + +def draw_arrows(win, visible_height, max_index, state, show_save_option): + + # vh = visible_height + (1 if show_save_option else 0) + mi = max_index - (2 if show_save_option else 0) + + if visible_height < mi: + if state.start_index[-1] > 0: + win.addstr(3, 2, "▲", get_color("settings_default")) + else: + win.addstr(3, 2, " ", get_color("settings_default")) + + if mi - state.start_index[-1] >= visible_height + (0 if 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 json_editor(stdscr, state): + selected_index = 0 # Track the selected option script_dir = os.path.dirname(os.path.abspath(__file__)) parent_dir = os.path.abspath(os.path.join(script_dir, os.pardir)) file_path = os.path.join(parent_dir, "config.json") - # file_path = "config.json" + show_save_option = True # Always show the Save button + menu_index = [] # Ensure the file exists if not os.path.exists(file_path): @@ -212,15 +251,15 @@ def json_editor(stdscr): original_data = json.load(f) data = original_data # Reference to the original data - current_data = data # Track the current level of the menu + current_menu = data # Track the current level of the menu # Render the menu - menu_win, menu_pad, options = render_menu(current_data, menu_path, selected_index) + menu_win, menu_pad, options = display_menu(current_menu, selected_index, show_save_option, state) need_redraw = True while True: if(need_redraw): - menu_win, menu_pad, options = render_menu(current_data, menu_path, selected_index) + menu_win, menu_pad, options = display_menu(current_menu, selected_index, show_save_option, state) menu_win.refresh() need_redraw = False @@ -232,55 +271,71 @@ def json_editor(stdscr): old_selected_index = selected_index selected_index = max_index if selected_index == 0 else selected_index - 1 - move_highlight(old_selected_index, selected_index, options, menu_win, menu_pad) + move_highlight(old_selected_index, selected_index, options, show_save_option, menu_win, menu_pad,state) elif key == curses.KEY_DOWN: old_selected_index = selected_index selected_index = 0 if selected_index == max_index else selected_index + 1 - move_highlight(old_selected_index, selected_index, options, menu_win, menu_pad) + move_highlight(old_selected_index, selected_index, options, show_save_option, menu_win, menu_pad, state) elif key == ord("\t") and show_save_option: old_selected_index = selected_index selected_index = max_index - move_highlight(old_selected_index, selected_index, options, menu_win, menu_pad) + move_highlight(old_selected_index, selected_index, options, show_save_option, menu_win, menu_pad, state) - elif key in (curses.KEY_RIGHT, ord("\n")): + elif key in (curses.KEY_RIGHT, 10, 13): # 10 = \n, 13 = carriage return need_redraw = True + + menu_win.erase() menu_win.refresh() if selected_index < len(options): # Handle selection of a menu item + selected_key = options[selected_index] + state.menu_path.append(str(selected_key)) + state.start_index.append(0) + menu_index.append(selected_index) + # Handle nested data - if isinstance(current_data, dict): - if selected_key in current_data: - selected_data = current_data[selected_key] + if isinstance(current_menu, dict): + if selected_key in current_menu: + selected_data = current_menu[selected_key] else: continue # Skip invalid key - elif isinstance(current_data, list): - selected_data = current_data[int(selected_key.strip("[]"))] + elif isinstance(current_menu, list): + selected_data = current_menu[int(selected_key.strip("[]"))] if isinstance(selected_data, list) and len(selected_data) == 2: # Edit color pair - new_value = edit_color_pair( - selected_key, selected_data) - current_data[selected_key] = new_value + + + + new_value = edit_color_pair(selected_key, selected_data) + state.menu_path.pop() + state.start_index.pop() + menu_index.pop() + current_menu[selected_key] = new_value elif isinstance(selected_data, (dict, list)): # Navigate into nested data - menu_path.append(str(selected_key)) - current_data = selected_data + + current_menu = selected_data selected_index = 0 # Reset the selected index else: # General value editing - new_value = edit_value(selected_key, selected_data) - current_data[selected_key] = new_value + + new_value = edit_value(selected_key, selected_data, state) + state.menu_path.pop() + state.start_index.pop() + current_menu[selected_key] = new_value need_redraw = True + else: # Save button selected @@ -294,17 +349,28 @@ def json_editor(stdscr): menu_win.erase() menu_win.refresh() + + # Navigate back in the menu - if len(menu_path) > 1: - menu_path.pop() - current_data = data - for path in menu_path[1:]: - current_data = current_data[path] if isinstance(current_data, dict) else current_data[int(path.strip("[]"))] - selected_index = 0 + + if len(state.menu_path) > 2: + selected_index = menu_index.pop() + state.menu_path.pop() + state.start_index.pop() + + + current_menu = data + for path in state.menu_path[2:]: + current_menu = current_menu[path] if isinstance(current_menu, dict) else current_menu[int(path.strip("[]"))] + + + + else: # Exit the editor menu_win.clear() menu_win.refresh() + break @@ -315,10 +381,16 @@ def save_json(file_path, data): setup_colors(reinit=True) def main(stdscr): + from contact.ui.ui_state import UIState + + state = UIState() + if len(state.menu_path) == 0: + state.menu_path = ["App Settings"] # Initialize if not set + curses.curs_set(0) stdscr.keypad(True) setup_colors() - json_editor(stdscr) + json_editor(stdscr, state) if __name__ == "__main__": curses.wrapper(main) \ No newline at end of file