From fffc3e3e97a498884a85f9ed2fee4eadaf190b02 Mon Sep 17 00:00:00 2001 From: pdxlocations Date: Mon, 19 Oct 2026 15:27:40 -0700 Subject: [PATCH] Fix single pane message crash: Refactor message rendering in draw_messages_window and add test for single pane typing behavior --- contact/ui/contact_ui.py | 13 ++++++---- tests/test_contact_ui.py | 52 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/contact/ui/contact_ui.py b/contact/ui/contact_ui.py index 72385ea..3552596 100644 --- a/contact/ui/contact_ui.py +++ b/contact/ui/contact_ui.py @@ -1651,14 +1651,17 @@ def draw_messages_window(scroll_to_bottom: bool = False, preserve_selection: boo return messages_pad.erase() + # Erasing the pad also removes the old highlight. Its row range may no + # longer exist after switching conversations or rewrapping messages. + ui_state.highlighted_message_range = () channel = ui_state.channel_list[ui_state.selected_channel] msg_line_count = 0 message_ranges = [] + rendered_lines = [] if channel in ui_state.all_messages: messages = ui_state.all_messages[channel] - rendered_lines = [] for prefix, message in messages: start_line = len(rendered_lines) full_message = normalize_message_text(f"{prefix}{message}") @@ -1677,10 +1680,10 @@ def draw_messages_window(scroll_to_bottom: bool = False, preserve_selection: boo message_ranges.append((start_line, len(rendered_lines), color)) msg_line_count = len(rendered_lines) - messages_pad.resize(max(1, msg_line_count), messages_win.getmaxyx()[1]) - for row, (line, color) in enumerate(rendered_lines): - messages_pad.addstr(row, 1, line, color) - ui_state.message_line_ranges[channel] = message_ranges + messages_pad.resize(max(1, msg_line_count), messages_win.getmaxyx()[1]) + for row, (line, color) in enumerate(rendered_lines): + messages_pad.addstr(row, 1, line, color) + ui_state.message_line_ranges[channel] = message_ranges paint_frame(messages_win, selected=(ui_state.current_window == 1), refresh=False) diff --git a/tests/test_contact_ui.py b/tests/test_contact_ui.py index d7c554e..4b642e8 100644 --- a/tests/test_contact_ui.py +++ b/tests/test_contact_ui.py @@ -214,6 +214,58 @@ class ContactUiTests(unittest.TestCase): contact_ui.messages_pad.resize.assert_called_once_with(2, 40) + def test_single_pane_typing_after_switching_to_shorter_conversation(self) -> None: + for messages in ([("RX: ", "hello")], [], None): + with self.subTest(messages=messages): + reset_shared_state() + ui_state.single_pane_mode = True + ui_state.current_window = 0 + ui_state.channel_list = [123] + ui_state.all_messages = {123: messages} if messages is not None else {} + ui_state.highlighted_message_range = (10, 12, 20) + ui_state.message_line_ranges = {123: [(10, 12, 20)]} + pad = mock.Mock() + pad.getmaxyx.return_value = (12, 40) + + def resize_pad(height, width): + pad.getmaxyx.return_value = (height, width) + + def change_attributes(row, column, width, color): + height, columns = pad.getmaxyx() + if not (0 <= row < height and 0 <= column < columns and column + width <= columns): + raise contact_ui.curses.error("chgat() returned ERR") + + pad.resize.side_effect = resize_pad + pad.chgat.side_effect = change_attributes + win = mock.Mock() + win.getmaxyx.return_value = (10, 40) + packetlog = mock.Mock() + packetlog.getmaxyx.return_value = (1, 40) + with ExitStack() as stack: + for name, value in (("messages_pad", pad), ("messages_win", win), ("packetlog_win", packetlog)): + stack.enter_context(mock.patch.object(contact_ui, name, value, create=True)) + for name in ("paint_frame", "refresh_pad", "draw_packetlog_win", "draw_window_arrows", "refresh_main_window"): + stack.enter_context(mock.patch.object(contact_ui, name)) + stack.enter_context(mock.patch.object(contact_ui, "get_color", return_value=10)) + stack.enter_context(mock.patch.object( + contact_ui, "handle_resize", side_effect=lambda *args: contact_ui.draw_messages_window(True) + )) + self.assertEqual(contact_ui.insert_input_text("", "a"), "a") + + self.assertEqual(ui_state.current_window, 1) + self.assertTrue(ui_state.input_focused) + self.assertEqual(ui_state.highlighted_message_range, ()) + pad.resize.assert_called_once_with(1, 40) + if messages: + self.assertEqual(ui_state.message_line_ranges[123], [(0, 1, 10)]) + self.assertEqual(pad.chgat.call_args_list, [ + mock.call(0, 1, 38, 10 | contact_ui.curses.A_REVERSE), + mock.call(0, 1, 38, 10), + ]) + else: + self.assertEqual(ui_state.message_line_ranges[123], []) + pad.chgat.assert_not_called() + def test_build_reply_prefix_includes_sender_and_five_character_excerpt(self) -> None: reply = contact_ui.build_reply_prefix( "[06:27:25] >> [6] B1G1: ",