mirror of
https://github.com/pdxlocations/contact.git
synced 2026-03-28 17:12:35 +01:00
Compare commits
2 Commits
1.4.19
...
refactor-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e31e39266 | ||
|
|
e3f85ffaf2 |
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.black-formatter",
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,20 @@ 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
|
||||
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 MenuState
|
||||
from contact.ui.navigation_utils import move_highlight
|
||||
|
||||
menu_state = MenuState()
|
||||
|
||||
@@ -41,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
|
||||
|
||||
@@ -69,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)
|
||||
@@ -77,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)
|
||||
|
||||
@@ -118,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
|
||||
@@ -139,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)
|
||||
@@ -184,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]:
|
||||
@@ -213,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])
|
||||
@@ -223,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
|
||||
@@ -241,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)
|
||||
@@ -271,79 +337,76 @@ 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
|
||||
|
||||
|
||||
def move_highlight(
|
||||
old_idx: int,
|
||||
options: list[str],
|
||||
menu_win: object,
|
||||
menu_pad: object,
|
||||
help_win: object,
|
||||
help_text: dict[str, str],
|
||||
max_help_lines: int,
|
||||
menu_state: MenuState
|
||||
) -> None:
|
||||
|
||||
if old_idx == menu_state.selected_index: # No-op
|
||||
return
|
||||
# def move_highlight(
|
||||
# old_idx: int,
|
||||
# options: list[str],
|
||||
# menu_win: object,
|
||||
# menu_pad: object,
|
||||
# help_win: object,
|
||||
# help_text: dict[str, str],
|
||||
# max_help_lines: int,
|
||||
# menu_state: MenuState
|
||||
# ) -> None:
|
||||
|
||||
max_index = len(options) + (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)
|
||||
# if old_idx == menu_state.selected_index: # No-op
|
||||
# return
|
||||
|
||||
# Adjust menu_state.start_index only when moving out of visible range
|
||||
if menu_state.selected_index == max_index and menu_state.show_save_option:
|
||||
pass
|
||||
elif menu_state.selected_index < menu_state.start_index[-1]: # Moving above the visible area
|
||||
menu_state.start_index[-1] = menu_state.selected_index
|
||||
elif menu_state.selected_index >= menu_state.start_index[-1] + visible_height: # Moving below the visible area
|
||||
menu_state.start_index[-1] = menu_state.selected_index - visible_height
|
||||
pass
|
||||
# max_index = len(options) + (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)
|
||||
|
||||
# Ensure menu_state.start_index is within bounds
|
||||
menu_state.start_index[-1] = max(0, min(menu_state.start_index[-1], max_index - visible_height + 1))
|
||||
# # Adjust menu_state.start_index only when moving out of visible range
|
||||
# if menu_state.selected_index == max_index and menu_state.show_save_option:
|
||||
# pass
|
||||
# elif menu_state.selected_index < menu_state.start_index[-1]: # Moving above the visible area
|
||||
# menu_state.start_index[-1] = menu_state.selected_index
|
||||
# elif menu_state.selected_index >= menu_state.start_index[-1] + visible_height: # Moving below the visible area
|
||||
# menu_state.start_index[-1] = menu_state.selected_index - visible_height
|
||||
# pass
|
||||
|
||||
# Clear old selection
|
||||
if menu_state.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_sensitive") if options[old_idx] in sensitive_settings else get_color("settings_default"))
|
||||
# # Ensure menu_state.start_index is within bounds
|
||||
# menu_state.start_index[-1] = max(0, min(menu_state.start_index[-1], max_index - visible_height + 1))
|
||||
|
||||
# Highlight new selection
|
||||
if menu_state.show_save_option and menu_state.selected_index == 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(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))
|
||||
# # Clear old selection
|
||||
# if menu_state.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_sensitive") if options[old_idx] in sensitive_settings else get_color("settings_default"))
|
||||
|
||||
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()[1] + menu_win.getmaxyx()[1] - 4)
|
||||
# # Highlight new selection
|
||||
# if menu_state.show_save_option and menu_state.selected_index == 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(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))
|
||||
|
||||
# Update help window
|
||||
transformed_path = transform_menu_path(menu_state.menu_path)
|
||||
selected_option = options[menu_state.selected_index] if menu_state.selected_index < 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])
|
||||
# menu_win.refresh()
|
||||
|
||||
draw_arrows(menu_win, visible_height, max_index, menu_state)
|
||||
# # 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()[1] + menu_win.getmaxyx()[1] - 4)
|
||||
|
||||
# # Update help window
|
||||
# transformed_path = transform_menu_path(menu_state.menu_path)
|
||||
# selected_option = options[menu_state.selected_index] if menu_state.selected_index < 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, menu_state)
|
||||
|
||||
|
||||
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:
|
||||
@@ -351,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()
|
||||
@@ -364,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
|
||||
@@ -394,14 +468,52 @@ def settings_menu(stdscr: object, interface: object) -> None:
|
||||
# max_help_lines = 4
|
||||
|
||||
if key == curses.KEY_UP:
|
||||
old_selected_index = menu_state.selected_index
|
||||
menu_state.selected_index = max_index if menu_state.selected_index == 0 else menu_state.selected_index - 1
|
||||
move_highlight(old_selected_index, options, menu_win, menu_pad, help_win, help_text, max_help_lines, menu_state)
|
||||
|
||||
old_idx = menu_state.selected_index
|
||||
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,
|
||||
start_index_ref=menu_state.start_index[-1:],
|
||||
selected_index=menu_state.selected_index,
|
||||
show_save=menu_state.show_save_option,
|
||||
help_win=help_win,
|
||||
help_updater=update_help_window,
|
||||
field_mapping=help_text,
|
||||
menu_path=transform_menu_path(menu_state.menu_path),
|
||||
max_help_lines=max_help_lines,
|
||||
sensitive_mode=True,
|
||||
)
|
||||
|
||||
elif key == curses.KEY_DOWN:
|
||||
old_selected_index = menu_state.selected_index
|
||||
menu_state.selected_index = 0 if menu_state.selected_index == max_index else menu_state.selected_index + 1
|
||||
move_highlight(old_selected_index, options, menu_win, menu_pad, help_win, help_text, max_help_lines, menu_state)
|
||||
old_idx = menu_state.selected_index
|
||||
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,
|
||||
start_index_ref=menu_state.start_index[-1:],
|
||||
selected_index=menu_state.selected_index,
|
||||
show_save=menu_state.show_save_option,
|
||||
help_win=help_win,
|
||||
help_updater=update_help_window,
|
||||
field_mapping=help_text,
|
||||
menu_path=transform_menu_path(menu_state.menu_path),
|
||||
max_help_lines=max_help_lines,
|
||||
sensitive_mode=True,
|
||||
)
|
||||
|
||||
elif key == curses.KEY_RESIZE:
|
||||
need_redraw = True
|
||||
@@ -414,11 +526,25 @@ def settings_menu(stdscr: object, interface: object) -> None:
|
||||
help_win.refresh()
|
||||
|
||||
elif key == ord("\t") and menu_state.show_save_option:
|
||||
old_selected_index = menu_state.selected_index
|
||||
old_idx = menu_state.selected_index
|
||||
menu_state.selected_index = max_index
|
||||
move_highlight(old_selected_index, options, menu_win, menu_pad, help_win, help_text, max_help_lines, menu_state)
|
||||
|
||||
elif key == curses.KEY_RIGHT or key == ord('\n'):
|
||||
move_highlight(
|
||||
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,
|
||||
help_win=help_win,
|
||||
help_updater=update_help_window,
|
||||
field_mapping=help_text,
|
||||
menu_path=transform_menu_path(menu_state.menu_path),
|
||||
max_help_lines=max_help_lines,
|
||||
sensitive_mode=True,
|
||||
)
|
||||
elif key == curses.KEY_RIGHT or key == ord("\n"):
|
||||
need_redraw = True
|
||||
menu_state.start_index.append(0)
|
||||
menu_win.erase()
|
||||
@@ -429,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")
|
||||
@@ -461,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)
|
||||
@@ -474,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:
|
||||
@@ -499,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()
|
||||
@@ -510,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")
|
||||
@@ -518,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")
|
||||
@@ -526,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")
|
||||
@@ -534,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")
|
||||
@@ -542,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")
|
||||
@@ -561,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)
|
||||
|
||||
@@ -589,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, {})
|
||||
|
||||
@@ -651,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:
|
||||
@@ -661,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
|
||||
|
||||
@@ -685,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
|
||||
|
||||
@@ -702,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")
|
||||
node.writeConfig("lora")
|
||||
|
||||
120
contact/ui/navigation_utils.py
Normal file
120
contact/ui/navigation_utils.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# contact/ui/navigation_utils.py
|
||||
|
||||
import curses
|
||||
from contact.ui.colors import get_color
|
||||
|
||||
save_option = "Save Changes"
|
||||
sensitive_settings = ["Reboot", "Reset Node DB", "Shutdown", "Factory Reset"]
|
||||
|
||||
|
||||
def move_highlight(
|
||||
old_idx: int,
|
||||
new_idx: int,
|
||||
options: list[str],
|
||||
win: curses.window,
|
||||
pad: curses.window,
|
||||
*,
|
||||
start_index_ref: list[int] = None,
|
||||
selected_index: int = None,
|
||||
max_help_lines: int = 0,
|
||||
show_save: bool = False,
|
||||
help_win: curses.window = None,
|
||||
help_updater=None,
|
||||
field_mapping=None,
|
||||
menu_path: list[str] = None,
|
||||
width: int = 80,
|
||||
sensitive_mode: bool = False,
|
||||
):
|
||||
if old_idx == new_idx:
|
||||
return
|
||||
|
||||
max_index = len(options) + (1 if show_save else 0) - 1
|
||||
visible_height = win.getmaxyx()[0] - 5 - (2 if show_save else 0)
|
||||
|
||||
# Scrolling logic
|
||||
if start_index_ref is not None:
|
||||
if new_idx == max_index and show_save:
|
||||
pass
|
||||
elif new_idx < start_index_ref[0]:
|
||||
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)
|
||||
)
|
||||
|
||||
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"),
|
||||
)
|
||||
else:
|
||||
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),
|
||||
)
|
||||
else:
|
||||
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,
|
||||
)
|
||||
|
||||
# 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
|
||||
)
|
||||
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],
|
||||
)
|
||||
|
||||
|
||||
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"))
|
||||
@@ -5,21 +5,28 @@ from typing import Any
|
||||
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
|
||||
from contact.ui.navigation_utils import move_highlight
|
||||
|
||||
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
|
||||
@@ -38,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()
|
||||
@@ -48,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)
|
||||
@@ -63,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
|
||||
|
||||
@@ -82,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
|
||||
@@ -112,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
|
||||
@@ -132,98 +158,120 @@ 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)
|
||||
|
||||
return menu_win, menu_pad, options
|
||||
|
||||
|
||||
def move_highlight(
|
||||
old_idx: int,
|
||||
options: list[str],
|
||||
menu_win: curses.window,
|
||||
menu_pad: curses.window,
|
||||
menu_state: Any
|
||||
) -> None:
|
||||
|
||||
if old_idx == menu_state.selected_index: # No-op
|
||||
return
|
||||
# def move_highlight(
|
||||
# old_idx: int,
|
||||
# options: list[str],
|
||||
# menu_win: curses.window,
|
||||
# menu_pad: curses.window,
|
||||
# menu_state: Any
|
||||
# ) -> None:
|
||||
|
||||
max_index = len(options) + (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)
|
||||
# if old_idx == menu_state.selected_index: # No-op
|
||||
# return
|
||||
|
||||
# Adjust menu_state.start_index only when moving out of visible range
|
||||
if menu_state.selected_index == max_index and menu_state.show_save_option:
|
||||
pass
|
||||
elif menu_state.selected_index < menu_state.start_index[-1]: # Moving above the visible area
|
||||
menu_state.start_index[-1] = menu_state.selected_index
|
||||
elif menu_state.selected_index >= menu_state.start_index[-1] + visible_height: # Moving below the visible area
|
||||
menu_state.start_index[-1] = menu_state.selected_index - visible_height
|
||||
pass
|
||||
# max_index = len(options) + (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)
|
||||
|
||||
# Ensure menu_state.start_index is within bounds
|
||||
menu_state.start_index[-1] = max(0, min(menu_state.start_index[-1], max_index - visible_height + 1))
|
||||
# # Adjust menu_state.start_index only when moving out of visible range
|
||||
# if menu_state.selected_index == max_index and menu_state.show_save_option:
|
||||
# pass
|
||||
# elif menu_state.selected_index < menu_state.start_index[-1]: # Moving above the visible area
|
||||
# menu_state.start_index[-1] = menu_state.selected_index
|
||||
# elif menu_state.selected_index >= menu_state.start_index[-1] + visible_height: # Moving below the visible area
|
||||
# menu_state.start_index[-1] = menu_state.selected_index - visible_height
|
||||
# pass
|
||||
|
||||
# Clear old selection
|
||||
if menu_state.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_sensitive") if options[old_idx] in sensitive_settings else get_color("settings_default"))
|
||||
# # Ensure menu_state.start_index is within bounds
|
||||
# menu_state.start_index[-1] = max(0, min(menu_state.start_index[-1], max_index - visible_height + 1))
|
||||
|
||||
# Highlight new selection
|
||||
if menu_state.show_save_option and menu_state.selected_index == 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(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))
|
||||
# # Clear old selection
|
||||
# if menu_state.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_sensitive") if options[old_idx] in sensitive_settings else get_color("settings_default"))
|
||||
|
||||
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()[1] + menu_win.getmaxyx()[1] - 4)
|
||||
# # Highlight new selection
|
||||
# if menu_state.show_save_option and menu_state.selected_index == 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(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))
|
||||
|
||||
draw_arrows(menu_win, visible_height, max_index, menu_state)
|
||||
# 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()[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:
|
||||
@@ -231,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"))
|
||||
@@ -264,30 +314,68 @@ 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_selected_index = menu_state.selected_index
|
||||
menu_state.selected_index = max_index if menu_state.selected_index == 0 else menu_state.selected_index - 1
|
||||
move_highlight(old_selected_index, options, menu_win, menu_pad, menu_state)
|
||||
old_idx = menu_state.selected_index
|
||||
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,
|
||||
start_index_ref=menu_state.start_index[-1:],
|
||||
selected_index=menu_state.selected_index,
|
||||
show_save=menu_state.show_save_option,
|
||||
sensitive_mode=True,
|
||||
)
|
||||
|
||||
elif key == curses.KEY_DOWN:
|
||||
|
||||
old_selected_index = menu_state.selected_index
|
||||
menu_state.selected_index = 0 if menu_state.selected_index == max_index else menu_state.selected_index + 1
|
||||
move_highlight(old_selected_index, options, menu_win, menu_pad, menu_state)
|
||||
old_idx = menu_state.selected_index
|
||||
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,
|
||||
start_index_ref=menu_state.start_index[-1:],
|
||||
selected_index=menu_state.selected_index,
|
||||
show_save=menu_state.show_save_option,
|
||||
sensitive_mode=True,
|
||||
)
|
||||
|
||||
elif key == ord("\t") and menu_state.show_save_option:
|
||||
old_selected_index = menu_state.selected_index
|
||||
old_idx = menu_state.selected_index
|
||||
menu_state.selected_index = max_index
|
||||
move_highlight(old_selected_index, options, menu_win, menu_pad, menu_state)
|
||||
move_highlight(
|
||||
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,
|
||||
)
|
||||
|
||||
elif key in (curses.KEY_RIGHT, 10, 13): # 10 = \n, 13 = carriage return
|
||||
|
||||
@@ -295,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:
|
||||
@@ -308,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
|
||||
@@ -331,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)
|
||||
@@ -353,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
|
||||
|
||||
|
||||
@@ -369,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
|
||||
|
||||
@@ -381,5 +478,6 @@ def main(stdscr: curses.window) -> None:
|
||||
setup_colors()
|
||||
json_editor(stdscr, menu_state)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
curses.wrapper(main)
|
||||
curses.wrapper(main)
|
||||
|
||||
@@ -5,10 +5,12 @@ import ipaddress
|
||||
import re
|
||||
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
|
||||
@@ -24,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():
|
||||
@@ -39,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."""
|
||||
@@ -61,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)
|
||||
|
||||
@@ -105,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()
|
||||
|
||||
@@ -164,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
|
||||
@@ -235,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)
|
||||
@@ -271,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
|
||||
@@ -300,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()
|
||||
@@ -313,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
|
||||
@@ -337,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
|
||||
@@ -364,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
|
||||
|
||||
@@ -383,14 +464,32 @@ def get_list_input(prompt: str, current_option: Optional[str], list_options: lis
|
||||
key = list_win.getch()
|
||||
|
||||
if key == curses.KEY_UP:
|
||||
old_selected_index = selected_index
|
||||
old_idx = selected_index
|
||||
selected_index = max(0, selected_index - 1)
|
||||
move_highlight(old_selected_index, selected_index, list_options, list_win, list_pad)
|
||||
scroll_ref = [scroll_offset]
|
||||
move_highlight(
|
||||
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:
|
||||
old_selected_index = selected_index
|
||||
old_idx = selected_index
|
||||
selected_index = min(len(list_options) - 1, selected_index + 1)
|
||||
move_highlight(old_selected_index, selected_index, list_options, list_win, list_pad)
|
||||
elif key == ord('\n'): # Enter key
|
||||
scroll_ref = [scroll_offset]
|
||||
move_highlight(
|
||||
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
|
||||
list_win.clear()
|
||||
list_win.refresh()
|
||||
return list_options[selected_index]
|
||||
@@ -400,57 +499,54 @@ def get_list_input(prompt: str, current_option: Optional[str], list_options: lis
|
||||
return current_option
|
||||
|
||||
|
||||
def move_highlight(
|
||||
old_idx: int,
|
||||
new_idx: int,
|
||||
options: list[str],
|
||||
list_win: curses.window,
|
||||
list_pad: curses.window
|
||||
) -> int:
|
||||
# def move_highlight(
|
||||
# old_idx: int,
|
||||
# new_idx: int,
|
||||
# options: list[str],
|
||||
# list_win: curses.window,
|
||||
# list_pad: curses.window
|
||||
# ) -> int:
|
||||
|
||||
global scroll_offset
|
||||
if 'scroll_offset' not in globals():
|
||||
scroll_offset = 0 # Initialize if not set
|
||||
# global scroll_offset
|
||||
# if 'scroll_offset' not in globals():
|
||||
# scroll_offset = 0 # Initialize if not set
|
||||
|
||||
if old_idx == new_idx:
|
||||
return # No-op
|
||||
# if old_idx == new_idx:
|
||||
# return # No-op
|
||||
|
||||
max_index = len(options) - 1
|
||||
visible_height = list_win.getmaxyx()[0] - 5
|
||||
# max_index = len(options) - 1
|
||||
# visible_height = list_win.getmaxyx()[0] - 5
|
||||
|
||||
# Adjust scroll_offset only when moving out of visible range
|
||||
if new_idx < scroll_offset: # Moving above the visible area
|
||||
scroll_offset = new_idx
|
||||
elif new_idx >= scroll_offset + visible_height: # Moving below the visible area
|
||||
scroll_offset = new_idx - visible_height
|
||||
# # Adjust scroll_offset only when moving out of visible range
|
||||
# if new_idx < scroll_offset: # Moving above the visible area
|
||||
# scroll_offset = new_idx
|
||||
# elif new_idx >= scroll_offset + visible_height: # Moving below the visible area
|
||||
# scroll_offset = new_idx - visible_height
|
||||
|
||||
# Ensure scroll_offset is within bounds
|
||||
scroll_offset = max(0, min(scroll_offset, max_index - visible_height + 1))
|
||||
# # Ensure scroll_offset is within bounds
|
||||
# scroll_offset = max(0, min(scroll_offset, max_index - visible_height + 1))
|
||||
|
||||
# Clear old highlight
|
||||
list_pad.chgat(old_idx, 0, list_pad.getmaxyx()[1], get_color("settings_default"))
|
||||
# # Clear old highlight
|
||||
# list_pad.chgat(old_idx, 0, list_pad.getmaxyx()[1], get_color("settings_default"))
|
||||
|
||||
# Highlight new selection
|
||||
list_pad.chgat(new_idx, 0, list_pad.getmaxyx()[1], get_color("settings_default", reverse=True))
|
||||
# # Highlight new selection
|
||||
# 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()[1] + list_win.getmaxyx()[1] - 4)
|
||||
|
||||
draw_arrows(list_win, visible_height, max_index, scroll_offset)
|
||||
# list_win.refresh()
|
||||
|
||||
return scroll_offset # Return updated scroll_offset to be stored externally
|
||||
# # 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()[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:
|
||||
@@ -463,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"))
|
||||
|
||||
Reference in New Issue
Block a user