diff --git a/src/window.py b/src/window.py index a2784a7..5aa1dbb 100644 --- a/src/window.py +++ b/src/window.py @@ -344,407 +344,50 @@ class RosterWindow(Adw.ApplicationWindow): self.add_action(action) self.get_application().set_accels_for_action("win.save-request", ["s"]) - def _setup_method_dropdown(self): - """Populate HTTP method dropdown.""" - methods = Gtk.StringList() - methods.append("GET") - methods.append("POST") - methods.append("PUT") - methods.append("DELETE") - self.method_dropdown.set_model(methods) - self.method_dropdown.set_selected(0) # Default to GET - - def _setup_request_tabs(self): - """Create request tabs programmatically.""" - # Create stack for switching between pages - self.request_stack = Gtk.Stack() - self.request_stack.set_vexpand(True) - self.request_stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE) - self.request_stack.set_transition_duration(150) - - # Create stack switcher for the tabs - request_switcher = Gtk.StackSwitcher() - request_switcher.set_stack(self.request_stack) - request_switcher.set_halign(Gtk.Align.CENTER) - request_switcher.set_margin_top(8) - request_switcher.set_margin_bottom(8) - - # Add switcher and stack to container - self.request_tabs_container.append(request_switcher) - self.request_tabs_container.append(self.request_stack) - - # Headers tab - headers_scroll = Gtk.ScrolledWindow() - headers_scroll.set_vexpand(True) # Expand to fill available space - headers_scroll.set_min_content_height(150) # Show ~3-4 rows minimum - self.headers_listbox = Gtk.ListBox() - self.headers_listbox.add_css_class("boxed-list") - headers_scroll.set_child(self.headers_listbox) - - # Add header button container - headers_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - headers_box.set_margin_start(12) - headers_box.set_margin_end(12) - headers_box.set_margin_top(12) - headers_box.set_margin_bottom(12) - headers_box.append(headers_scroll) - - self.add_header_button = Gtk.Button(label="Add Header") - self.add_header_button.connect("clicked", self.on_add_header_clicked) - headers_box.append(self.add_header_button) - - self.request_stack.add_titled(headers_box, "headers", "Headers") - - # 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_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) - - # 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.""" - # Create stack for switching between pages - self.response_stack = Gtk.Stack() - self.response_stack.set_vexpand(True) - self.response_stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE) - self.response_stack.set_transition_duration(150) - - # Create stack switcher for the tabs - response_switcher = Gtk.StackSwitcher() - response_switcher.set_stack(self.response_stack) - response_switcher.set_halign(Gtk.Align.CENTER) - response_switcher.set_margin_top(8) - response_switcher.set_margin_bottom(8) - - # Add switcher and stack to container - self.response_tabs_container.append(response_switcher) - self.response_tabs_container.append(self.response_stack) - - # Response Headers tab - headers_scroll = Gtk.ScrolledWindow() - headers_scroll.set_vexpand(True) - self.response_headers_textview = Gtk.TextView() - self.response_headers_textview.set_editable(False) - self.response_headers_textview.set_monospace(True) - self.response_headers_textview.set_left_margin(12) - self.response_headers_textview.set_right_margin(12) - self.response_headers_textview.set_top_margin(12) - self.response_headers_textview.set_bottom_margin(12) - headers_scroll.set_child(self.response_headers_textview) - - self.response_stack.add_titled(headers_scroll, "headers", "Headers") - - # Response Body tab with syntax highlighting - body_scroll = Gtk.ScrolledWindow() - body_scroll.set_vexpand(True) - 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") - - @Gtk.Template.Callback() - def on_send_clicked(self, button): - """Handle Send button click.""" + def _on_send_clicked(self, widget): + """Handle Send button click from a tab widget.""" # Validate URL - url = self.url_entry.get_text().strip() - if not url: + request = widget.get_request() + if not request.url.strip(): self._show_toast("Please enter a URL") return - # Build request from UI - request = self._build_request_from_ui() - # Disable send button during request - self.send_button.set_sensitive(False) - self.send_button.set_label("Sending...") - self.status_label.set_text("Sending...") - self.time_label.set_text("") + widget.send_button.set_sensitive(False) + widget.send_button.set_label("Sending...") - # Execute async (no threading needed!) - self.http_client.execute_request_async( - request, - self._handle_response_callback, - request # user_data - ) + # Execute async + def callback(response, error, user_data): + """Callback runs on main thread.""" + # Re-enable send button + widget.send_button.set_sensitive(True) + widget.send_button.set_label("Send") - def _handle_response_callback(self, response, error, request): - """Callback runs on main thread automatically.""" - self._handle_response(request, response, error) + # Update tab with response + if response: + widget.display_response(response) + else: + widget.display_error(error or "Unknown error") - def _handle_response(self, request, response, error): - """Handle response in main thread.""" - # Re-enable send button - self.send_button.set_sensitive(True) - self.send_button.set_label("Send") + # Save response to tab + page = self.tab_view.get_selected_page() + if page: + tab = self.page_to_tab.get(page) + if tab: + tab.response = response - # Save response to current tab - if self.current_tab_id: - current_tab = self.tab_manager.get_tab_by_id(self.current_tab_id) - if current_tab: - current_tab.response = response + # Create history entry + entry = HistoryEntry( + timestamp=datetime.now().isoformat(), + request=request, + response=response, + error=error + ) + self.history_manager.add_entry(entry) - # Create history entry - entry = HistoryEntry( - timestamp=datetime.now().isoformat(), - request=request, - response=response, - error=error - ) - - # Save to history - self.history_manager.add_entry(entry) - - # Update UI - if response: - self._display_response(response) - else: - self._display_error(error) - - # Refresh history list - self._load_history() - - def _build_request_from_ui(self): - """Build HttpRequest from UI inputs.""" - method = self.method_dropdown.get_selected_item().get_string() - url = self.url_entry.get_text().strip() - - # Collect headers - headers = {} - child = self.headers_listbox.get_first_child() - while child is not None: - # In GTK4, ListBox wraps children in ListBoxRow, so we need to get the actual child - if isinstance(child, Gtk.ListBoxRow): - header_row = child.get_child() - if isinstance(header_row, HeaderRow): - key, value = header_row.get_header() - if key and value: - headers[key] = value - child = child.get_next_sibling() - - # Get body - buffer = self.body_sourceview.get_buffer() - body = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), False) - - # 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.""" - # Update status - status_text = f"{response.status_code} {response.status_text}" - self.status_label.set_text(status_text) - - # Update time - time_text = f"{response.response_time_ms:.0f} ms" - self.time_label.set_text(time_text) - - # Update response headers - buffer = self.response_headers_textview.get_buffer() - buffer.set_text(response.headers) - - # 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.""" - self.status_label.set_text("Error") - self.time_label.set_text("") - - # Clear response headers - buffer = self.response_headers_textview.get_buffer() - buffer.set_text("") - - # Display error in body - 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() - - def _add_header_row(self, key='', value=''): - """Add a header row to the list.""" - row = HeaderRow() - row.set_header(key, value) - row.connect('remove-requested', self._on_header_remove) - row.connect('changed', self._on_request_changed) - self.headers_listbox.append(row) - - # Mark as changed if we're adding a non-empty header - if key or value: - self._on_request_changed(None) - - def _on_header_remove(self, header_row): - """Handle header row removal.""" - # In GTK4, we need to remove the parent ListBoxRow, not the HeaderRow itself - parent = header_row.get_parent() - if parent: - self.headers_listbox.remove(parent) - # Mark as changed - self._on_request_changed(None) + self.http_client.execute_request_async(request, callback, None) + # History and Project Management def _load_history(self): """Load history from file and populate list.""" # Clear existing history items @@ -805,9 +448,6 @@ class RosterWindow(Adw.ApplicationWindow): saved_request_id=None ) - # Switch to headers tab - self.request_stack.set_visible_child_name("headers") - self._show_toast("Request loaded from history") def _show_toast(self, message): @@ -1047,9 +687,6 @@ class RosterWindow(Adw.ApplicationWindow): syntax=request.syntax ) - # Update tab indicator to remove modified marker - self._update_tab_indicator(current_tab) - def _show_overwrite_dialog(self, project, name, existing_request_id, request): """Show dialog asking if user wants to overwrite existing request.""" dialog = Adw.AlertDialog() @@ -1184,10 +821,6 @@ class RosterWindow(Adw.ApplicationWindow): if new_tab: new_tab.original_request = None new_tab.modified = True - self._update_tab_indicator(new_tab) - - # Switch to headers tab - self.request_stack.set_visible_child_name("headers") def _on_delete_request(self, widget, saved_request, project): """Delete saved request with confirmation."""