This commit is contained in:
pdxlocations
2025-07-25 00:18:51 -07:00
parent 4378f3045c
commit 18329f0128
4 changed files with 80 additions and 32 deletions

View File

@@ -335,7 +335,6 @@ def handle_ctrl_t(stdscr: curses.window) -> None:
send_traceroute()
curses.curs_set(0) # Hide cursor
contact.ui.dialog.dialog(
stdscr,
f"Traceroute Sent To: {get_name_from_database(ui_state.node_list[ui_state.selected_node])}",
"Results will appear in messages window.\nNote: Traceroute is limited to once every 30 seconds.",
)

View File

@@ -268,7 +268,7 @@ def settings_menu(stdscr: object, interface: object) -> None:
break
elif selected_option == "Export Config File":
filename = get_text_input("Enter a filename for the config file")
filename = get_text_input("Enter a filename for the config file", None)
if not filename:
logging.info("Export aborted: No filename provided.")
menu_state.start_index.pop()
@@ -290,7 +290,7 @@ def settings_menu(stdscr: object, interface: object) -> None:
with open(yaml_file_path, "w", encoding="utf-8") as file:
file.write(config_text)
logging.info(f"Config file saved to {yaml_file_path}")
dialog(stdscr, "Config File Saved:", yaml_file_path)
dialog("Config File Saved:", yaml_file_path)
menu_state.start_index.pop()
continue
except PermissionError:
@@ -306,14 +306,14 @@ def settings_menu(stdscr: object, interface: object) -> None:
# Check if folder exists and is not empty
if not os.path.exists(config_folder) or not any(os.listdir(config_folder)):
dialog(stdscr, "", " No config files found. Export a config first.")
dialog("", " No config files found. Export a config first.")
continue # Return to menu
file_list = [f for f in os.listdir(config_folder) if os.path.isfile(os.path.join(config_folder, f))]
# Ensure file_list is not empty before proceeding
if not file_list:
dialog(stdscr, "", " No config files found. Export a config first.")
dialog("", " No config files found. Export a config first.")
continue
filename = get_list_input("Choose a config file", None, file_list)
@@ -327,7 +327,7 @@ def settings_menu(stdscr: object, interface: object) -> None:
elif selected_option == "Config URL":
current_value = interface.localNode.getURL()
new_value = get_text_input(f"Config URL is currently: {current_value}")
new_value = get_text_input(f"Config URL is currently: {current_value}", None)
if new_value is not None:
current_value = new_value
overwrite = get_list_input(f"Are you sure you want to load this config?", None, ["Yes", "No"])
@@ -395,7 +395,9 @@ def settings_menu(stdscr: object, interface: object) -> None:
if selected_option in ["longName", "shortName", "isLicensed"]:
if selected_option in ["longName", "shortName"]:
new_value = get_text_input(f"{human_readable_name} is currently: {current_value}")
new_value = get_text_input(
f"{human_readable_name} is currently: {current_value}", selected_option
)
new_value = current_value if new_value is None else new_value
menu_state.current_menu[selected_option] = (field, new_value)
@@ -414,7 +416,7 @@ def settings_menu(stdscr: object, interface: object) -> None:
menu_state.start_index.pop()
elif selected_option in ["latitude", "longitude", "altitude"]:
new_value = get_text_input(f"{human_readable_name} is currently: {current_value}")
new_value = get_text_input(f"{human_readable_name} is currently: {current_value}", selected_option)
new_value = current_value if new_value is None else new_value
menu_state.current_menu[selected_option] = (field, new_value)
@@ -453,17 +455,17 @@ def settings_menu(stdscr: object, interface: object) -> None:
menu_state.start_index.pop()
elif field.type == 13: # Field type 13 corresponds to UINT32
new_value = get_text_input(f"{human_readable_name} is currently: {current_value}")
new_value = get_text_input(f"{human_readable_name} is currently: {current_value}", selected_option)
new_value = current_value if new_value is None else int(new_value)
menu_state.start_index.pop()
elif field.type == 2: # Field type 13 corresponds to INT64
new_value = get_text_input(f"{human_readable_name} is currently: {current_value}")
new_value = get_text_input(f"{human_readable_name} is currently: {current_value}", selected_option)
new_value = current_value if new_value is None else float(new_value)
menu_state.start_index.pop()
else: # Handle other field types
new_value = get_text_input(f"{human_readable_name} is currently: {current_value}")
new_value = get_text_input(f"{human_readable_name} is currently: {current_value}", selected_option)
new_value = current_value if new_value is None else new_value
menu_state.start_index.pop()

View File

@@ -2,14 +2,13 @@ import curses
from contact.ui.colors import get_color
def dialog(stdscr: curses.window, title: str, message: str) -> None:
height, width = stdscr.getmaxyx()
def dialog(title: str, message: str) -> None:
height, width = curses.LINES, curses.COLS
# Calculate dialog dimensions
max_line_lengh = 0
message_lines = message.splitlines()
for l in message_lines:
max_line_length = max(len(l), max_line_lengh)
max_line_length = max(len(l) for l in message_lines)
dialog_width = max(len(title) + 4, max_line_length + 4)
dialog_height = len(message_lines) + 4
x = (width - dialog_width) // 2
@@ -24,12 +23,19 @@ def dialog(stdscr: curses.window, title: str, message: str) -> None:
# Add title
win.addstr(0, 2, title, get_color("settings_default"))
# Add message
for i, l in enumerate(message_lines):
win.addstr(2 + i, 2, l, get_color("settings_default"))
# Add message (centered)
for i, line in enumerate(message_lines):
msg_x = (dialog_width - len(line)) // 2
win.addstr(2 + i, msg_x, line, get_color("settings_default"))
# Add button
win.addstr(dialog_height - 2, (dialog_width - 4) // 2, " Ok ", get_color("settings_default", reverse=True))
# Add centered OK button
ok_text = " Ok "
win.addstr(
dialog_height - 2,
(dialog_width - len(ok_text)) // 2,
ok_text,
get_color("settings_default", reverse=True),
)
# Refresh dialog window
win.refresh()
@@ -37,8 +43,7 @@ def dialog(stdscr: curses.window, title: str, message: str) -> None:
# Get user input
while True:
char = win.getch()
# Close dialog with enter, space, or esc
if char in (curses.KEY_ENTER, 10, 13, 32, 27):
if char in (curses.KEY_ENTER, 10, 13, 32, 27): # Enter, space, or Esc
win.erase()
win.refresh()
return

View File

@@ -6,9 +6,10 @@ from typing import Any, Optional, List
from contact.ui.colors import get_color
from contact.ui.nav_utils import move_highlight, draw_arrows, wrap_text
from contact.ui.dialog import dialog
def get_text_input(prompt: str) -> Optional[str]:
def get_text_input(prompt: str, selected_config: str) -> Optional[str]:
"""Handles user input with wrapped text for long prompts."""
height = 8
width = 80
@@ -39,7 +40,22 @@ def get_text_input(prompt: str) -> Optional[str]:
input_win.refresh()
curses.curs_set(1)
max_length = 4 if "shortName" in prompt else None
max_length = None
fixed_length = None
input_type = str
if selected_config:
if "shortName" in selected_config:
max_length = 4
elif "longName" in selected_config:
max_length = 32
elif "fixed_pin" in selected_config:
fixed_length = 6
max_length = fixed_length # enforce fixed length
input_type = int
elif "adc_multiplier_override" in selected_config:
input_type = float
user_input = ""
# Start user input after the prompt text
@@ -55,18 +71,44 @@ def get_text_input(prompt: str) -> Optional[str]:
curses.curs_set(0)
return None # Exit without saving
elif key in (chr(curses.KEY_ENTER), chr(10), chr(13)): # Enter key
break
elif key in (chr(curses.KEY_ENTER), chr(10), chr(13)):
if fixed_length and len(user_input) != fixed_length:
curses.curs_set(0)
dialog(input_win, "Error", f"Value must be exactly {fixed_length} characters long.")
curses.curs_set(1)
elif input_type is int and not user_input.isdigit():
curses.curs_set(0)
dialog(input_win, "Error", "Only numeric digits (09) allowed.")
curses.curs_set(1)
elif input_type is float:
try:
float(user_input)
except ValueError:
curses.curs_set(0)
dialog(input_win, "Error", "Must be a valid floating point number.")
curses.curs_set(1)
else:
break
else:
break
elif key in (curses.KEY_BACKSPACE, chr(127)): # Handle Backspace
if user_input:
user_input = user_input[:-1] # Remove last character
elif max_length is None or len(user_input) < max_length: # Enforce max length
if isinstance(key, str):
user_input += key
else:
user_input += chr(key)
elif max_length is None or len(user_input) < max_length:
try:
char = chr(key) if not isinstance(key, str) else key
if input_type is int:
if char.isdigit():
user_input += char
elif input_type is float:
if char.isdigit() or (char == "." and "." not in user_input):
user_input += char
else:
user_input += char
except ValueError:
pass # Ignore invalid input
# First line must be manually handled before using wrap_text()
first_line = user_input[:first_line_width] # Cut to max first line width