Add Scripts feature with JavaScript postprocessing
Implements JavaScript-based postprocessing for HTTP responses using gjs. Adds Scripts tab with preprocessing (disabled) and postprocessing (functional) sections.
This commit is contained in:
parent
1fab66db48
commit
96bbbcbc97
@ -41,6 +41,7 @@ roster_sources = [
|
|||||||
'environments_dialog.py',
|
'environments_dialog.py',
|
||||||
'preferences_dialog.py',
|
'preferences_dialog.py',
|
||||||
'request_tab_widget.py',
|
'request_tab_widget.py',
|
||||||
|
'script_executor.py',
|
||||||
]
|
]
|
||||||
|
|
||||||
install_data(roster_sources, install_dir: moduledir)
|
install_data(roster_sources, install_dir: moduledir)
|
||||||
|
|||||||
@ -205,6 +205,7 @@ class RequestTab:
|
|||||||
original_request: Optional[HttpRequest] = None # For change detection
|
original_request: Optional[HttpRequest] = None # For change detection
|
||||||
project_id: Optional[str] = None # Project association for environment variables
|
project_id: Optional[str] = None # Project association for environment variables
|
||||||
selected_environment_id: Optional[str] = None # Selected environment for variable substitution
|
selected_environment_id: Optional[str] = None # Selected environment for variable substitution
|
||||||
|
scripts: Optional['Scripts'] = None # Scripts for preprocessing and postprocessing
|
||||||
|
|
||||||
def is_modified(self) -> bool:
|
def is_modified(self) -> bool:
|
||||||
"""Check if current request differs from original."""
|
"""Check if current request differs from original."""
|
||||||
@ -231,7 +232,8 @@ class RequestTab:
|
|||||||
'modified': self.modified,
|
'modified': self.modified,
|
||||||
'original_request': self.original_request.to_dict() if self.original_request else None,
|
'original_request': self.original_request.to_dict() if self.original_request else None,
|
||||||
'project_id': self.project_id,
|
'project_id': self.project_id,
|
||||||
'selected_environment_id': self.selected_environment_id
|
'selected_environment_id': self.selected_environment_id,
|
||||||
|
'scripts': self.scripts.to_dict() if self.scripts else None
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -246,5 +248,28 @@ class RequestTab:
|
|||||||
modified=data.get('modified', False),
|
modified=data.get('modified', False),
|
||||||
original_request=HttpRequest.from_dict(data['original_request']) if data.get('original_request') else None,
|
original_request=HttpRequest.from_dict(data['original_request']) if data.get('original_request') else None,
|
||||||
project_id=data.get('project_id'),
|
project_id=data.get('project_id'),
|
||||||
selected_environment_id=data.get('selected_environment_id')
|
selected_environment_id=data.get('selected_environment_id'),
|
||||||
|
scripts=Scripts.from_dict(data['scripts']) if data.get('scripts') else None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Scripts:
|
||||||
|
"""Scripts for request preprocessing and postprocessing."""
|
||||||
|
preprocessing: str = ""
|
||||||
|
postprocessing: str = ""
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""Convert to dictionary for JSON serialization."""
|
||||||
|
return {
|
||||||
|
'preprocessing': self.preprocessing,
|
||||||
|
'postprocessing': self.postprocessing
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data):
|
||||||
|
"""Create instance from dictionary."""
|
||||||
|
return cls(
|
||||||
|
preprocessing=data.get('preprocessing', ''),
|
||||||
|
postprocessing=data.get('postprocessing', '')
|
||||||
)
|
)
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import xml.dom.minidom
|
|||||||
class RequestTabWidget(Gtk.Box):
|
class RequestTabWidget(Gtk.Box):
|
||||||
"""Widget representing a single request tab's UI."""
|
"""Widget representing a single request tab's UI."""
|
||||||
|
|
||||||
def __init__(self, tab_id, request=None, response=None, project_id=None, selected_environment_id=None, **kwargs):
|
def __init__(self, tab_id, request=None, response=None, project_id=None, selected_environment_id=None, scripts=None, **kwargs):
|
||||||
super().__init__(orientation=Gtk.Orientation.VERTICAL, **kwargs)
|
super().__init__(orientation=Gtk.Orientation.VERTICAL, **kwargs)
|
||||||
|
|
||||||
self.tab_id = tab_id
|
self.tab_id = tab_id
|
||||||
@ -39,6 +39,7 @@ class RequestTabWidget(Gtk.Box):
|
|||||||
self.original_request = None
|
self.original_request = None
|
||||||
self.project_id = project_id
|
self.project_id = project_id
|
||||||
self.selected_environment_id = selected_environment_id
|
self.selected_environment_id = selected_environment_id
|
||||||
|
self.scripts = scripts
|
||||||
self.project_manager = None # Will be injected from window
|
self.project_manager = None # Will be injected from window
|
||||||
self.environment_dropdown = None # Will be created if project_id is set
|
self.environment_dropdown = None # Will be created if project_id is set
|
||||||
self.env_separator = None # Visual separator after environment dropdown
|
self.env_separator = None # Visual separator after environment dropdown
|
||||||
@ -52,6 +53,10 @@ class RequestTabWidget(Gtk.Box):
|
|||||||
if request:
|
if request:
|
||||||
self._load_request(request)
|
self._load_request(request)
|
||||||
|
|
||||||
|
# Load initial scripts
|
||||||
|
if scripts:
|
||||||
|
self.load_scripts(scripts)
|
||||||
|
|
||||||
# Setup change tracking
|
# Setup change tracking
|
||||||
self._setup_change_tracking()
|
self._setup_change_tracking()
|
||||||
|
|
||||||
@ -136,6 +141,9 @@ class RequestTabWidget(Gtk.Box):
|
|||||||
# Body tab
|
# Body tab
|
||||||
self._build_body_tab()
|
self._build_body_tab()
|
||||||
|
|
||||||
|
# Scripts tab
|
||||||
|
self._build_scripts_tab()
|
||||||
|
|
||||||
split_pane.set_start_child(request_box)
|
split_pane.set_start_child(request_box)
|
||||||
|
|
||||||
# Response Panel
|
# Response Panel
|
||||||
@ -199,6 +207,91 @@ class RequestTabWidget(Gtk.Box):
|
|||||||
body_scroll.set_child(self.response_body_sourceview)
|
body_scroll.set_child(self.response_body_sourceview)
|
||||||
self.response_stack.add_titled(body_scroll, "body", "Body")
|
self.response_stack.add_titled(body_scroll, "body", "Body")
|
||||||
|
|
||||||
|
# Script Results Panel (collapsible)
|
||||||
|
self.script_results_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||||
|
self.script_results_container.set_visible(False) # Initially hidden
|
||||||
|
|
||||||
|
# Separator
|
||||||
|
results_separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
|
||||||
|
results_separator.set_margin_top(6)
|
||||||
|
self.script_results_container.append(results_separator)
|
||||||
|
|
||||||
|
# Header
|
||||||
|
results_header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||||
|
results_header.set_margin_start(12)
|
||||||
|
results_header.set_margin_end(12)
|
||||||
|
results_header.set_margin_top(6)
|
||||||
|
results_header.set_margin_bottom(6)
|
||||||
|
|
||||||
|
results_label = Gtk.Label(label="Script Output")
|
||||||
|
results_label.add_css_class("heading")
|
||||||
|
results_label.set_halign(Gtk.Align.START)
|
||||||
|
results_header.append(results_label)
|
||||||
|
|
||||||
|
# Spacer
|
||||||
|
results_spacer = Gtk.Box()
|
||||||
|
results_spacer.set_hexpand(True)
|
||||||
|
results_header.append(results_spacer)
|
||||||
|
|
||||||
|
# Status icon
|
||||||
|
self.script_status_icon = Gtk.Image()
|
||||||
|
self.script_status_icon.set_icon_name("emblem-ok-symbolic")
|
||||||
|
results_header.append(self.script_status_icon)
|
||||||
|
|
||||||
|
self.script_results_container.append(results_header)
|
||||||
|
|
||||||
|
# Output text view (scrollable)
|
||||||
|
self.script_output_scroll = Gtk.ScrolledWindow()
|
||||||
|
self.script_output_scroll.set_min_content_height(60)
|
||||||
|
self.script_output_scroll.set_max_content_height(60)
|
||||||
|
self.script_output_scroll.set_margin_start(12)
|
||||||
|
self.script_output_scroll.set_margin_end(12)
|
||||||
|
self.script_output_scroll.set_margin_bottom(6)
|
||||||
|
|
||||||
|
self.script_output_textview = Gtk.TextView()
|
||||||
|
self.script_output_textview.set_editable(False)
|
||||||
|
self.script_output_textview.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
|
||||||
|
self.script_output_textview.set_monospace(True)
|
||||||
|
self.script_output_textview.set_left_margin(6)
|
||||||
|
self.script_output_textview.set_right_margin(6)
|
||||||
|
self.script_output_textview.set_top_margin(6)
|
||||||
|
self.script_output_textview.set_bottom_margin(6)
|
||||||
|
|
||||||
|
self.script_output_scroll.set_child(self.script_output_textview)
|
||||||
|
self.script_results_container.append(self.script_output_scroll)
|
||||||
|
|
||||||
|
# Expander control (separator-based, shown when output > 3 lines)
|
||||||
|
self.script_expander = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||||
|
self.script_expander.set_margin_start(12)
|
||||||
|
self.script_expander.set_margin_end(12)
|
||||||
|
self.script_expander.set_margin_bottom(6)
|
||||||
|
self.script_expander.set_visible(False) # Hidden by default
|
||||||
|
|
||||||
|
expander_sep1 = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
|
||||||
|
expander_sep1.set_hexpand(True)
|
||||||
|
self.script_expander.append(expander_sep1)
|
||||||
|
|
||||||
|
self.script_expander_label = Gtk.Label(label="Show full output")
|
||||||
|
self.script_expander_label.add_css_class("dim-label")
|
||||||
|
self.script_expander_label.add_css_class("caption")
|
||||||
|
self.script_expander.append(self.script_expander_label)
|
||||||
|
|
||||||
|
expander_sep2 = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
|
||||||
|
expander_sep2.set_hexpand(True)
|
||||||
|
self.script_expander.append(expander_sep2)
|
||||||
|
|
||||||
|
# Add click gesture for expander
|
||||||
|
expander_gesture = Gtk.GestureClick()
|
||||||
|
expander_gesture.connect("released", self._on_script_expander_clicked)
|
||||||
|
self.script_expander.add_controller(expander_gesture)
|
||||||
|
|
||||||
|
self.script_results_container.append(self.script_expander)
|
||||||
|
|
||||||
|
# Track expansion state
|
||||||
|
self.script_output_expanded = False
|
||||||
|
|
||||||
|
response_box.append(self.script_results_container)
|
||||||
|
|
||||||
# Status Bar
|
# Status Bar
|
||||||
status_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
status_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
||||||
status_box.set_margin_start(12)
|
status_box.set_margin_start(12)
|
||||||
@ -322,6 +415,167 @@ class RequestTabWidget(Gtk.Box):
|
|||||||
|
|
||||||
self.request_stack.add_titled(body_box, "body", "Body")
|
self.request_stack.add_titled(body_box, "body", "Body")
|
||||||
|
|
||||||
|
def _build_scripts_tab(self):
|
||||||
|
"""Build the scripts tab with preprocessing and postprocessing sections."""
|
||||||
|
scripts_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||||
|
|
||||||
|
# Vertical paned to separate preprocessing and postprocessing
|
||||||
|
paned = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL)
|
||||||
|
paned.set_position(300)
|
||||||
|
paned.set_vexpand(True)
|
||||||
|
|
||||||
|
# Preprocessing Section (disabled for now)
|
||||||
|
preprocessing_section = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||||
|
preprocessing_section.set_margin_start(12)
|
||||||
|
preprocessing_section.set_margin_end(12)
|
||||||
|
preprocessing_section.set_margin_top(12)
|
||||||
|
preprocessing_section.set_margin_bottom(12)
|
||||||
|
|
||||||
|
preprocessing_label = Gtk.Label(label="Preprocessing (Coming Soon)")
|
||||||
|
preprocessing_label.set_halign(Gtk.Align.START)
|
||||||
|
preprocessing_label.add_css_class("heading")
|
||||||
|
preprocessing_label.add_css_class("dim-label")
|
||||||
|
preprocessing_section.append(preprocessing_label)
|
||||||
|
|
||||||
|
preprocessing_scroll = Gtk.ScrolledWindow()
|
||||||
|
preprocessing_scroll.set_vexpand(True)
|
||||||
|
|
||||||
|
self.preprocessing_sourceview = GtkSource.View()
|
||||||
|
self.preprocessing_sourceview.set_editable(False)
|
||||||
|
self.preprocessing_sourceview.set_sensitive(False)
|
||||||
|
self.preprocessing_sourceview.set_show_line_numbers(True)
|
||||||
|
self.preprocessing_sourceview.set_highlight_current_line(False)
|
||||||
|
self.preprocessing_sourceview.set_left_margin(12)
|
||||||
|
self.preprocessing_sourceview.set_right_margin(12)
|
||||||
|
self.preprocessing_sourceview.set_top_margin(12)
|
||||||
|
self.preprocessing_sourceview.set_bottom_margin(12)
|
||||||
|
|
||||||
|
# Set JavaScript syntax highlighting
|
||||||
|
lang_manager = GtkSource.LanguageManager.get_default()
|
||||||
|
js_lang = lang_manager.get_language('js')
|
||||||
|
if js_lang:
|
||||||
|
self.preprocessing_sourceview.get_buffer().set_language(js_lang)
|
||||||
|
|
||||||
|
# Set font
|
||||||
|
css_provider = Gtk.CssProvider()
|
||||||
|
css_provider.load_from_data(b"""
|
||||||
|
textview {
|
||||||
|
font-family: "Source Code Pro";
|
||||||
|
font-size: 12pt;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.preprocessing_sourceview.get_style_context().add_provider(
|
||||||
|
css_provider,
|
||||||
|
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
||||||
|
)
|
||||||
|
|
||||||
|
preprocessing_scroll.set_child(self.preprocessing_sourceview)
|
||||||
|
preprocessing_section.append(preprocessing_scroll)
|
||||||
|
|
||||||
|
info_label = Gtk.Label(label="Preprocessing will be available in a future release")
|
||||||
|
info_label.set_halign(Gtk.Align.START)
|
||||||
|
info_label.add_css_class("dim-label")
|
||||||
|
info_label.add_css_class("caption")
|
||||||
|
preprocessing_section.append(info_label)
|
||||||
|
|
||||||
|
paned.set_start_child(preprocessing_section)
|
||||||
|
|
||||||
|
# Postprocessing Section (functional)
|
||||||
|
postprocessing_section = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||||
|
postprocessing_section.set_margin_start(12)
|
||||||
|
postprocessing_section.set_margin_end(12)
|
||||||
|
postprocessing_section.set_margin_top(12)
|
||||||
|
postprocessing_section.set_margin_bottom(12)
|
||||||
|
|
||||||
|
postprocessing_label = Gtk.Label(label="Postprocessing")
|
||||||
|
postprocessing_label.set_halign(Gtk.Align.START)
|
||||||
|
postprocessing_label.add_css_class("heading")
|
||||||
|
postprocessing_section.append(postprocessing_label)
|
||||||
|
|
||||||
|
postprocessing_scroll = Gtk.ScrolledWindow()
|
||||||
|
postprocessing_scroll.set_vexpand(True)
|
||||||
|
|
||||||
|
self.postprocessing_sourceview = GtkSource.View()
|
||||||
|
self.postprocessing_sourceview.set_editable(True)
|
||||||
|
self.postprocessing_sourceview.set_show_line_numbers(True)
|
||||||
|
self.postprocessing_sourceview.set_highlight_current_line(True)
|
||||||
|
self.postprocessing_sourceview.set_left_margin(12)
|
||||||
|
self.postprocessing_sourceview.set_right_margin(12)
|
||||||
|
self.postprocessing_sourceview.set_top_margin(12)
|
||||||
|
self.postprocessing_sourceview.set_bottom_margin(12)
|
||||||
|
|
||||||
|
# Set JavaScript syntax highlighting
|
||||||
|
if js_lang:
|
||||||
|
self.postprocessing_sourceview.get_buffer().set_language(js_lang)
|
||||||
|
|
||||||
|
# Set font
|
||||||
|
self.postprocessing_sourceview.get_style_context().add_provider(
|
||||||
|
css_provider,
|
||||||
|
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set up theme
|
||||||
|
self._setup_scripts_theme()
|
||||||
|
|
||||||
|
# Set placeholder text
|
||||||
|
buffer = self.postprocessing_sourceview.get_buffer()
|
||||||
|
placeholder_text = """// Available: response object
|
||||||
|
// - response.body (string)
|
||||||
|
// - response.headers (object)
|
||||||
|
// - response.statusCode (number)
|
||||||
|
// - response.statusText (string)
|
||||||
|
// - response.responseTime (number, in ms)
|
||||||
|
//
|
||||||
|
// Use console.log() to output results
|
||||||
|
// Example:
|
||||||
|
// console.log('Status:', response.statusCode);
|
||||||
|
"""
|
||||||
|
buffer.set_text(placeholder_text)
|
||||||
|
|
||||||
|
# Connect change signal for tracking modifications
|
||||||
|
buffer.connect('changed', self._on_script_changed)
|
||||||
|
|
||||||
|
postprocessing_scroll.set_child(self.postprocessing_sourceview)
|
||||||
|
postprocessing_section.append(postprocessing_scroll)
|
||||||
|
|
||||||
|
# Toolbar
|
||||||
|
toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||||
|
|
||||||
|
# Spacer
|
||||||
|
spacer = Gtk.Box()
|
||||||
|
spacer.set_hexpand(True)
|
||||||
|
toolbar.append(spacer)
|
||||||
|
|
||||||
|
# Language label
|
||||||
|
lang_label = Gtk.Label(label="JavaScript")
|
||||||
|
lang_label.add_css_class("dim-label")
|
||||||
|
toolbar.append(lang_label)
|
||||||
|
|
||||||
|
postprocessing_section.append(toolbar)
|
||||||
|
|
||||||
|
paned.set_end_child(postprocessing_section)
|
||||||
|
|
||||||
|
scripts_box.append(paned)
|
||||||
|
|
||||||
|
self.request_stack.add_titled(scripts_box, "scripts", "Scripts")
|
||||||
|
|
||||||
|
def _setup_scripts_theme(self):
|
||||||
|
"""Set up GtkSourceView theme for scripts based on system color scheme."""
|
||||||
|
style_manager = Adw.StyleManager.get_default()
|
||||||
|
is_dark = style_manager.get_dark()
|
||||||
|
|
||||||
|
scheme_manager = GtkSource.StyleSchemeManager.get_default()
|
||||||
|
scheme = scheme_manager.get_scheme('Adwaita-dark' if is_dark else 'Adwaita')
|
||||||
|
|
||||||
|
if scheme:
|
||||||
|
self.postprocessing_sourceview.get_buffer().set_style_scheme(scheme)
|
||||||
|
|
||||||
|
def _on_script_changed(self, buffer):
|
||||||
|
"""Called when script content changes."""
|
||||||
|
# This will be used for change tracking
|
||||||
|
pass
|
||||||
|
|
||||||
def _setup_sourceview_theme(self):
|
def _setup_sourceview_theme(self):
|
||||||
"""Set up GtkSourceView theme based on system color scheme."""
|
"""Set up GtkSourceView theme based on system color scheme."""
|
||||||
style_manager = Adw.StyleManager.get_default()
|
style_manager = Adw.StyleManager.get_default()
|
||||||
@ -491,6 +745,41 @@ class RequestTabWidget(Gtk.Box):
|
|||||||
|
|
||||||
return HttpRequest(method=method, url=url, headers=headers, body=body, syntax=syntax)
|
return HttpRequest(method=method, url=url, headers=headers, body=body, syntax=syntax)
|
||||||
|
|
||||||
|
def get_scripts(self):
|
||||||
|
"""Get current scripts from UI."""
|
||||||
|
from .models import Scripts
|
||||||
|
|
||||||
|
# Get preprocessing script
|
||||||
|
preprocessing_buffer = self.preprocessing_sourceview.get_buffer()
|
||||||
|
preprocessing = preprocessing_buffer.get_text(
|
||||||
|
preprocessing_buffer.get_start_iter(),
|
||||||
|
preprocessing_buffer.get_end_iter(),
|
||||||
|
False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get postprocessing script
|
||||||
|
postprocessing_buffer = self.postprocessing_sourceview.get_buffer()
|
||||||
|
postprocessing = postprocessing_buffer.get_text(
|
||||||
|
postprocessing_buffer.get_start_iter(),
|
||||||
|
postprocessing_buffer.get_end_iter(),
|
||||||
|
False
|
||||||
|
)
|
||||||
|
|
||||||
|
return Scripts(preprocessing=preprocessing, postprocessing=postprocessing)
|
||||||
|
|
||||||
|
def load_scripts(self, scripts):
|
||||||
|
"""Load scripts into UI."""
|
||||||
|
if not scripts:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Load preprocessing script
|
||||||
|
preprocessing_buffer = self.preprocessing_sourceview.get_buffer()
|
||||||
|
preprocessing_buffer.set_text(scripts.preprocessing)
|
||||||
|
|
||||||
|
# Load postprocessing script
|
||||||
|
postprocessing_buffer = self.postprocessing_sourceview.get_buffer()
|
||||||
|
postprocessing_buffer.set_text(scripts.postprocessing)
|
||||||
|
|
||||||
def display_response(self, response):
|
def display_response(self, response):
|
||||||
"""Display response in this tab's UI."""
|
"""Display response in this tab's UI."""
|
||||||
self.response = response
|
self.response = response
|
||||||
@ -533,6 +822,67 @@ class RequestTabWidget(Gtk.Box):
|
|||||||
source_buffer.set_text(error)
|
source_buffer.set_text(error)
|
||||||
source_buffer.set_language(None)
|
source_buffer.set_language(None)
|
||||||
|
|
||||||
|
def display_script_results(self, script_result):
|
||||||
|
"""Display script execution results."""
|
||||||
|
from .script_executor import ScriptResult
|
||||||
|
|
||||||
|
if not isinstance(script_result, ScriptResult):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Show container
|
||||||
|
self.script_results_container.set_visible(True)
|
||||||
|
|
||||||
|
# Set status icon
|
||||||
|
if script_result.success:
|
||||||
|
self.script_status_icon.set_icon_name("emblem-ok-symbolic")
|
||||||
|
output_text = script_result.output
|
||||||
|
else:
|
||||||
|
self.script_status_icon.set_icon_name("dialog-error-symbolic")
|
||||||
|
output_text = script_result.error if script_result.error else "Unknown error"
|
||||||
|
|
||||||
|
# Set output text
|
||||||
|
buffer = self.script_output_textview.get_buffer()
|
||||||
|
buffer.set_text(output_text)
|
||||||
|
|
||||||
|
# Determine if expander is needed
|
||||||
|
num_lines = output_text.count('\n') + 1
|
||||||
|
needs_expander = num_lines > 3 or len(output_text) > 150
|
||||||
|
|
||||||
|
if needs_expander:
|
||||||
|
self.script_expander.set_visible(True)
|
||||||
|
self.script_expander.set_cursor_from_name("pointer")
|
||||||
|
else:
|
||||||
|
self.script_expander.set_visible(False)
|
||||||
|
|
||||||
|
# Reset expansion state
|
||||||
|
self.script_output_expanded = False
|
||||||
|
self.script_output_scroll.set_min_content_height(60)
|
||||||
|
self.script_output_scroll.set_max_content_height(60)
|
||||||
|
self.script_expander_label.set_text("Show full output")
|
||||||
|
|
||||||
|
def _clear_script_results(self):
|
||||||
|
"""Clear and hide script results panel."""
|
||||||
|
self.script_results_container.set_visible(False)
|
||||||
|
buffer = self.script_output_textview.get_buffer()
|
||||||
|
buffer.set_text("")
|
||||||
|
self.script_expander.set_visible(False)
|
||||||
|
self.script_output_expanded = False
|
||||||
|
|
||||||
|
def _on_script_expander_clicked(self, gesture, n_press, x, y):
|
||||||
|
"""Toggle script output expansion."""
|
||||||
|
self.script_output_expanded = not self.script_output_expanded
|
||||||
|
|
||||||
|
if self.script_output_expanded:
|
||||||
|
# Expand: set max first to avoid assertion errors
|
||||||
|
self.script_output_scroll.set_max_content_height(300)
|
||||||
|
self.script_output_scroll.set_min_content_height(300)
|
||||||
|
self.script_expander_label.set_text("Collapse output")
|
||||||
|
else:
|
||||||
|
# Collapse: set min first to avoid assertion errors
|
||||||
|
self.script_output_scroll.set_min_content_height(60)
|
||||||
|
self.script_output_scroll.set_max_content_height(60)
|
||||||
|
self.script_expander_label.set_text("Show full output")
|
||||||
|
|
||||||
def _extract_content_type(self, headers_text):
|
def _extract_content_type(self, headers_text):
|
||||||
"""Extract content-type from response headers."""
|
"""Extract content-type from response headers."""
|
||||||
if not headers_text:
|
if not headers_text:
|
||||||
|
|||||||
153
src/script_executor.py
Normal file
153
src/script_executor.py
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
# script_executor.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 <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Tuple, Optional
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ScriptResult:
|
||||||
|
"""Result of script execution."""
|
||||||
|
success: bool
|
||||||
|
output: str # console.log output or error message
|
||||||
|
error: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ScriptExecutor:
|
||||||
|
"""Executes JavaScript code using gjs (GNOME JavaScript)."""
|
||||||
|
|
||||||
|
TIMEOUT_SECONDS = 5
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def execute_postprocessing_script(script_code: str, response) -> ScriptResult:
|
||||||
|
"""
|
||||||
|
Execute postprocessing script with response object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
script_code: JavaScript code to execute
|
||||||
|
response: HttpResponse object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ScriptResult with output or error
|
||||||
|
"""
|
||||||
|
if not script_code.strip():
|
||||||
|
return ScriptResult(success=True, output="")
|
||||||
|
|
||||||
|
# Create response object for JavaScript
|
||||||
|
response_obj = ScriptExecutor._create_response_object(response)
|
||||||
|
|
||||||
|
# Build complete script with context
|
||||||
|
script_with_context = f"""
|
||||||
|
const response = {json.dumps(response_obj)};
|
||||||
|
|
||||||
|
// Capture console.log output
|
||||||
|
let consoleOutput = [];
|
||||||
|
const console = {{
|
||||||
|
log: function(...args) {{
|
||||||
|
consoleOutput.push(args.map(arg => String(arg)).join(' '));
|
||||||
|
}}
|
||||||
|
}};
|
||||||
|
|
||||||
|
// User script
|
||||||
|
try {{
|
||||||
|
{script_code}
|
||||||
|
}} catch (error) {{
|
||||||
|
consoleOutput.push('Error: ' + error.message);
|
||||||
|
throw error;
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Output results
|
||||||
|
print(consoleOutput.join('\\n'));
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Execute with gjs
|
||||||
|
output, error, returncode = ScriptExecutor._run_gjs_script(
|
||||||
|
script_with_context,
|
||||||
|
timeout=ScriptExecutor.TIMEOUT_SECONDS
|
||||||
|
)
|
||||||
|
|
||||||
|
if returncode == 0:
|
||||||
|
return ScriptResult(success=True, output=output.strip())
|
||||||
|
else:
|
||||||
|
error_msg = error.strip() if error else "Script execution failed"
|
||||||
|
return ScriptResult(success=False, output="", error=error_msg)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_response_object(response) -> dict:
|
||||||
|
"""Convert HttpResponse to JavaScript-compatible dict."""
|
||||||
|
# Parse headers text into dict
|
||||||
|
headers = {}
|
||||||
|
if response.headers:
|
||||||
|
for line in response.headers.split('\n')[1:]: # Skip status line
|
||||||
|
line = line.strip()
|
||||||
|
if ':' in line:
|
||||||
|
key, value = line.split(':', 1)
|
||||||
|
headers[key.strip()] = value.strip()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'body': response.body,
|
||||||
|
'headers': headers,
|
||||||
|
'statusCode': response.status_code,
|
||||||
|
'statusText': response.status_text,
|
||||||
|
'responseTime': response.response_time_ms
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _run_gjs_script(script_code: str, timeout: int = 5) -> Tuple[str, str, int]:
|
||||||
|
"""
|
||||||
|
Run JavaScript code using gjs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
script_code: Complete JavaScript code
|
||||||
|
timeout: Execution timeout in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (stdout, stderr, returncode)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Write script to temporary file
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False) as f:
|
||||||
|
f.write(script_code)
|
||||||
|
script_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Execute with gjs
|
||||||
|
result = subprocess.run(
|
||||||
|
['gjs', script_path],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
return result.stdout, result.stderr, result.returncode
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Clean up temp file
|
||||||
|
Path(script_path).unlink(missing_ok=True)
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return "", f"Script execution timeout ({timeout}s)", 1
|
||||||
|
except FileNotFoundError:
|
||||||
|
return "", "gjs not found. Please ensure GNOME JavaScript is installed.", 1
|
||||||
|
except Exception as e:
|
||||||
|
return "", f"Script execution error: {str(e)}", 1
|
||||||
@ -36,7 +36,8 @@ class TabManager:
|
|||||||
saved_request_id: Optional[str] = None,
|
saved_request_id: Optional[str] = None,
|
||||||
response: Optional[HttpResponse] = None,
|
response: Optional[HttpResponse] = None,
|
||||||
project_id: Optional[str] = None,
|
project_id: Optional[str] = None,
|
||||||
selected_environment_id: Optional[str] = None) -> RequestTab:
|
selected_environment_id: Optional[str] = None,
|
||||||
|
scripts=None) -> RequestTab:
|
||||||
"""Create a new tab and add it to the collection."""
|
"""Create a new tab and add it to the collection."""
|
||||||
tab_id = str(uuid.uuid4())
|
tab_id = str(uuid.uuid4())
|
||||||
|
|
||||||
@ -62,7 +63,8 @@ class TabManager:
|
|||||||
modified=False,
|
modified=False,
|
||||||
original_request=original_request,
|
original_request=original_request,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
selected_environment_id=selected_environment_id
|
selected_environment_id=selected_environment_id,
|
||||||
|
scripts=scripts
|
||||||
)
|
)
|
||||||
|
|
||||||
self.tabs.append(tab)
|
self.tabs.append(tab)
|
||||||
|
|||||||
@ -300,18 +300,18 @@ class RosterWindow(Adw.ApplicationWindow):
|
|||||||
return f"{base_name} (copy {counter})"
|
return f"{base_name} (copy {counter})"
|
||||||
|
|
||||||
def _create_new_tab(self, name="New Request", request=None, saved_request_id=None, response=None,
|
def _create_new_tab(self, name="New Request", request=None, saved_request_id=None, response=None,
|
||||||
project_id=None, selected_environment_id=None):
|
project_id=None, selected_environment_id=None, scripts=None):
|
||||||
"""Create a new tab with RequestTabWidget."""
|
"""Create a new tab with RequestTabWidget."""
|
||||||
# Create tab in tab manager
|
# Create tab in tab manager
|
||||||
if not request:
|
if not request:
|
||||||
request = HttpRequest(method="GET", url="", headers={}, body="", syntax="RAW")
|
request = HttpRequest(method="GET", url="", headers={}, body="", syntax="RAW")
|
||||||
|
|
||||||
tab = self.tab_manager.create_tab(name, request, saved_request_id, response,
|
tab = self.tab_manager.create_tab(name, request, saved_request_id, response,
|
||||||
project_id, selected_environment_id)
|
project_id, selected_environment_id, scripts)
|
||||||
self.current_tab_id = tab.id
|
self.current_tab_id = tab.id
|
||||||
|
|
||||||
# Create RequestTabWidget for this tab
|
# Create RequestTabWidget for this tab
|
||||||
widget = RequestTabWidget(tab.id, request, response, project_id, selected_environment_id)
|
widget = RequestTabWidget(tab.id, request, response, project_id, selected_environment_id, scripts)
|
||||||
widget.original_request = tab.original_request
|
widget.original_request = tab.original_request
|
||||||
widget.project_manager = self.project_manager # Inject project manager
|
widget.project_manager = self.project_manager # Inject project manager
|
||||||
|
|
||||||
@ -435,8 +435,21 @@ class RosterWindow(Adw.ApplicationWindow):
|
|||||||
# Update tab with response
|
# Update tab with response
|
||||||
if response:
|
if response:
|
||||||
widget.display_response(response)
|
widget.display_response(response)
|
||||||
|
|
||||||
|
# Execute postprocessing script if exists
|
||||||
|
scripts = widget.get_scripts()
|
||||||
|
if scripts and scripts.postprocessing.strip():
|
||||||
|
from .script_executor import ScriptExecutor
|
||||||
|
script_result = ScriptExecutor.execute_postprocessing_script(
|
||||||
|
scripts.postprocessing,
|
||||||
|
response
|
||||||
|
)
|
||||||
|
widget.display_script_results(script_result)
|
||||||
|
else:
|
||||||
|
widget._clear_script_results()
|
||||||
else:
|
else:
|
||||||
widget.display_error(error or "Unknown error")
|
widget.display_error(error or "Unknown error")
|
||||||
|
widget._clear_script_results()
|
||||||
|
|
||||||
# Save response to tab
|
# Save response to tab
|
||||||
page = self.tab_view.get_selected_page()
|
page = self.tab_view.get_selected_page()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user