From c9a01bf31ef26d633a1d1bb476f70fc50df370e9 Mon Sep 17 00:00:00 2001 From: vesp Date: Sat, 20 Dec 2025 18:17:22 +0100 Subject: [PATCH] Add syntax highlighting and formatting for request/response bodies - Add automatic JSON and XML formatting for response bodies - Replace TextView with GtkSourceView for syntax highlighting - Add syntax selector dropdown (RAW/JSON/XML) for request body - Use Source Code Pro font (12pt) for better readability - Integrate with GNOME theme (Adwaita light/dark) - Add line numbers and current line highlighting - Persist syntax selection when saving/loading requests - Maintain backward compatibility with existing saved requests --- src/models.py | 4 ++ src/window.py | 133 ++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 122 insertions(+), 15 deletions(-) diff --git a/src/models.py b/src/models.py index 433a741..0b58c4b 100644 --- a/src/models.py +++ b/src/models.py @@ -28,6 +28,7 @@ class HttpRequest: url: str headers: Dict[str, str] # Key-value pairs body: str # Raw text body + syntax: str = "RAW" # Syntax highlighting: "RAW", "JSON", or "XML" def to_dict(self): """Convert to dictionary for JSON serialization.""" @@ -36,6 +37,9 @@ class HttpRequest: @classmethod def from_dict(cls, data): """Create instance from dictionary.""" + # Provide default for syntax field for backward compatibility + if 'syntax' not in data: + data = {**data, 'syntax': 'RAW'} return cls(**data) diff --git a/src/window.py b/src/window.py index 8660992..f2026b6 100644 --- a/src/window.py +++ b/src/window.py @@ -126,11 +126,41 @@ class RosterWindow(Adw.ApplicationWindow): """Handle system theme changes.""" is_dark = style_manager.get_dark() style_scheme_manager = GtkSource.StyleSchemeManager.get_default() - scheme_name = 'classic-dark' if is_dark else 'classic' + scheme_name = 'Adwaita-dark' if is_dark else 'Adwaita' scheme = style_scheme_manager.get_scheme(scheme_name) if scheme: self.response_body_sourceview.get_buffer().set_style_scheme(scheme) + # Also update request body theme + self.body_sourceview.get_buffer().set_style_scheme(scheme) + + def _setup_request_body_theme(self): + """Set up GtkSourceView theme for request body.""" + style_manager = Adw.StyleManager.get_default() + is_dark = style_manager.get_dark() + + style_scheme_manager = GtkSource.StyleSchemeManager.get_default() + scheme_name = 'Adwaita-dark' if is_dark else 'Adwaita' + scheme = style_scheme_manager.get_scheme(scheme_name) + + if scheme: + self.body_sourceview.get_buffer().set_style_scheme(scheme) + + def _on_request_body_language_changed(self, dropdown, param): + """Handle request body language selection change.""" + selected = dropdown.get_selected() + language_manager = GtkSource.LanguageManager.get_default() + + buffer = self.body_sourceview.get_buffer() + + if selected == 0: # RAW + buffer.set_language(None) + elif selected == 1: # JSON + language = language_manager.get_language('json') + buffer.set_language(language) + elif selected == 2: # XML + language = language_manager.get_language('xml') + buffer.set_language(language) def _create_actions(self): """Create window-level actions.""" @@ -181,18 +211,70 @@ class RosterWindow(Adw.ApplicationWindow): self.request_stack.add_titled(headers_box, "headers", "Headers") - # Body tab + # Body tab with syntax highlighting + body_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + + # SourceView for request body body_scroll = Gtk.ScrolledWindow() body_scroll.set_vexpand(True) - self.body_textview = Gtk.TextView() - self.body_textview.set_monospace(True) - self.body_textview.set_left_margin(12) - self.body_textview.set_right_margin(12) - self.body_textview.set_top_margin(12) - self.body_textview.set_bottom_margin(12) - body_scroll.set_child(self.body_textview) + self.body_sourceview = GtkSource.View() + self.body_sourceview.set_editable(True) + self.body_sourceview.set_show_line_numbers(True) + self.body_sourceview.set_highlight_current_line(True) + self.body_sourceview.set_left_margin(12) + self.body_sourceview.set_right_margin(12) + self.body_sourceview.set_top_margin(12) + self.body_sourceview.set_bottom_margin(12) - self.request_stack.add_titled(body_scroll, "body", "Body") + # Apply same font styling as response body + css_provider = Gtk.CssProvider() + css_provider.load_from_data(b""" + textview { + font-family: "Source Code Pro"; + font-size: 12pt; + line-height: 1.2; + } + """) + self.body_sourceview.get_style_context().add_provider( + css_provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ) + + # Set up theme for request body + self._setup_request_body_theme() + + body_scroll.set_child(self.body_sourceview) + body_box.append(body_scroll) + + # Bottom toolbar with language selector + toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + toolbar.set_margin_start(12) + toolbar.set_margin_end(12) + toolbar.set_margin_top(6) + toolbar.set_margin_bottom(6) + + # Spacer to push dropdown to the right + spacer = Gtk.Box() + spacer.set_hexpand(True) + toolbar.append(spacer) + + # Language selector label and dropdown + lang_label = Gtk.Label(label="Syntax:") + toolbar.append(lang_label) + + lang_list = Gtk.StringList() + lang_list.append("RAW") + lang_list.append("JSON") + lang_list.append("XML") + + self.body_language_dropdown = Gtk.DropDown(model=lang_list) + self.body_language_dropdown.set_selected(0) # Default to RAW + self.body_language_dropdown.connect("notify::selected", self._on_request_body_language_changed) + toolbar.append(self.body_language_dropdown) + + body_box.append(toolbar) + + self.request_stack.add_titled(body_box, "body", "Body") def _setup_response_tabs(self): """Create response tabs programmatically.""" @@ -247,7 +329,7 @@ class RosterWindow(Adw.ApplicationWindow): textview { font-family: "Source Code Pro"; font-size: 12pt; - line-height: 1,2; + line-height: 1.2; } """) self.response_body_sourceview.get_style_context().add_provider( @@ -334,10 +416,15 @@ class RosterWindow(Adw.ApplicationWindow): child = child.get_next_sibling() # Get body - buffer = self.body_textview.get_buffer() + buffer = self.body_sourceview.get_buffer() body = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), False) - return HttpRequest(method=method, url=url, headers=headers, body=body) + # Get syntax selection + syntax_index = self.body_language_dropdown.get_selected() + syntax_options = ["RAW", "JSON", "XML"] + syntax = syntax_options[syntax_index] + + return HttpRequest(method=method, url=url, headers=headers, body=body, syntax=syntax) def _display_response(self, response): """Display response in UI.""" @@ -522,9 +609,17 @@ class RosterWindow(Adw.ApplicationWindow): self._add_header_row() # Set body - buffer = self.body_textview.get_buffer() + buffer = self.body_sourceview.get_buffer() buffer.set_text(request.body) + # Restore syntax selection + syntax_options = ["RAW", "JSON", "XML"] + syntax = getattr(request, 'syntax', 'RAW') # Default to RAW for old requests + if syntax in syntax_options: + self.body_language_dropdown.set_selected(syntax_options.index(syntax)) + else: + self.body_language_dropdown.set_selected(0) # Default to RAW + # Switch to headers tab self.request_stack.set_visible_child_name("headers") @@ -767,9 +862,17 @@ class RosterWindow(Adw.ApplicationWindow): self._add_header_row() # Set body - buffer = self.body_textview.get_buffer() + buffer = self.body_sourceview.get_buffer() buffer.set_text(req.body) + # Restore syntax selection + syntax_options = ["RAW", "JSON", "XML"] + syntax = getattr(req, 'syntax', 'RAW') # Default to RAW for old requests + if syntax in syntax_options: + self.body_language_dropdown.set_selected(syntax_options.index(syntax)) + else: + self.body_language_dropdown.set_selected(0) # Default to RAW + # Switch to headers tab self.request_stack.set_visible_child_name("headers")