mirror of
https://github.com/pdxlocations/contact.git
synced 2026-08-07 01:13:19 +02:00
Refactor Navigation Functions (#169)
* working changes * working changes * working changes * not working changes * working changes * cleanup
This commit is contained in:
+5
-232
@@ -14,6 +14,7 @@ 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.nav_utils import move_highlight, draw_arrows, update_help_window
|
||||
|
||||
menu_state = MenuState()
|
||||
|
||||
@@ -37,9 +38,6 @@ config_folder = os.path.join(locals_dir, "node-configs")
|
||||
# Load translations
|
||||
field_mapping, help_text = parse_ini_file(translation_file)
|
||||
|
||||
# Aliases
|
||||
Segment = tuple[str, str, bool, bool]
|
||||
WrappedLine = list[Segment]
|
||||
|
||||
def display_menu(menu_state: MenuState) -> tuple[object, object]: # curses.window or pad types
|
||||
|
||||
@@ -107,7 +105,7 @@ def display_menu(menu_state: MenuState) -> tuple[object, object]: # curses.wind
|
||||
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)
|
||||
|
||||
draw_arrows(menu_win, visible_height, max_index, menu_state)
|
||||
draw_arrows(menu_win, visible_height, max_index, menu_state.start_index, show_save_option=False)
|
||||
|
||||
return menu_win, menu_pad
|
||||
|
||||
@@ -131,231 +129,6 @@ def draw_help_window(
|
||||
|
||||
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
|
||||
help_text: dict[str, str],
|
||||
transformed_path: list[str],
|
||||
selected_option: str | None,
|
||||
max_help_lines: int,
|
||||
width: int,
|
||||
help_y: 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)
|
||||
|
||||
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)
|
||||
|
||||
# Ensure help window does not exceed screen size
|
||||
if help_y + help_height > curses.LINES:
|
||||
help_y = curses.LINES - help_height
|
||||
|
||||
# Create or update the help window
|
||||
if help_win is None:
|
||||
help_win = curses.newwin(help_height, width, help_y, help_x)
|
||||
else:
|
||||
help_win.erase()
|
||||
help_win.refresh()
|
||||
help_win.resize(help_height, width)
|
||||
help_win.mvwin(help_y, help_x)
|
||||
|
||||
help_win.bkgd(get_color("background"))
|
||||
help_win.attrset(get_color("window_frame"))
|
||||
help_win.border()
|
||||
|
||||
for idx, line_segments in enumerate(wrapped_help):
|
||||
x_pos = 2 # Start after border
|
||||
for text, color, bold, underline in line_segments:
|
||||
try:
|
||||
attr = get_color(color, bold=bold, underline=underline)
|
||||
help_win.addstr(1 + idx, x_pos, text, attr)
|
||||
x_pos += len(text)
|
||||
except curses.error:
|
||||
pass # Prevent crashes
|
||||
|
||||
help_win.refresh()
|
||||
return help_win
|
||||
|
||||
|
||||
def get_wrapped_help_text(
|
||||
help_text: dict[str, str],
|
||||
transformed_path: list[str],
|
||||
selected_option: str | None,
|
||||
width: 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
|
||||
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
|
||||
}
|
||||
|
||||
def extract_ansi_segments(text: str) -> list[Segment]:
|
||||
"""Extracts and replaces ANSI color codes, ensuring spaces are preserved."""
|
||||
matches = []
|
||||
last_pos = 0
|
||||
pattern_matches = []
|
||||
|
||||
# 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))
|
||||
|
||||
# Sort matches by start position to process sequentially
|
||||
pattern_matches.sort(key=lambda x: x[0])
|
||||
|
||||
for start, end, content, color, bold, underline in pattern_matches:
|
||||
# Preserve non-matching text including spaces
|
||||
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
|
||||
|
||||
# Preserve any trailing text
|
||||
if last_pos < len(text):
|
||||
matches.append((text[last_pos:], "settings_default", False, False))
|
||||
|
||||
return matches
|
||||
|
||||
def wrap_ansi_text(segments: list[Segment], wrap_width: int) -> list[WrappedLine]:
|
||||
"""Wraps text while preserving ANSI formatting and spaces."""
|
||||
wrapped_lines = []
|
||||
line_buffer = []
|
||||
line_length = 0
|
||||
|
||||
for text, color, bold, underline in segments:
|
||||
words = re.findall(r'\S+|\s+', text) # Capture words and spaces separately
|
||||
|
||||
for word in words:
|
||||
word_length = len(word)
|
||||
|
||||
if line_length + word_length > wrap_width and word.strip():
|
||||
# If the word (ignoring spaces) exceeds width, wrap the line
|
||||
wrapped_lines.append(line_buffer)
|
||||
line_buffer = []
|
||||
line_length = 0
|
||||
|
||||
line_buffer.append((word, color, bold, underline))
|
||||
line_length += word_length
|
||||
|
||||
if line_buffer:
|
||||
wrapped_lines.append(line_buffer)
|
||||
|
||||
return wrapped_lines
|
||||
|
||||
raw_lines = help_content.split("\\n") # Preserve new lines
|
||||
wrapped_help = []
|
||||
|
||||
for raw_line in raw_lines:
|
||||
color_segments = extract_ansi_segments(raw_line)
|
||||
wrapped_segments = wrap_ansi_text(color_segments, wrap_width)
|
||||
wrapped_help.extend(wrapped_segments)
|
||||
pass
|
||||
|
||||
# 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))
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
# 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
|
||||
|
||||
# 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))
|
||||
|
||||
# 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"))
|
||||
|
||||
# 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))
|
||||
|
||||
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)
|
||||
|
||||
# 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
|
||||
) -> None:
|
||||
|
||||
# vh = visible_height + (1 if 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:
|
||||
win.addstr(3, 2, "▲", get_color("settings_default"))
|
||||
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) :
|
||||
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()
|
||||
@@ -396,12 +169,12 @@ def settings_menu(stdscr: object, interface: object) -> None:
|
||||
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)
|
||||
move_highlight(old_selected_index, options, menu_win, menu_pad, menu_state=menu_state, help_win=help_win, help_text=help_text, max_help_lines=max_help_lines)
|
||||
|
||||
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)
|
||||
move_highlight(old_selected_index, options, menu_win, menu_pad, menu_state=menu_state, help_win=help_win, help_text=help_text, max_help_lines=max_help_lines)
|
||||
|
||||
elif key == curses.KEY_RESIZE:
|
||||
need_redraw = True
|
||||
@@ -416,7 +189,7 @@ def settings_menu(stdscr: object, interface: object) -> None:
|
||||
elif key == ord("\t") and menu_state.show_save_option:
|
||||
old_selected_index = 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)
|
||||
move_highlight(old_selected_index, options, menu_win, menu_pad, menu_state=menu_state, help_win=help_win, help_text=help_text, max_help_lines=max_help_lines)
|
||||
|
||||
elif key == curses.KEY_RIGHT or key == ord('\n'):
|
||||
need_redraw = True
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
import curses
|
||||
import re
|
||||
from contact.ui.colors import get_color
|
||||
from contact.utilities.control_utils import transform_menu_path
|
||||
from typing import Any
|
||||
|
||||
# Aliases
|
||||
Segment = tuple[str, str, bool, bool]
|
||||
WrappedLine = list[Segment]
|
||||
|
||||
width = 80
|
||||
sensitive_settings = ["Reboot", "Reset Node DB", "Shutdown", "Factory Reset"]
|
||||
save_option = "Save Changes"
|
||||
|
||||
|
||||
def move_highlight(
|
||||
old_idx: int,
|
||||
options: list[str],
|
||||
menu_win: curses.window,
|
||||
menu_pad: curses.window,
|
||||
**kwargs: Any
|
||||
) -> None:
|
||||
|
||||
show_save_option = None
|
||||
start_index = [0]
|
||||
help_text = None
|
||||
max_help_lines = 0
|
||||
help_win = None
|
||||
|
||||
if "help_win" in kwargs:
|
||||
help_win = kwargs["help_win"]
|
||||
|
||||
if "menu_state" in kwargs:
|
||||
new_idx = kwargs["menu_state"].selected_index
|
||||
show_save_option = kwargs["menu_state"].show_save_option
|
||||
start_index = kwargs["menu_state"].start_index
|
||||
transformed_path = transform_menu_path(kwargs["menu_state"].menu_path)
|
||||
else:
|
||||
new_idx = kwargs["selected_index"]
|
||||
transformed_path = []
|
||||
|
||||
if "help_text" in kwargs:
|
||||
help_text = kwargs["help_text"]
|
||||
|
||||
if "max_help_lines" in kwargs:
|
||||
max_help_lines = kwargs["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 menu_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
|
||||
|
||||
# Ensure menu_state.start_index is within bounds
|
||||
start_index[-1] = max(0, min(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_sensitive") if options[old_idx] in sensitive_settings else get_color("settings_default"))
|
||||
|
||||
# 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_sensitive", reverse=True) if options[new_idx] in sensitive_settings else get_color("settings_default", reverse=True))
|
||||
|
||||
menu_win.refresh()
|
||||
|
||||
# Refresh pad only if scrolling is needed
|
||||
menu_pad.refresh(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 only if help_text is populated
|
||||
selected_option = options[new_idx] if new_idx < len(options) else None
|
||||
help_y = menu_win.getbegyx()[0] + menu_win.getmaxyx()[0]
|
||||
if help_win:
|
||||
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=False)
|
||||
|
||||
|
||||
def draw_arrows(
|
||||
win: object,
|
||||
visible_height: int,
|
||||
max_index: int,
|
||||
start_index: list[int],
|
||||
show_save_option: bool
|
||||
) -> None:
|
||||
|
||||
# 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 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) :
|
||||
win.addstr(visible_height + 3, 2, "▼", get_color("settings_default"))
|
||||
else:
|
||||
win.addstr(visible_height + 3, 2, " ", get_color("settings_default"))
|
||||
|
||||
|
||||
def update_help_window(
|
||||
help_win: object, # curses window or None
|
||||
help_text: dict[str, str],
|
||||
transformed_path: list[str],
|
||||
selected_option: str | None,
|
||||
max_help_lines: int,
|
||||
width: int,
|
||||
help_y: 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)
|
||||
|
||||
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)
|
||||
|
||||
# Ensure help window does not exceed screen size
|
||||
if help_y + help_height > curses.LINES:
|
||||
help_y = curses.LINES - help_height
|
||||
|
||||
# Create or update the help window
|
||||
if help_win is None:
|
||||
help_win = curses.newwin(help_height, width, help_y, help_x)
|
||||
else:
|
||||
help_win.erase()
|
||||
help_win.refresh()
|
||||
help_win.resize(help_height, width)
|
||||
help_win.mvwin(help_y, help_x)
|
||||
|
||||
help_win.bkgd(get_color("background"))
|
||||
help_win.attrset(get_color("window_frame"))
|
||||
help_win.border()
|
||||
|
||||
for idx, line_segments in enumerate(wrapped_help):
|
||||
x_pos = 2 # Start after border
|
||||
for text, color, bold, underline in line_segments:
|
||||
try:
|
||||
attr = get_color(color, bold=bold, underline=underline)
|
||||
help_win.addstr(1 + idx, x_pos, text, attr)
|
||||
x_pos += len(text)
|
||||
except curses.error:
|
||||
pass # Prevent crashes
|
||||
|
||||
help_win.refresh()
|
||||
return help_win
|
||||
|
||||
def get_wrapped_help_text(
|
||||
help_text: dict[str, str],
|
||||
transformed_path: list[str],
|
||||
selected_option: str | None,
|
||||
width: 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
|
||||
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
|
||||
}
|
||||
|
||||
def extract_ansi_segments(text: str) -> list[Segment]:
|
||||
"""Extracts and replaces ANSI color codes, ensuring spaces are preserved."""
|
||||
matches = []
|
||||
last_pos = 0
|
||||
pattern_matches = []
|
||||
|
||||
# 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))
|
||||
|
||||
# Sort matches by start position to process sequentially
|
||||
pattern_matches.sort(key=lambda x: x[0])
|
||||
|
||||
for start, end, content, color, bold, underline in pattern_matches:
|
||||
# Preserve non-matching text including spaces
|
||||
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
|
||||
|
||||
# Preserve any trailing text
|
||||
if last_pos < len(text):
|
||||
matches.append((text[last_pos:], "settings_default", False, False))
|
||||
|
||||
return matches
|
||||
|
||||
def wrap_ansi_text(segments: list[Segment], wrap_width: int) -> list[WrappedLine]:
|
||||
"""Wraps text while preserving ANSI formatting and spaces."""
|
||||
wrapped_lines = []
|
||||
line_buffer = []
|
||||
line_length = 0
|
||||
|
||||
for text, color, bold, underline in segments:
|
||||
words = re.findall(r'\S+|\s+', text) # Capture words and spaces separately
|
||||
|
||||
for word in words:
|
||||
word_length = len(word)
|
||||
|
||||
if line_length + word_length > wrap_width and word.strip():
|
||||
# If the word (ignoring spaces) exceeds width, wrap the line
|
||||
wrapped_lines.append(line_buffer)
|
||||
line_buffer = []
|
||||
line_length = 0
|
||||
|
||||
line_buffer.append((word, color, bold, underline))
|
||||
line_length += word_length
|
||||
|
||||
if line_buffer:
|
||||
wrapped_lines.append(line_buffer)
|
||||
|
||||
return wrapped_lines
|
||||
|
||||
raw_lines = help_content.split("\\n") # Preserve new lines
|
||||
wrapped_help = []
|
||||
|
||||
for raw_line in raw_lines:
|
||||
color_segments = extract_ansi_segments(raw_line)
|
||||
wrapped_segments = wrap_ansi_text(color_segments, wrap_width)
|
||||
wrapped_help.extend(wrapped_segments)
|
||||
pass
|
||||
|
||||
# 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))
|
||||
|
||||
return wrapped_help
|
||||
|
||||
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
|
||||
wrapped_lines = []
|
||||
line_buffer = ""
|
||||
line_length = 0
|
||||
margin = 2 # Left and right margin
|
||||
wrap_width -= margin
|
||||
|
||||
for word in words:
|
||||
word_length = len(word)
|
||||
|
||||
if word_length > wrap_width: # Break long words
|
||||
if line_buffer:
|
||||
wrapped_lines.append(line_buffer)
|
||||
line_buffer = ""
|
||||
line_length = 0
|
||||
for i in range(0, word_length, wrap_width):
|
||||
wrapped_lines.append(word[i:i+wrap_width])
|
||||
continue
|
||||
|
||||
if line_length + word_length > wrap_width and word.strip():
|
||||
wrapped_lines.append(line_buffer)
|
||||
line_buffer = ""
|
||||
line_length = 0
|
||||
|
||||
line_buffer += word
|
||||
line_length += word_length
|
||||
|
||||
if line_buffer:
|
||||
wrapped_lines.append(line_buffer)
|
||||
|
||||
return wrapped_lines
|
||||
|
||||
@@ -5,8 +5,11 @@ 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.nav_utils import move_highlight, draw_arrows
|
||||
|
||||
|
||||
width = 80
|
||||
max_help_lines = 6
|
||||
save_option = "Save Changes"
|
||||
sensitive_settings = []
|
||||
|
||||
@@ -162,79 +165,11 @@ def display_menu(menu_state: Any) -> tuple[Any, Any, list[str]]:
|
||||
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)
|
||||
|
||||
draw_arrows(menu_win, visible_height, max_index, menu_state)
|
||||
draw_arrows(menu_win, visible_height, max_index, menu_state.start_index, show_save_option=False)
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
# 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
|
||||
|
||||
# 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))
|
||||
|
||||
# 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"))
|
||||
|
||||
# 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))
|
||||
|
||||
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
|
||||
) -> None:
|
||||
|
||||
mi = max_index - (2 if menu_state.show_save_option else 0)
|
||||
|
||||
if visible_height < mi:
|
||||
if menu_state.start_index[-1] > 0:
|
||||
win.addstr(3, 2, "▲", get_color("settings_default"))
|
||||
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) :
|
||||
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: curses.window, menu_state: Any) -> None:
|
||||
@@ -246,6 +181,8 @@ def json_editor(stdscr: curses.window, menu_state: Any) -> None:
|
||||
file_path = os.path.join(parent_dir, "config.json")
|
||||
|
||||
menu_state.show_save_option = True # Always show the Save button
|
||||
menu_state.help_win = None
|
||||
menu_state.help_text = {}
|
||||
|
||||
# Ensure the file exists
|
||||
if not os.path.exists(file_path):
|
||||
@@ -276,18 +213,18 @@ def json_editor(stdscr: curses.window, menu_state: Any) -> None:
|
||||
|
||||
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)
|
||||
menu_state.help_win = move_highlight(old_selected_index, options, menu_win, menu_pad, menu_state=menu_state, max_help_lines=max_help_lines)
|
||||
|
||||
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)
|
||||
menu_state.help_win = move_highlight(old_selected_index, options, menu_win, menu_pad, menu_state=menu_state, max_help_lines=max_help_lines)
|
||||
|
||||
elif key == ord("\t") and menu_state.show_save_option:
|
||||
old_selected_index = menu_state.selected_index
|
||||
menu_state.selected_index = max_index
|
||||
move_highlight(old_selected_index, options, menu_win, menu_pad, menu_state)
|
||||
menu_state.help_win = move_highlight(old_selected_index, options, menu_win, menu_pad, menu_state=menu_state, max_help_lines=max_help_lines)
|
||||
|
||||
elif key in (curses.KEY_RIGHT, 10, 13): # 10 = \n, 13 = carriage return
|
||||
|
||||
|
||||
@@ -5,41 +5,8 @@ import ipaddress
|
||||
import re
|
||||
from typing import Any, Optional
|
||||
from contact.ui.colors import get_color
|
||||
from contact.ui.nav_utils import move_highlight, draw_arrows, wrap_text
|
||||
|
||||
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
|
||||
wrapped_lines = []
|
||||
line_buffer = ""
|
||||
line_length = 0
|
||||
margin = 2 # Left and right margin
|
||||
wrap_width -= margin
|
||||
|
||||
for word in words:
|
||||
word_length = len(word)
|
||||
|
||||
if word_length > wrap_width: # Break long words
|
||||
if line_buffer:
|
||||
wrapped_lines.append(line_buffer)
|
||||
line_buffer = ""
|
||||
line_length = 0
|
||||
for i in range(0, word_length, wrap_width):
|
||||
wrapped_lines.append(word[i:i+wrap_width])
|
||||
continue
|
||||
|
||||
if line_length + word_length > wrap_width and word.strip():
|
||||
wrapped_lines.append(line_buffer)
|
||||
line_buffer = ""
|
||||
line_length = 0
|
||||
|
||||
line_buffer += word
|
||||
line_length += word_length
|
||||
|
||||
if line_buffer:
|
||||
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."""
|
||||
@@ -377,7 +344,7 @@ def get_list_input(prompt: str, current_option: Optional[str], list_options: lis
|
||||
max_index = len(list_options) - 1
|
||||
visible_height = list_win.getmaxyx()[0] - 5
|
||||
|
||||
draw_arrows(list_win, visible_height, max_index, 0)
|
||||
draw_arrows(list_win, visible_height, max_index, [0], show_save_option=False) # Initial call to draw arrows
|
||||
|
||||
while True:
|
||||
key = list_win.getch()
|
||||
@@ -385,11 +352,11 @@ def get_list_input(prompt: str, current_option: Optional[str], list_options: lis
|
||||
if key == curses.KEY_UP:
|
||||
old_selected_index = selected_index
|
||||
selected_index = max(0, selected_index - 1)
|
||||
move_highlight(old_selected_index, selected_index, list_options, list_win, list_pad)
|
||||
move_highlight(old_selected_index, list_options, list_win, list_pad, selected_index=selected_index)
|
||||
elif key == curses.KEY_DOWN:
|
||||
old_selected_index = 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)
|
||||
move_highlight(old_selected_index, list_options, list_win, list_pad, selected_index=selected_index)
|
||||
elif key == ord('\n'): # Enter key
|
||||
list_win.clear()
|
||||
list_win.refresh()
|
||||
@@ -398,69 +365,3 @@ def get_list_input(prompt: str, current_option: Optional[str], list_options: lis
|
||||
list_win.clear()
|
||||
list_win.refresh()
|
||||
return current_option
|
||||
|
||||
|
||||
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
|
||||
|
||||
if old_idx == new_idx:
|
||||
return # No-op
|
||||
|
||||
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
|
||||
|
||||
# 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"))
|
||||
|
||||
# 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)
|
||||
|
||||
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
|
||||
) -> 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"))
|
||||
|
||||
Reference in New Issue
Block a user