# history_item.py # # Copyright 2025 Pavel Baksy # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # SPDX-License-Identifier: GPL-3.0-or-later from gi.repository import Gtk, GObject, Gdk from datetime import datetime @Gtk.Template(resource_path='/cz/vesp/roster/widgets/history-item.ui') class HistoryItem(Gtk.Box): """Widget for displaying a history entry.""" __gtype_name__ = 'HistoryItem' expand_icon = Gtk.Template.Child() details_revealer = Gtk.Template.Child() method_label = Gtk.Template.Child() url_label = Gtk.Template.Child() timestamp_label = Gtk.Template.Child() status_label = Gtk.Template.Child() delete_button = Gtk.Template.Child() request_headers_label = Gtk.Template.Child() request_body_scroll = Gtk.Template.Child() request_body_text = Gtk.Template.Child() request_expander = Gtk.Template.Child() request_expander_label = Gtk.Template.Child() copy_request_button = Gtk.Template.Child() response_headers_label = Gtk.Template.Child() response_body_scroll = Gtk.Template.Child() response_body_text = Gtk.Template.Child() response_expander = Gtk.Template.Child() response_expander_label = Gtk.Template.Child() copy_response_button = Gtk.Template.Child() load_button = Gtk.Template.Child() __gsignals__ = { 'load-requested': (GObject.SIGNAL_RUN_FIRST, None, ()), 'delete-requested': (GObject.SIGNAL_RUN_FIRST, None, ()) } def __init__(self, entry): super().__init__() self.entry = entry self.expanded = False self.request_expanded = False self.response_expanded = False self._populate_ui() def _populate_ui(self): """Populate UI with entry data.""" # Summary self.method_label.set_text(self.entry.request.method) self.url_label.set_text(self.entry.request.url) # Format timestamp try: dt = datetime.fromisoformat(self.entry.timestamp) timestamp_str = dt.strftime("%H:%M:%S") except: timestamp_str = self.entry.timestamp self.timestamp_label.set_text(timestamp_str) # Status if self.entry.response: status_text = f"{self.entry.response.status_code} {self.entry.response.status_text}" self.status_label.set_text(status_text) elif self.entry.error: self.status_label.set_text("Error") # Details - Request headers headers_str = "\n".join([f"{k}: {v}" for k, v in self.entry.request.headers.items()]) if not headers_str: headers_str = "(no headers)" self.request_headers_label.set_text(headers_str) # Details - Request body body_str = self.entry.request.body if self.entry.request.body else "(empty)" self.request_body_text.get_buffer().set_text(body_str) # Count lines to determine if we need the expander num_lines = body_str.count('\n') + 1 # Show expander if content is longer than ~3 lines (60px) needs_expander = num_lines > 3 or len(body_str) > 150 self.request_expander.set_visible(needs_expander) # Set cursor to pointer for clickability hint if needs_expander: self.request_expander.set_cursor_from_name("pointer") # Details - Response if self.entry.response: self.response_headers_label.set_text(self.entry.response.headers) response_body = self.entry.response.body if self.entry.response.body else "(empty)" self.response_body_text.get_buffer().set_text(response_body) # Count lines to determine if we need the expander num_lines = response_body.count('\n') + 1 needs_expander = num_lines > 3 or len(response_body) > 150 self.response_expander.set_visible(needs_expander) # Set cursor to pointer for clickability hint if needs_expander: self.response_expander.set_cursor_from_name("pointer") elif self.entry.error: self.response_headers_label.set_text("(error)") error_text = self.entry.error self.response_body_text.get_buffer().set_text(error_text) # Show expander for long errors num_lines = error_text.count('\n') + 1 needs_expander = num_lines > 3 or len(error_text) > 150 self.response_expander.set_visible(needs_expander) # Set cursor to pointer for clickability hint if needs_expander: self.response_expander.set_cursor_from_name("pointer") @Gtk.Template.Callback() def on_clicked(self, gesture, n_press, x, y): """Toggle expansion when clicked.""" self.toggle_expanded() @Gtk.Template.Callback() def on_load_clicked(self, button): """Emit load signal when load button clicked.""" self.emit('load-requested') @Gtk.Template.Callback() def on_delete_clicked(self, button): """Emit delete signal when delete button clicked.""" self.emit('delete-requested') @Gtk.Template.Callback() def on_request_expander_clicked(self, gesture, n_press, x, y): """Toggle request body expansion.""" self.request_expanded = not self.request_expanded if self.request_expanded: # Expand to show full scrollable content # Set max first, then min to avoid assertion errors self.request_body_scroll.set_max_content_height(300) self.request_body_scroll.set_min_content_height(300) self.request_expander_label.set_text("Collapse request") else: # Collapse to show only few lines # Set min first, then max to avoid assertion errors self.request_body_scroll.set_min_content_height(60) self.request_body_scroll.set_max_content_height(60) self.request_expander_label.set_text("Show full request") @Gtk.Template.Callback() def on_response_expander_clicked(self, gesture, n_press, x, y): """Toggle response body expansion.""" self.response_expanded = not self.response_expanded if self.response_expanded: # Expand to show full scrollable content # Set max first, then min to avoid assertion errors self.response_body_scroll.set_max_content_height(300) self.response_body_scroll.set_min_content_height(300) self.response_expander_label.set_text("Collapse response") else: # Collapse to show only few lines # Set min first, then max to avoid assertion errors self.response_body_scroll.set_min_content_height(60) self.response_body_scroll.set_max_content_height(60) self.response_expander_label.set_text("Show full response") @Gtk.Template.Callback() def on_copy_request_clicked(self, button): """Copy full request to clipboard.""" # Build full request text request_text = f"{self.entry.request.method} {self.entry.request.url}\n\n" # Add headers if self.entry.request.headers: request_text += "Headers:\n" for key, value in self.entry.request.headers.items(): request_text += f"{key}: {value}\n" request_text += "\n" # Add body if self.entry.request.body: request_text += "Body:\n" request_text += self.entry.request.body # Copy to clipboard clipboard = Gdk.Display.get_default().get_clipboard() clipboard.set(request_text) @Gtk.Template.Callback() def on_copy_response_clicked(self, button): """Copy full response to clipboard.""" if self.entry.response: # Build full response text response_text = f"{self.entry.response.status_code} {self.entry.response.status_text}\n\n" # Add headers if self.entry.response.headers: response_text += self.entry.response.headers response_text += "\n\n" # Add body if self.entry.response.body: response_text += self.entry.response.body elif self.entry.error: response_text = f"Error:\n{self.entry.error}" else: response_text = "(no response)" # Copy to clipboard clipboard = Gdk.Display.get_default().get_clipboard() clipboard.set(response_text) def toggle_expanded(self): """Toggle between collapsed and expanded view.""" self.expanded = not self.expanded self.details_revealer.set_reveal_child(self.expanded) # Update icon if self.expanded: self.expand_icon.set_from_icon_name('go-down-symbolic') else: self.expand_icon.set_from_icon_name('go-next-symbolic')