diff --git a/contact/ui/contact_ui.py b/contact/ui/contact_ui.py index 596a519..54b742d 100644 --- a/contact/ui/contact_ui.py +++ b/contact/ui/contact_ui.py @@ -1134,74 +1134,83 @@ def handle_backtick(stdscr: curses.window) -> None: handle_resize(stdscr, False) return - options = ["Local settings"] - remote_node_num = None + selected_node_num = None if ui_state.node_list: - remote_node_num = ui_state.node_list[ui_state.selected_node] - options.append(f"Remote admin: {get_name_from_database(remote_node_num)}") - choice = get_list_input("Open settings for", None, options) - if choice == "Local settings": - settings_menu(stdscr, interface) - elif remote_node_num is not None: - wait_win = None + selected_node_num = ui_state.node_list[ui_state.selected_node] - def remote_status(message: Optional[str]) -> None: - nonlocal wait_win - if message is None: + # The local node is already the target for normal settings; do not offer a + # misleading remote-admin choice when it is selected in the node pane. + if selected_node_num == interface_state.myNodeNum: + settings_menu(stdscr, interface) + else: + options = ["Local settings"] + remote_node_num = None + if selected_node_num is not None: + remote_node_num = selected_node_num + options.append(f"Remote admin: {get_name_from_database(remote_node_num)}") + choice = get_list_input("Open settings for", None, options) + if choice == "Local settings": + settings_menu(stdscr, interface) + elif remote_node_num is not None: + wait_win = None + + def remote_status(message: Optional[str]) -> None: + nonlocal wait_win + if message is None: + if wait_win is not None: + wait_win.erase() + wait_win.refresh() + wait_win = None + return + if wait_win is None: + wait_win = show_remote_admin_wait(stdscr, get_name_from_database(remote_node_num)) + update_remote_admin_wait(wait_win, message) + + def remote_cancel_requested() -> bool: + if wait_win is None: + return False + try: + wait_win.timeout(0) + return wait_win.getch() == 27 + finally: + wait_win.timeout(-1) + + try: + remote_node = interface.getNode(remote_node_num, False) + verify_remote_admin( + remote_node, + status_callback=remote_status, + cancel_callback=remote_cancel_requested, + ) + remote_status(None) + settings_menu( + stdscr, + interface, + # Settings requests configs explicitly; avoid getNode's + # implicit channel request racing that setup. + node=remote_node, + remote=True, + status_callback=remote_status, + cancel_callback=remote_cancel_requested, + ) + except RemoteAdminCancelled: + logging.info("Remote admin request cancelled for %s", remote_node_num) + remote_status(None) + except (SystemExit, PermissionError) as exc: + logging.warning("Remote admin was rejected for %s: %s", remote_node_num, exc) if wait_win is not None: wait_win.erase() wait_win.refresh() - wait_win = None - return - if wait_win is None: - wait_win = show_remote_admin_wait(stdscr, get_name_from_database(remote_node_num)) - update_remote_admin_wait(wait_win, message) - - def remote_cancel_requested() -> bool: - if wait_win is None: - return False - try: - wait_win.timeout(0) - return wait_win.getch() == 27 - finally: - wait_win.timeout(-1) - - try: - remote_node = interface.getNode(remote_node_num, False) - verify_remote_admin( - remote_node, - status_callback=remote_status, - cancel_callback=remote_cancel_requested, - ) - remote_status(None) - settings_menu( - stdscr, - interface, - # Settings requests configs explicitly; avoid getNode's - # implicit channel request racing that setup. - node=remote_node, - remote=True, - status_callback=remote_status, - cancel_callback=remote_cancel_requested, - ) - except RemoteAdminCancelled: - logging.info("Remote admin request cancelled for %s", remote_node_num) - remote_status(None) - except (SystemExit, PermissionError) as exc: - logging.warning("Remote admin was rejected for %s: %s", remote_node_num, exc) - if wait_win is not None: - wait_win.erase() - wait_win.refresh() - contact.ui.dialog.dialog( - "Remote admin rejected", - "The selected node did not authorize this admin request.", - ) - except Exception as exc: - logging.exception("Remote admin failed for %s", remote_node_num) - if wait_win is not None: - wait_win.erase() - wait_win.refresh() - contact.ui.dialog.dialog("Remote admin failed", str(exc)) + contact.ui.dialog.dialog( + "Remote admin rejected", + "The selected node did not authorize this admin request.", + ) + except Exception as exc: + logging.exception("Remote admin failed for %s", remote_node_num) + if wait_win is not None: + wait_win.erase() + wait_win.refresh() + contact.ui.dialog.dialog("Remote admin failed", str(exc)) ui_state.current_window = previous_window ui_state.single_pane_mode = config.single_pane_mode.lower() == "true" curses.curs_set(1) diff --git a/tests/test_contact_ui.py b/tests/test_contact_ui.py index 2386d5e..1cdad8f 100644 --- a/tests/test_contact_ui.py +++ b/tests/test_contact_ui.py @@ -64,6 +64,25 @@ class ContactUiTests(unittest.TestCase): ) settings_menu.assert_not_called() + def test_handle_backtick_opens_local_settings_for_selected_local_node(self) -> None: + stdscr = mock.Mock() + ui_state.current_window = 2 + ui_state.node_list = [123] + ui_state.selected_node = 0 + contact_ui.interface_state.myNodeNum = 123 + contact_ui.interface_state.interface = mock.Mock() + + with mock.patch.object(contact_ui.curses, "curs_set"): + with mock.patch.object(contact_ui, "get_list_input") as chooser: + with mock.patch.object(contact_ui, "settings_menu") as settings_menu: + with mock.patch.object(contact_ui, "get_channels"): + with mock.patch.object(contact_ui, "refresh_node_list"): + with mock.patch.object(contact_ui, "handle_resize"): + contact_ui.handle_backtick(stdscr) + + chooser.assert_not_called() + settings_menu.assert_called_once_with(stdscr, contact_ui.interface_state.interface) + def test_show_remote_admin_wait_draws_status(self) -> None: stdscr = mock.Mock() stdscr.getmaxyx.return_value = (24, 100)