# window.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
import gi
gi.require_version('GtkSource', '5')
from gi.repository import Adw, Gtk, GLib, Gio, GtkSource
from .models import HttpRequest, HttpResponse, HistoryEntry, RequestTab
from .http_client import HttpClient
from .history_manager import HistoryManager
from .project_manager import ProjectManager
from .tab_manager import TabManager
from .icon_picker_dialog import IconPickerDialog
from .widgets.header_row import HeaderRow
from .widgets.history_item import HistoryItem
from .widgets.project_item import ProjectItem
from datetime import datetime
import threading
import json
import xml.dom.minidom
import uuid
@Gtk.Template(resource_path='/cz/vesp/roster/main-window.ui')
class RosterWindow(Adw.ApplicationWindow):
__gtype_name__ = 'RosterWindow'
# Top bar widgets
method_dropdown = Gtk.Template.Child()
url_entry = Gtk.Template.Child()
send_button = Gtk.Template.Child()
new_request_button = Gtk.Template.Child()
tab_bar_container = Gtk.Template.Child()
# Panes
main_pane = Gtk.Template.Child()
split_pane = Gtk.Template.Child()
# Sidebar widgets
projects_listbox = Gtk.Template.Child()
add_project_button = Gtk.Template.Child()
save_request_button = Gtk.Template.Child()
# Containers for tabs
request_tabs_container = Gtk.Template.Child()
response_tabs_container = Gtk.Template.Child()
# Status bar
status_label = Gtk.Template.Child()
time_label = Gtk.Template.Child()
# History
history_listbox = Gtk.Template.Child()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.http_client = HttpClient()
self.history_manager = HistoryManager()
self.project_manager = ProjectManager()
self.tab_manager = TabManager()
# Tab tracking - maps tab_id to UI state
self.tab_notebook = Gtk.Notebook()
self.tab_notebook.set_show_tabs(False) # We'll use custom tab bar
self.tab_notebook.set_show_border(False)
self.current_tab_id = None
# Create window actions
self._create_actions()
# Setup UI
self._setup_method_dropdown()
self._setup_request_tabs()
self._setup_response_tabs()
self._setup_tab_system()
self._load_history()
self._load_projects()
# Add initial header row
self._add_header_row()
# Set split pane position to center after window is shown
self.connect("map", self._on_window_mapped)
# Connect to close-request to warn about unsaved changes
self.connect("close-request", self._on_close_request)
# Create first tab
self._create_new_tab()
def _on_window_mapped(self, widget):
"""Set split pane position to center when window is mapped."""
# Use idle_add to ensure the widget is fully allocated
GLib.idle_add(self._center_split_pane)
def _center_split_pane(self):
"""Center the split pane divider."""
# Get the allocated width of the paned widget
allocation = self.split_pane.get_allocation()
if allocation.width > 0:
self.split_pane.set_position(allocation.width // 2)
return False # Don't repeat
def _on_close_request(self, window):
"""Handle window close request - warn if there are unsaved changes."""
# Save current tab state first
if self.current_tab_id:
current_tab = self.tab_manager.get_tab_by_id(self.current_tab_id)
if current_tab:
current_tab.request = self._build_request_from_ui()
# Check if any tabs have unsaved changes
modified_tabs = [tab for tab in self.tab_manager.tabs if tab.is_modified()]
if modified_tabs:
# Show warning dialog
dialog = Adw.AlertDialog()
dialog.set_heading("Unsaved Changes")
if len(modified_tabs) == 1:
dialog.set_body(f"'{modified_tabs[0].name}' has unsaved changes. Close anyway?")
else:
dialog.set_body(f"{len(modified_tabs)} requests have unsaved changes. Close anyway?")
dialog.add_response("cancel", "Cancel")
dialog.add_response("close", "Close")
dialog.set_response_appearance("close", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.set_default_response("cancel")
dialog.set_close_response("cancel")
dialog.connect("response", self._on_close_app_dialog_response)
dialog.present(self)
# Prevent closing for now - will close if user confirms
return True
# No unsaved changes, allow closing
return False
def _on_close_app_dialog_response(self, dialog, response):
"""Handle close application dialog response."""
if response == "close":
# User confirmed - close the window
self.destroy()
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 = '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 _setup_tab_system(self):
"""Set up the tab system."""
# Connect new request button
self.new_request_button.connect("clicked", self._on_new_request_clicked)
# Connect change tracking to detect modifications
self.url_entry.connect("changed", self._on_request_changed)
self.method_dropdown.connect("notify::selected", self._on_request_changed)
self.body_language_dropdown.connect("notify::selected", self._on_request_changed)
# Track body changes
body_buffer = self.body_sourceview.get_buffer()
body_buffer.connect("changed", self._on_request_changed)
def _on_request_changed(self, widget, *args):
"""Track changes to mark tab as modified."""
if self.current_tab_id:
current_tab = self.tab_manager.get_tab_by_id(self.current_tab_id)
if current_tab:
# Check if modified
current_tab.modified = current_tab.is_modified()
# Update the current request in the tab
current_tab.request = self._build_request_from_ui()
# Update tab bar to show modified indicator
self._update_tab_bar_visibility()
def _is_empty_new_request_tab(self):
"""Check if current tab is an empty 'New Request' tab."""
if not self.current_tab_id:
return False
current_tab = self.tab_manager.get_tab_by_id(self.current_tab_id)
if not current_tab:
return False
# Check if it's a "New Request" (not from saved request)
if current_tab.saved_request_id:
return False
# Check if it's empty (no URL, no body, no headers)
request = self._build_request_from_ui()
is_empty = (
not request.url.strip() and
not request.body.strip() and
not request.headers
)
return is_empty
def _create_new_tab(self, name="New Request", request=None, saved_request_id=None):
"""Create a new tab (simplified version - just clears current request for now)."""
# Create tab in tab manager
if not request:
request = HttpRequest(method="GET", url="", headers={}, body="", syntax="RAW")
tab = self.tab_manager.create_tab(name, request, saved_request_id)
self.current_tab_id = tab.id
# For simplified version, just load this request into UI
self._load_request_to_ui(request)
# Update tab bar visibility
self._update_tab_bar_visibility()
return tab.id
def _update_tab_bar_visibility(self):
"""Show/hide tab bar based on number of tabs."""
num_tabs = len(self.tab_manager.tabs)
# Show tab bar only if we have 2 or more tabs
self.tab_bar_container.set_visible(num_tabs >= 2)
# Update tab bar with buttons
if num_tabs >= 2:
# Clear existing children
child = self.tab_bar_container.get_first_child()
while child:
next_child = child.get_next_sibling()
self.tab_bar_container.remove(child)
child = next_child
# Add tab buttons with close button
for tab in self.tab_manager.tabs:
# Create a box for tab label + close button
tab_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
# Tab label with modified indicator
tab_label = tab.name[:20] # Truncate long names
if tab.is_modified():
tab_label += " •" # Add dot for unsaved changes
# Tab label button
tab_label_btn = Gtk.Button(label=tab_label)
tab_label_btn.add_css_class("flat")
if tab.id == self.current_tab_id:
tab_label_btn.add_css_class("suggested-action")
tab_label_btn.connect("clicked", lambda btn, tid=tab.id: self._switch_to_tab(tid))
tab_box.append(tab_label_btn)
# Close button
close_btn = Gtk.Button()
close_btn.set_icon_name("window-close-symbolic")
close_btn.add_css_class("flat")
close_btn.add_css_class("circular")
close_btn.set_tooltip_text("Close tab")
close_btn.connect("clicked", lambda btn, tid=tab.id: self._close_tab(tid))
tab_box.append(close_btn)
self.tab_bar_container.append(tab_box)
def _close_tab(self, tab_id):
"""Close a tab with unsaved change warning."""
# Update current tab state
if self.current_tab_id == tab_id:
current_tab = self.tab_manager.get_tab_by_id(self.current_tab_id)
if current_tab:
current_tab.request = self._build_request_from_ui()
# Check if tab has unsaved changes
tab = self.tab_manager.get_tab_by_id(tab_id)
if tab and tab.is_modified():
# Show warning dialog
dialog = Adw.AlertDialog()
dialog.set_heading("Close Unsaved Request?")
dialog.set_body(f"'{tab.name}' has unsaved changes. Close anyway?")
dialog.add_response("cancel", "Cancel")
dialog.add_response("close", "Close")
dialog.set_response_appearance("close", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.set_default_response("cancel")
dialog.set_close_response("cancel")
dialog.connect("response", lambda d, r: self._on_close_tab_dialog_response(r, tab_id))
dialog.present(self)
else:
# No unsaved changes, close directly
self._do_close_tab(tab_id)
def _on_close_tab_dialog_response(self, response, tab_id):
"""Handle close tab dialog response."""
if response == "close":
self._do_close_tab(tab_id)
def _do_close_tab(self, tab_id):
"""Actually close the tab (after confirmation if needed)."""
# Close the tab
success = self.tab_manager.close_tab(tab_id)
if success:
# If we closed the last tab, create a new one
if len(self.tab_manager.tabs) == 0:
self._create_new_tab()
else:
# Switch to the active tab (tab_manager already updated active_tab_id)
active_tab = self.tab_manager.get_active_tab()
if active_tab:
self.current_tab_id = active_tab.id
self._load_request_to_ui(active_tab.request)
if active_tab.response:
self._display_response(active_tab.response)
else:
# Clear response display
self.status_label.set_text("Ready")
self.time_label.set_text("")
buffer = self.response_headers_textview.get_buffer()
buffer.set_text("")
buffer = self.response_body_sourceview.get_buffer()
buffer.set_text("")
# Update tab bar
self._update_tab_bar_visibility()
def _switch_to_tab(self, tab_id):
"""Switch to a different tab."""
# Save current tab state
if self.current_tab_id:
current_tab = self.tab_manager.get_tab_by_id(self.current_tab_id)
if current_tab:
current_tab.request = self._build_request_from_ui()
# Load new tab
tab = self.tab_manager.get_tab_by_id(tab_id)
if tab:
self.current_tab_id = tab_id
self._load_request_to_ui(tab.request)
if tab.response:
self._display_response(tab.response)
self._update_tab_bar_visibility()
def _load_request_to_ui(self, request):
"""Load a request into the UI."""
# Set method
methods = ["GET", "POST", "PUT", "DELETE"]
if request.method in methods:
self.method_dropdown.set_selected(methods.index(request.method))
# Set URL
self.url_entry.set_text(request.url)
# Clear and set headers
while child := self.headers_listbox.get_first_child():
self.headers_listbox.remove(child)
for key, value in request.headers.items():
self._add_header_row(key, value)
if not request.headers:
self._add_header_row()
# Set body
buffer = self.body_sourceview.get_buffer()
buffer.set_text(request.body)
# Set syntax
syntax_options = ["RAW", "JSON", "XML"]
if request.syntax in syntax_options:
self.body_language_dropdown.set_selected(syntax_options.index(request.syntax))
def _on_new_request_clicked(self, button):
"""Handle New Request button click."""
self._create_new_tab()
def _create_actions(self):
"""Create window-level actions."""
# New tab shortcut (Ctrl+T)
action = Gio.SimpleAction.new("new-tab", None)
action.connect("activate", lambda a, p: self._on_new_request_clicked(None))
self.add_action(action)
self.get_application().set_accels_for_action("win.new-tab", ["t"])
# Close tab shortcut (Ctrl+W)
action = Gio.SimpleAction.new("close-tab", None)
action.connect("activate", lambda a, p: self._close_current_tab())
self.add_action(action)
self.get_application().set_accels_for_action("win.close-tab", ["w"])
def _close_current_tab(self):
"""Close the currently active tab."""
if self.current_tab_id:
self._close_tab(self.current_tab_id)
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)
# Create stack switcher for the tabs
request_switcher = Gtk.StackSwitcher()
request_switcher.set_stack(self.request_stack)
request_switcher.set_halign(Gtk.Align.CENTER)
# 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()
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)
# Create stack switcher for the tabs
response_switcher = Gtk.StackSwitcher()
response_switcher.set_stack(self.response_stack)
response_switcher.set_halign(Gtk.Align.CENTER)
# 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."""
# Validate URL
url = self.url_entry.get_text().strip()
if not url:
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("")
# Execute in thread to avoid blocking UI
thread = threading.Thread(target=self._execute_request_thread, args=(request,))
thread.daemon = True
thread.start()
def _execute_request_thread(self, request):
"""Execute request in background thread."""
response, error = self.http_client.execute_request(request)
# Update UI in main thread
GLib.idle_add(self._handle_response, request, response, 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 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
)
# 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)
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)
def _load_history(self):
"""Load history from file and populate list."""
# Clear existing history items
while True:
child = self.history_listbox.get_first_child()
if child is None:
break
self.history_listbox.remove(child)
# Load and display history
entries = self.history_manager.load_history()
for entry in entries:
item = HistoryItem(entry)
item.connect('load-requested', self._on_history_load_requested, entry)
self.history_listbox.append(item)
def _on_history_load_requested(self, widget, entry):
"""Handle load request from history item."""
# Smart loading: replaces empty tabs or creates new tab if modified
self._load_request_from_entry(entry)
def _load_request_from_entry(self, entry):
"""Load request from history entry - smart loading based on current tab state."""
request = entry.request
# Generate name from method and URL
url_parts = request.url.split('/')
url_name = url_parts[-1] if url_parts else request.url
name = f"{request.method} {url_name[:30]}" if url_name else f"{request.method} Request"
# Check if current tab is an empty "New Request"
if self._is_empty_new_request_tab():
# Replace the empty tab with this request
if self.current_tab_id:
current_tab = self.tab_manager.get_tab_by_id(self.current_tab_id)
if current_tab:
# Update the tab
current_tab.name = name
current_tab.request = request
current_tab.saved_request_id = None # Not from saved requests
current_tab.original_request = HttpRequest(
method=request.method,
url=request.url,
headers=request.headers.copy(),
body=request.body,
syntax=getattr(request, 'syntax', 'RAW')
)
current_tab.modified = False
# Load into UI
self._load_request_to_ui(request)
self._update_tab_bar_visibility()
else:
# Current tab has changes or is not a "New Request"
# Create a new tab
self._create_new_tab(
name=name,
request=request,
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):
"""Show a toast notification."""
toast = Adw.Toast()
toast.set_title(message)
toast.set_timeout(3)
# Get the toast overlay (we need to add one)
# For now, just print to console
print(f"Toast: {message}")
# Project Management Methods
def _load_projects(self):
"""Load and display projects."""
# Clear existing
while child := self.projects_listbox.get_first_child():
self.projects_listbox.remove(child)
# Load and populate
projects = self.project_manager.load_projects()
for project in projects:
item = ProjectItem(project)
item.connect('edit-requested', self._on_project_edit, project)
item.connect('delete-requested', self._on_project_delete, project)
item.connect('add-request-requested', self._on_add_to_project, project)
item.connect('request-load-requested', self._on_load_request)
item.connect('request-delete-requested', self._on_delete_request, project)
self.projects_listbox.append(item)
@Gtk.Template.Callback()
def on_add_project_clicked(self, button):
"""Show dialog to create project."""
dialog = Adw.AlertDialog()
dialog.set_heading("New Project")
dialog.set_body("Enter a name for the new project:")
entry = Gtk.Entry()
entry.set_placeholder_text("Project name")
dialog.set_extra_child(entry)
dialog.add_response("cancel", "Cancel")
dialog.add_response("create", "Create")
dialog.set_response_appearance("create", Adw.ResponseAppearance.SUGGESTED)
dialog.set_default_response("create")
dialog.set_close_response("cancel")
def on_response(dlg, response):
if response == "create":
name = entry.get_text().strip()
if name:
self.project_manager.add_project(name)
self._load_projects()
dialog.connect("response", on_response)
dialog.present(self)
def _on_project_edit(self, widget, project):
"""Show edit project dialog with icon picker."""
dialog = Adw.AlertDialog()
dialog.set_heading("Edit Project")
dialog.set_body(f"Edit '{project.name}':")
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
# Project name entry
name_label = Gtk.Label(label="Name:", xalign=0)
entry = Gtk.Entry()
entry.set_text(project.name)
box.append(name_label)
box.append(entry)
# Icon selector button
icon_label = Gtk.Label(label="Icon:", xalign=0)
icon_button = Gtk.Button()
icon_button.set_child(Gtk.Image.new_from_icon_name(project.icon))
icon_button.selected_icon = project.icon # Store current selection
def on_icon_button_clicked(btn):
icon_dialog = IconPickerDialog(current_icon=btn.selected_icon)
def on_icon_selected(dlg, icon_name):
btn.selected_icon = icon_name
btn.set_child(Gtk.Image.new_from_icon_name(icon_name))
icon_dialog.connect('icon-selected', on_icon_selected)
icon_dialog.present(self)
icon_button.connect('clicked', on_icon_button_clicked)
box.append(icon_label)
box.append(icon_button)
dialog.set_extra_child(box)
dialog.add_response("cancel", "Cancel")
dialog.add_response("save", "Save")
dialog.set_response_appearance("save", Adw.ResponseAppearance.SUGGESTED)
def on_response(dlg, response):
if response == "save":
new_name = entry.get_text().strip()
new_icon = icon_button.selected_icon
if new_name:
self.project_manager.update_project(project.id, new_name, new_icon)
self._load_projects()
dialog.connect("response", on_response)
dialog.present(self)
def _on_project_delete(self, widget, project):
"""Show delete confirmation."""
dialog = Adw.AlertDialog()
dialog.set_heading("Delete Project?")
dialog.set_body(f"Delete '{project.name}' and all its saved requests? This cannot be undone.")
dialog.add_response("cancel", "Cancel")
dialog.add_response("delete", "Delete")
dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.set_close_response("cancel")
def on_response(dlg, response):
if response == "delete":
self.project_manager.delete_project(project.id)
self._load_projects()
dialog.connect("response", on_response)
dialog.present(self)
@Gtk.Template.Callback()
def on_save_request_clicked(self, button):
"""Save current request to a project."""
request = self._build_request_from_ui()
if not request.url.strip():
self._show_toast("Cannot save: URL is empty")
return
projects = self.project_manager.load_projects()
if not projects:
# Show error - no projects
dialog = Adw.AlertDialog()
dialog.set_heading("No Projects")
dialog.set_body("Create a project first before saving requests.")
dialog.add_response("ok", "OK")
dialog.present(self)
return
# Create save dialog
dialog = Adw.AlertDialog()
dialog.set_heading("Save Request")
dialog.set_body("Choose a project and enter a name:")
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
# Project dropdown
project_list = Gtk.StringList()
for p in projects:
project_list.append(p.name)
dropdown = Gtk.DropDown(model=project_list)
box.append(dropdown)
# Name entry
entry = Gtk.Entry()
entry.set_placeholder_text("Request name")
box.append(entry)
dialog.set_extra_child(box)
dialog.add_response("cancel", "Cancel")
dialog.add_response("save", "Save")
dialog.set_response_appearance("save", Adw.ResponseAppearance.SUGGESTED)
def on_response(dlg, response):
if response == "save":
name = entry.get_text().strip()
if name:
selected = dropdown.get_selected()
project = projects[selected]
self.project_manager.add_request(project.id, name, request)
self._load_projects()
dialog.connect("response", on_response)
dialog.present(self)
def _on_add_to_project(self, widget, project):
"""Save current request to specific project."""
request = self._build_request_from_ui()
if not request.url.strip():
self._show_toast("Cannot save: URL is empty")
return
dialog = Adw.AlertDialog()
dialog.set_heading(f"Save to {project.name}")
dialog.set_body("Enter a name for this request:")
entry = Gtk.Entry()
entry.set_placeholder_text("Request name")
dialog.set_extra_child(entry)
dialog.add_response("cancel", "Cancel")
dialog.add_response("save", "Save")
dialog.set_response_appearance("save", Adw.ResponseAppearance.SUGGESTED)
def on_response(dlg, response):
if response == "save":
name = entry.get_text().strip()
if name:
self.project_manager.add_request(project.id, name, request)
self._load_projects()
dialog.connect("response", on_response)
dialog.present(self)
def _on_load_request(self, widget, saved_request):
"""Load saved request - smart loading based on current tab state."""
req = saved_request.request
# Check if current tab is an empty "New Request"
if self._is_empty_new_request_tab():
# Replace the empty tab with this request
if self.current_tab_id:
current_tab = self.tab_manager.get_tab_by_id(self.current_tab_id)
if current_tab:
# Update the tab
current_tab.name = saved_request.name
current_tab.request = req
current_tab.saved_request_id = saved_request.id
current_tab.original_request = HttpRequest(
method=req.method,
url=req.url,
headers=req.headers.copy(),
body=req.body,
syntax=req.syntax
)
current_tab.modified = False
# Load into UI
self._load_request_to_ui(req)
self._update_tab_bar_visibility()
else:
# Current tab has changes or is not a "New Request"
# Create a new tab
self._create_new_tab(
name=saved_request.name,
request=req,
saved_request_id=saved_request.id
)
# 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."""
dialog = Adw.AlertDialog()
dialog.set_heading("Delete Request?")
dialog.set_body(f"Delete '{saved_request.name}'? This cannot be undone.")
dialog.add_response("cancel", "Cancel")
dialog.add_response("delete", "Delete")
dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE)
def on_response(dlg, response):
if response == "delete":
self.project_manager.delete_request(project.id, saved_request.id)
self._load_projects()
dialog.connect("response", on_response)
dialog.present(self)