diff --git a/contact/ui/contact_ui.py b/contact/ui/contact_ui.py index a0adf59..cf0d881 100644 --- a/contact/ui/contact_ui.py +++ b/contact/ui/contact_ui.py @@ -13,7 +13,7 @@ from contact.utilities.input_handlers import get_list_input import contact.ui.default_config as config import contact.ui.dialog from contact.ui.nav_utils import move_main_highlight, draw_main_arrows, get_msg_window_lines, wrap_text -from contact.utilities.singleton import ui_state, interface_state +from contact.utilities.singleton import ui_state, interface_state, menu_state def handle_resize(stdscr: curses.window, firstrun: bool) -> None: @@ -341,7 +341,6 @@ def handle_ctrl_t(stdscr: curses.window) -> None: send_traceroute() curses.curs_set(0) # Hide cursor contact.ui.dialog.dialog( - stdscr, f"Traceroute Sent To: {get_name_from_database(ui_state.node_list[ui_state.selected_node])}", "Results will appear in messages window.\nNote: Traceroute is limited to once every 30 seconds.", ) @@ -364,7 +363,10 @@ def handle_backspace(entry_win: curses.window, input_text: str) -> str: def handle_backtick(stdscr: curses.window) -> None: """Handle backtick key events to open the settings menu.""" curses.curs_set(0) + previous_window = ui_state.current_window + ui_state.current_window = 4 settings_menu(stdscr, interface_state.interface) + ui_state.current_window = previous_window curses.curs_set(1) refresh_node_list() handle_resize(stdscr, False) @@ -599,6 +601,8 @@ def draw_messages_window(scroll_to_bottom: bool = False) -> None: refresh_pad(1) draw_packetlog_win() + if ui_state.current_window == 4: + menu_state.need_redraw = True def draw_node_list() -> None: diff --git a/contact/ui/control_ui.py b/contact/ui/control_ui.py index 45c84c1..1e0f4b5 100644 --- a/contact/ui/control_ui.py +++ b/contact/ui/control_ui.py @@ -21,9 +21,7 @@ from contact.ui.dialog import dialog from contact.ui.menus import generate_menu_from_protobuf from contact.ui.nav_utils import move_highlight, draw_arrows, update_help_window from contact.ui.user_config import json_editor -from contact.ui.ui_state import MenuState - -menu_state = MenuState() +from contact.utilities.singleton import menu_state # Constants width = 80 @@ -47,7 +45,9 @@ config_folder = os.path.abspath(config.node_configs_file_path) field_mapping, help_text = parse_ini_file(translation_file) -def display_menu(menu_state: MenuState) -> tuple[object, object]: + +def display_menu() -> 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) @@ -108,7 +108,7 @@ def display_menu(menu_state: MenuState) -> tuple[object, object]: ) # 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_win.refresh() menu_pad.refresh( @@ -134,7 +134,6 @@ def draw_help_window( menu_height: int, max_help_lines: int, transformed_path: List[str], - menu_state: MenuState, ) -> None: global help_win @@ -170,29 +169,32 @@ def settings_menu(stdscr: object, interface: object) -> None: modified_settings = {} - need_redraw = True + menu_state.need_redraw = True menu_state.show_save_option = False while True: - if need_redraw: + if menu_state.need_redraw: + menu_state.need_redraw = False options = list(menu_state.current_menu.keys()) + # Determine if save option should be shown + path = menu_state.menu_path 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(path) > 2 and ("Radio Settings" in path or "Module Settings" in path)) + or (len(path) == 2 and "User Settings" in path) + or (len(path) == 3 and "Channels" in path) ) # Display the menu - menu_win, menu_pad = display_menu(menu_state) + menu_win, menu_pad = display_menu() - need_redraw = False + if menu_win is None: + continue # Skip if menu_win is not initialized - # Capture user input + menu_win.timeout(200) # wait up to 200 ms for a keypress (or less if key is pressed) key = menu_win.getch() + if key == -1: + continue max_index = len(options) + (1 if menu_state.show_save_option else 0) - 1 # max_help_lines = 4 @@ -226,7 +228,7 @@ def settings_menu(stdscr: object, interface: object) -> None: ) elif key == curses.KEY_RESIZE: - need_redraw = True + menu_state.need_redraw = True curses.update_lines_cols() menu_win.erase() @@ -250,7 +252,7 @@ def settings_menu(stdscr: object, interface: object) -> None: ) elif key == curses.KEY_RIGHT or key == ord("\n"): - need_redraw = True + menu_state.need_redraw = True menu_state.start_index.append(0) menu_win.erase() help_win.erase() @@ -392,7 +394,7 @@ def settings_menu(stdscr: object, interface: object) -> None: menu_state.start_index.pop() menu_state.selected_index = 4 continue - # need_redraw = True + # menu_state.need_redraw = True field_info = menu_state.current_menu.get(selected_option) if isinstance(field_info, tuple): @@ -511,7 +513,7 @@ def settings_menu(stdscr: object, interface: object) -> None: menu_state.selected_index = 0 elif key == curses.KEY_LEFT: - need_redraw = True + menu_state.need_redraw = True menu_win.erase() help_win.erase() diff --git a/contact/ui/dialog.py b/contact/ui/dialog.py index 42823d3..fe5efe5 100644 --- a/contact/ui/dialog.py +++ b/contact/ui/dialog.py @@ -1,14 +1,18 @@ import curses from contact.ui.colors import get_color +from contact.utilities.singleton import menu_state, ui_state def dialog(title: str, message: str) -> None: """Display a dialog with a title and message.""" + previous_window = ui_state.current_window + ui_state.current_window = 4 + curses.update_lines_cols() height, width = curses.LINES, curses.COLS - # Calculate dialog dimensions + # Parse message into lines and calculate dimensions message_lines = message.splitlines() max_line_length = max(len(l) for l in message_lines) dialog_width = max(len(title) + 4, max_line_length + 4) @@ -16,36 +20,44 @@ def dialog(title: str, message: str) -> None: x = (width - dialog_width) // 2 y = (height - dialog_height) // 2 - # Create dialog window + def draw_window(): + win.erase() + win.bkgd(get_color("background")) + win.attrset(get_color("window_frame")) + win.border(0) + + win.addstr(0, 2, title, get_color("settings_default")) + + for i, line in enumerate(message_lines): + msg_x = (dialog_width - len(line)) // 2 + win.addstr(2 + i, msg_x, line, get_color("settings_default")) + + ok_text = " Ok " + win.addstr( + dialog_height - 2, + (dialog_width - len(ok_text)) // 2, + ok_text, + get_color("settings_default", reverse=True), + ) + + win.refresh() + win = curses.newwin(dialog_height, dialog_width, y, x) - win.bkgd(get_color("background")) - win.attrset(get_color("window_frame")) - win.border(0) + draw_window() - # Add title - win.addstr(0, 2, title, get_color("settings_default")) - - # Add message (centered) - for i, line in enumerate(message_lines): - msg_x = (dialog_width - len(line)) // 2 - win.addstr(2 + i, msg_x, line, get_color("settings_default")) - - # Add centered OK button - ok_text = " Ok " - win.addstr( - dialog_height - 2, - (dialog_width - len(ok_text)) // 2, - ok_text, - get_color("settings_default", reverse=True), - ) - - # Refresh dialog window - win.refresh() - - # Get user input while True: + win.timeout(200) char = win.getch() - if char in (curses.KEY_ENTER, 10, 13, 32, 27): # Enter, space, or Esc + + if menu_state.need_redraw: + menu_state.need_redraw = False + draw_window() + + if char in (curses.KEY_ENTER, 10, 13, 32, 27): # Enter, space, Esc win.erase() win.refresh() + ui_state.current_window = previous_window return + + if char == -1: + continue diff --git a/contact/ui/ui_state.py b/contact/ui/ui_state.py index 80f9278..b888040 100644 --- a/contact/ui/ui_state.py +++ b/contact/ui/ui_state.py @@ -10,6 +10,7 @@ class MenuState: current_menu: Union[Dict[str, Any], List[Any], str, int] = field(default_factory=dict) menu_path: List[str] = field(default_factory=list) show_save_option: bool = False + need_redraw: bool = False @dataclass diff --git a/contact/ui/user_config.py b/contact/ui/user_config.py index 4a34fc3..8d33394 100644 --- a/contact/ui/user_config.py +++ b/contact/ui/user_config.py @@ -7,6 +7,7 @@ from contact.ui.colors import get_color, setup_colors, COLOR_MAP import contact.ui.default_config as config from contact.ui.nav_utils import move_highlight, draw_arrows from contact.utilities.input_handlers import get_list_input +from contact.utilities.singleton import menu_state width = 80 @@ -108,7 +109,7 @@ def edit_value(key: str, current_value: str) -> str: return user_input if user_input else current_value -def display_menu(menu_state: Any) -> tuple[Any, Any, List[str]]: +def display_menu() -> tuple[Any, Any, List[str]]: """ Render the configuration menu with a Save button directly added to the window. """ @@ -213,14 +214,14 @@ def json_editor(stdscr: curses.window, menu_state: Any) -> None: menu_state.current_menu = data # Track the current level of the menu # Render the menu - menu_win, menu_pad, options = display_menu(menu_state) - need_redraw = True + menu_win, menu_pad, options = display_menu() + menu_state.need_redraw = True while True: - if need_redraw: - menu_win, menu_pad, options = display_menu(menu_state) + if menu_state.need_redraw == True: + menu_win, menu_pad, options = display_menu() menu_win.refresh() - need_redraw = False + menu_state.need_redraw = False max_index = len(options) + (1 if menu_state.show_save_option else 0) - 1 key = menu_win.getch() @@ -250,7 +251,7 @@ def json_editor(stdscr: curses.window, menu_state: Any) -> None: elif key in (curses.KEY_RIGHT, 10, 13): # 10 = \n, 13 = carriage return - need_redraw = True + menu_state.need_redraw = True menu_win.erase() menu_win.refresh() @@ -289,7 +290,7 @@ def json_editor(stdscr: curses.window, menu_state: Any) -> None: menu_state.menu_index.pop() menu_state.start_index.pop() menu_state.current_menu[selected_key] = new_value - need_redraw = True + menu_state.need_redraw = True else: # Save button selected @@ -300,7 +301,7 @@ def json_editor(stdscr: curses.window, menu_state: Any) -> None: elif key in (27, curses.KEY_LEFT): # Escape or Left Arrow - need_redraw = True + menu_state.need_redraw = True menu_win.erase() menu_win.refresh() diff --git a/contact/utilities/singleton.py b/contact/utilities/singleton.py index 17fb99b..8fca9c3 100644 --- a/contact/utilities/singleton.py +++ b/contact/utilities/singleton.py @@ -1,5 +1,6 @@ -from contact.ui.ui_state import ChatUIState, InterfaceState, AppState +from contact.ui.ui_state import ChatUIState, InterfaceState, AppState, MenuState ui_state = ChatUIState() interface_state = InterfaceState() app_state = AppState() +menu_state = MenuState()