From 9806333aa6c41f81ec635d3d7054c2c70896a9e9 Mon Sep 17 00:00:00 2001 From: Pavel Baksy Date: Sat, 20 Dec 2025 17:52:59 +0100 Subject: [PATCH] Add response body color highlighting --- src/window.py | 156 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 141 insertions(+), 15 deletions(-) diff --git a/src/window.py b/src/window.py index cf14ad1..8660992 100644 --- a/src/window.py +++ b/src/window.py @@ -17,7 +17,9 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -from gi.repository import Adw, Gtk, GLib, Gio +import gi +gi.require_version('GtkSource', '5') +from gi.repository import Adw, Gtk, GLib, Gio, GtkSource from .models import HttpRequest, HttpResponse, HistoryEntry from .http_client import HttpClient from .history_manager import HistoryManager @@ -28,6 +30,8 @@ from .widgets.history_item import HistoryItem from .widgets.project_item import ProjectItem from datetime import datetime import threading +import json +import xml.dom.minidom @Gtk.Template(resource_path='/cz/vesp/roster/main-window.ui') @@ -95,6 +99,39 @@ class RosterWindow(Adw.ApplicationWindow): self.split_pane.set_position(allocation.width // 2) return False # Don't repeat + def _setup_sourceview_theme(self): + """Set up GtkSourceView theme based on system color scheme.""" + # Get the style manager to detect dark mode + style_manager = Adw.StyleManager.get_default() + is_dark = style_manager.get_dark() + + # Get the appropriate color scheme (using classic for more subtle colors) + # You can change these to customize the color theme: + # - 'classic' / 'classic-dark' (subtle, muted colors) + # - 'Adwaita' / 'Adwaita-dark' (GNOME default) + # - 'tango' (colorful) + # - 'solarized-light' / 'solarized-dark' (popular alternative) + # - 'kate' / 'kate-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.response_body_sourceview.get_buffer().set_style_scheme(scheme) + + # Listen for theme changes + style_manager.connect('notify::dark', self._on_theme_changed) + + def _on_theme_changed(self, style_manager, param): + """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 = style_scheme_manager.get_scheme(scheme_name) + + if scheme: + self.response_body_sourceview.get_buffer().set_style_scheme(scheme) + def _create_actions(self): """Create window-level actions.""" pass @@ -186,17 +223,39 @@ class RosterWindow(Adw.ApplicationWindow): self.response_stack.add_titled(headers_scroll, "headers", "Headers") - # Response Body tab + # Response Body tab with syntax highlighting body_scroll = Gtk.ScrolledWindow() body_scroll.set_vexpand(True) - self.response_body_textview = Gtk.TextView() - self.response_body_textview.set_editable(False) - self.response_body_textview.set_monospace(True) - self.response_body_textview.set_left_margin(12) - self.response_body_textview.set_right_margin(12) - self.response_body_textview.set_top_margin(12) - self.response_body_textview.set_bottom_margin(12) - body_scroll.set_child(self.response_body_textview) + self.response_body_sourceview = GtkSource.View() + self.response_body_sourceview.set_editable(False) + self.response_body_sourceview.set_show_line_numbers(True) + self.response_body_sourceview.set_highlight_current_line(True) + self.response_body_sourceview.set_left_margin(12) + self.response_body_sourceview.set_right_margin(12) + self.response_body_sourceview.set_top_margin(12) + self.response_body_sourceview.set_bottom_margin(12) + + # Set up syntax highlighting with system theme colors + self._setup_sourceview_theme() + + # Set font family and size + # You can customize these values: + # - font-family: Change to any font (e.g., "Monospace", "JetBrains Mono", "Fira Code") + # - font-size: Change to any size (e.g., 10pt, 12pt, 14pt, 16pt) + css_provider = Gtk.CssProvider() + css_provider.load_from_data(b""" + textview { + font-family: "Source Code Pro"; + font-size: 12pt; + line-height: 1,2; + } + """) + self.response_body_sourceview.get_style_context().add_provider( + css_provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ) + + body_scroll.set_child(self.response_body_sourceview) self.response_stack.add_titled(body_scroll, "body", "Body") @@ -294,9 +353,17 @@ class RosterWindow(Adw.ApplicationWindow): buffer = self.response_headers_textview.get_buffer() buffer.set_text(response.headers) - # Update response body - buffer = self.response_body_textview.get_buffer() - buffer.set_text(response.body) + # Extract content-type and format body + content_type = self._extract_content_type(response.headers) + formatted_body = self._format_response_body(response.body, content_type) + + # Update response body with syntax highlighting + source_buffer = self.response_body_sourceview.get_buffer() + source_buffer.set_text(formatted_body) + + # Set language for syntax highlighting + language = self._get_language_from_content_type(content_type) + source_buffer.set_language(language) def _display_error(self, error): """Display error in UI.""" @@ -308,12 +375,71 @@ class RosterWindow(Adw.ApplicationWindow): buffer.set_text("") # Display error in body - buffer = self.response_body_textview.get_buffer() - buffer.set_text(error) + source_buffer = self.response_body_sourceview.get_buffer() + source_buffer.set_text(error) + source_buffer.set_language(None) # No syntax highlighting for errors # Show toast self._show_toast(f"Request failed: {error}") + def _extract_content_type(self, headers_text): + """Extract content-type from response headers.""" + if not headers_text: + return "" + + # Parse headers line by line + for line in headers_text.split('\n'): + line = line.strip() + if line.lower().startswith('content-type:'): + # Extract value after colon + return line.split(':', 1)[1].strip().lower() + + return "" + + def _get_language_from_content_type(self, content_type): + """Get GtkSourceView language ID from content type.""" + if not content_type: + return None + + language_manager = GtkSource.LanguageManager.get_default() + + # Map content types to language IDs + if 'application/json' in content_type or 'text/json' in content_type: + return language_manager.get_language('json') + elif 'application/xml' in content_type or 'text/xml' in content_type: + return language_manager.get_language('xml') + elif 'text/html' in content_type: + return language_manager.get_language('html') + elif 'application/javascript' in content_type or 'text/javascript' in content_type: + return language_manager.get_language('js') + elif 'text/css' in content_type: + return language_manager.get_language('css') + + return None + + def _format_response_body(self, body, content_type): + """Format response body based on content type.""" + if not body or not body.strip(): + return body + + try: + # JSON formatting + if 'application/json' in content_type or 'text/json' in content_type: + parsed = json.loads(body) + return json.dumps(parsed, indent=2, ensure_ascii=False) + + # XML formatting + elif 'application/xml' in content_type or 'text/xml' in content_type: + dom = xml.dom.minidom.parseString(body) + return dom.toprettyxml(indent=" ") + + except Exception as e: + # If formatting fails, return original body + print(f"Failed to format body: {e}") + + # Return original for other content types or if formatting failed + return body + def on_add_header_clicked(self, button): """Add new header row.""" self._add_header_row()