From a91b8dcc995277f7516d329107e804c47b4cae6f Mon Sep 17 00:00:00 2001 From: Ed_ Date: Tue, 17 Mar 2026 23:10:33 -0400 Subject: [PATCH] feat(gui): Refactor text viewer to use rich rendering and toolbar --- src/gui_2.py | 48 ++++++++++++++++++++++++++++------- tests/test_gui_text_viewer.py | 28 ++++++++++++++++++++ 2 files changed, 67 insertions(+), 9 deletions(-) create mode 100644 tests/test_gui_text_viewer.py diff --git a/src/gui_2.py b/src/gui_2.py index 68da9cd..b64aa56 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -40,7 +40,7 @@ else: win32con = None from pydantic import BaseModel -from imgui_bundle import imgui, hello_imgui, immapp, imgui_node_editor as ed +from imgui_bundle import imgui, hello_imgui, immapp, imgui_node_editor as ed, imgui_color_text_edit as ced PROVIDERS: list[str] = ["gemini", "anthropic", "gemini_cli", "deepseek", "minimax"] COMMS_CLAMP_CHARS: int = 300 @@ -107,6 +107,7 @@ class App: self.controller.init_state() self.show_windows.setdefault("Diagnostics", False) self.controller.start_services(self) + self.controller._predefined_callbacks['_render_text_viewer'] = self._render_text_viewer self.show_preset_manager_window = False self.show_tool_preset_manager_window = False self.show_persona_editor_window = False @@ -115,6 +116,7 @@ class App: self.text_viewer_content = '' self.text_viewer_type = 'text' self.text_viewer_wrap = True + self._text_viewer_editor: Optional[ced.TextEditor] = None self.ui_active_tool_preset = "" self.ui_active_bias_profile = "" self.ui_active_persona = "" @@ -286,9 +288,9 @@ class App: f.write(data) # ---------------------------------------------------------------- helpers - def _render_text_viewer(self, label: str, content: str, text_type: str = 'text') -> None: + def _render_text_viewer(self, label: str, content: str, text_type: str = 'text', force_open: bool = False) -> None: self.text_viewer_type = text_type - if imgui.button("[+]##" + str(id(content))): + if imgui.button("[+]##" + str(id(content))) or force_open: self.show_text_viewer = True self.text_viewer_title = label self.text_viewer_content = content @@ -1003,14 +1005,42 @@ class App: expanded, opened = imgui.begin(f"Text Viewer - {self.text_viewer_title}", self.show_text_viewer) self.show_text_viewer = bool(opened) if expanded: - if self.ui_word_wrap: - imgui.begin_child("tv_wrap", imgui.ImVec2(-1, -1), False) - imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) - imgui.text(self.text_viewer_content) - imgui.pop_text_wrap_pos() + # Toolbar + if imgui.button("Copy"): + imgui.set_clipboard_text(self.text_viewer_content) + imgui.same_line() + _, self.text_viewer_wrap = imgui.checkbox("Word Wrap", self.text_viewer_wrap) + imgui.separator() + + renderer = markdown_helper.get_renderer() + tv_type = getattr(self, "text_viewer_type", "text") + + if tv_type == 'markdown': + imgui.begin_child("tv_md_scroll", imgui.ImVec2(-1, -1), True) + markdown_helper.render(self.text_viewer_content, context_id='text_viewer') imgui.end_child() + elif tv_type in renderer._lang_map: + if self._text_viewer_editor is None: + self._text_viewer_editor = ced.TextEditor() + self._text_viewer_editor.set_read_only_enabled(True) + self._text_viewer_editor.set_show_line_numbers_enabled(True) + + # Sync text and language + lang_id = renderer._lang_map[tv_type] + if self._text_viewer_editor.get_text().strip() != self.text_viewer_content.strip(): + self._text_viewer_editor.set_text(self.text_viewer_content) + self._text_viewer_editor.set_language_definition(lang_id) + + self._text_viewer_editor.render('##tv_editor', a_size=imgui.ImVec2(-1, -1)) else: - imgui.input_text_multiline("##tv_c", self.text_viewer_content, imgui.ImVec2(-1, -1), imgui.InputTextFlags_.read_only) + if self.text_viewer_wrap: + imgui.begin_child("tv_wrap", imgui.ImVec2(-1, -1), False) + imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) + imgui.text(self.text_viewer_content) + imgui.pop_text_wrap_pos() + imgui.end_child() + else: + imgui.input_text_multiline("##tv_c", self.text_viewer_content, imgui.ImVec2(-1, -1), imgui.InputTextFlags_.read_only) imgui.end() # Inject File Modal if getattr(self, "show_inject_modal", False): diff --git a/tests/test_gui_text_viewer.py b/tests/test_gui_text_viewer.py new file mode 100644 index 0000000..4054f8c --- /dev/null +++ b/tests/test_gui_text_viewer.py @@ -0,0 +1,28 @@ +import pytest +import time +from src.api_hook_client import ApiHookClient + +def test_text_viewer_state_update(live_gui) -> None: + """ + Verifies that we can set text viewer state and it is reflected in GUI state. + """ + client = ApiHookClient() + label = "Test Viewer Label" + content = "This is test content for the viewer." + text_type = "markdown" + + # Add a task to push a custom callback that mutates the app state + def set_viewer_state(app): + app.show_text_viewer = True + app.text_viewer_title = label + app.text_viewer_content = content + app.text_viewer_type = text_type + + client.push_event("custom_callback", {"callback": set_viewer_state}) + time.sleep(0.5) + + state = client.get_gui_state() + assert state is not None + assert state.get('show_text_viewer') == True + assert state.get('text_viewer_title') == label + assert state.get('text_viewer_type') == text_type