Scroll Arrows for User Config (#161)

* almost working

* likely working changes

* fix width and launch

* unused UI state
This commit is contained in:
pdxlocations
2025-04-04 22:49:40 -07:00
committed by GitHub
parent a55d68a828
commit 4f64131d2e
4 changed files with 233 additions and 150 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
"request": "launch",
"cwd": "${workspaceFolder}",
"module": "contact.__main__",
"args": ["-c"]
"args": []
}
]
}
+69 -66
View File
@@ -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()
+8
View File
@@ -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
+155 -83
View File
@@ -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)