network tab

This commit is contained in:
Face
2025-09-05 19:07:21 +03:00
parent 21b4f455c2
commit 4d16a16fe4
42 changed files with 1463 additions and 154 deletions

View File

@@ -39,5 +39,9 @@ var HTML_CONTENT = """
<head>
<title>New tab</title>
</head>
<body><p>test</p></body>
<body>
<p>Welcome to Flumi Browser!</p>
<img src="https://httpbin.org/image/png" alt="Test image" />
<p>This page includes a test image to verify network functionality.</p>
</body>
""".to_utf8_buffer()

View File

@@ -26,7 +26,6 @@ func initialize_filter() -> void:
current_filter = ""
func connect_signals() -> void:
Trace.get_instance().log_message.connect(_on_trace_message)
clear_button.pressed.connect(_on_clear_pressed)
input_line.gui_input.connect(_on_input_gui_input)
@@ -37,9 +36,6 @@ func load_existing_logs() -> void:
for msg in existing_messages:
add_log_entry(msg.message, msg.level, msg.timestamp)
func _on_trace_message(message: Variant, level: String, timestamp: float) -> void:
call_deferred("add_log_entry", message, level, timestamp)
func _on_lua_print(message: String) -> void:
add_log_entry(message, "lua", Time.get_ticks_msec() / 1000.0)
@@ -84,31 +80,14 @@ func add_log_entry(message: Variant, level: String, timestamp: float) -> void:
func create_log_item(entry: Dictionary) -> Control:
if entry.level == "input":
var message_code_edit = CodeEdit.new()
var input_display_text = get_display_text_for_entry(entry)
message_code_edit.text = input_display_text
message_code_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
message_code_edit.scroll_fit_content_height = true
message_code_edit.editable = true
message_code_edit.context_menu_enabled = true
message_code_edit.shortcut_keys_enabled = true
message_code_edit.selecting_enabled = true
message_code_edit.deselect_on_focus_loss_enabled = true
message_code_edit.drag_and_drop_selection_enabled = false
message_code_edit.virtual_keyboard_enabled = false
message_code_edit.middle_mouse_paste_enabled = false
var code_style_normal = StyleBoxFlat.new()
code_style_normal.bg_color = Color.TRANSPARENT
code_style_normal.border_width_left = 0
code_style_normal.border_width_top = 0
code_style_normal.border_width_right = 0
code_style_normal.border_width_bottom = 0
code_style_normal.content_margin_bottom = 8
message_code_edit.add_theme_stylebox_override("normal", code_style_normal)
message_code_edit.add_theme_stylebox_override("focus", code_style_normal)
message_code_edit.syntax_highlighter = input_line.syntax_highlighter.duplicate()
var message_code_edit = CodeEditUtils.create_code_edit({
"text": input_display_text,
"scroll_fit_content_height": true,
"transparent_background": true,
"syntax_highlighter": input_line.syntax_highlighter.duplicate(),
"block_editing_signals": true
})
message_code_edit.gui_input.connect(_on_log_code_edit_gui_input)
message_code_edit.focus_entered.connect(_on_log_code_edit_focus_entered.bind(message_code_edit))

View File

@@ -2,7 +2,7 @@ extends RefCounted
class_name GurtProtocol
const DNS_SERVER_IP: String = "135.125.163.131"
const DNS_SERVER_PORT: int = 8877
const DNS_SERVER_PORT: int = 4878
static func is_gurt_domain(url: String) -> bool:
if url.begins_with("gurt://"):

View File

@@ -1 +0,0 @@
uid://cer1rniskhi24

View File

@@ -3,7 +3,7 @@ class_name LuaSyntaxHighlighter
extends SyntaxHighlighter
@export_group("Colors")
@export var font_color: Color = Color("#d4d4d4", Color.WHITE)
@export var font_color: Color = Color.from_string("#d4d4d4", Color.WHITE)
@export var keyword_color: Color = Color.from_string("#c586c0", Color.WHITE)
@export var gurt_globals_color: Color = Color.from_string("#569cd6", Color.WHITE)
@export var function_color: Color = Color.from_string("#dcdcaa", Color.WHITE)
@@ -48,9 +48,8 @@ func _init() -> void:
for m in members:
_member_keywords[m] = true
func _is_whitespace(char: String) -> bool:
return char == " " or char == "\t"
func _is_whitespace(ch: String) -> bool:
return ch == " " or ch == "\t"
func _clear_highlighting_cache():
_state_cache.clear()
@@ -142,9 +141,9 @@ func _get_line_syntax_highlighting(p_line: int) -> Dictionary:
if current_char == "0" and i + 1 < line_len and line_text[i+1].to_lower() == "x":
i += 2; is_hex = true
while i < line_len:
var char = line_text[i]
if (is_hex and char.is_valid_hex_number(false)) or \
(not is_hex and (char.is_valid_int() or char in "Ee.-+")):
var ch = line_text[i]
if (is_hex and ch.is_valid_hex_number(false)) or \
(not is_hex and (ch.is_valid_int() or ch in "Ee.-+")):
i += 1
else:
break

View File

@@ -7,12 +7,19 @@ func fetch_image(url: String) -> ImageTexture:
if url.is_empty():
return null
var network_request = NetworkManager.start_request(url, "GET", false)
var request_headers = PackedStringArray()
request_headers.append("User-Agent: " + UserAgent.get_user_agent())
var headers_dict = {}
headers_dict["User-Agent"] = UserAgent.get_user_agent()
NetworkManager.set_request_headers(network_request.id, headers_dict)
var error = http_request.request(url, request_headers)
if error != OK:
print("Error making HTTP request: ", error)
NetworkManager.fail_request(network_request.id, "HTTP request error: " + str(error))
http_request.queue_free()
return null
@@ -25,10 +32,19 @@ func fetch_image(url: String) -> ImageTexture:
http_request.queue_free()
var response_headers = {}
for header in headers:
var parts = header.split(":", 1)
if parts.size() == 2:
response_headers[parts[0].strip_edges()] = parts[1].strip_edges()
if result != HTTPRequest.RESULT_SUCCESS or response_code != 200:
print("Failed to fetch image. Result: ", result, " Response code: ", response_code)
NetworkManager.complete_request(network_request.id, response_code, "Request failed", response_headers, body.get_string_from_utf8(), body)
return null
NetworkManager.complete_request(network_request.id, response_code, "OK", response_headers, body.get_string_from_utf8(), body)
# Get content type from headers
var content_type = ""
for header in headers:
@@ -74,12 +90,19 @@ func fetch_text(url: String) -> String:
http_request.queue_free()
return ""
var network_request = NetworkManager.start_request(url, "GET", false)
var request_headers = PackedStringArray()
request_headers.append("User-Agent: " + UserAgent.get_user_agent())
var headers_dict = {}
headers_dict["User-Agent"] = UserAgent.get_user_agent()
NetworkManager.set_request_headers(network_request.id, headers_dict)
var error = http_request.request(url, request_headers)
if error != OK:
print("Error making HTTP request for text resource: ", url, " Error: ", error)
NetworkManager.fail_request(network_request.id, "HTTP request error: " + str(error))
http_request.queue_free()
return ""
@@ -87,15 +110,27 @@ func fetch_text(url: String) -> String:
var result = response[0] # HTTPClient.Result
var response_code = response[1] # int
var headers = response[2] # PackedStringArray
var body = response[3] # PackedByteArray
http_request.queue_free()
var response_headers = {}
for header in headers:
var parts = header.split(":", 1)
if parts.size() == 2:
response_headers[parts[0].strip_edges()] = parts[1].strip_edges()
var response_body = body.get_string_from_utf8()
if result != HTTPRequest.RESULT_SUCCESS or response_code != 200:
print("Failed to fetch text resource. URL: ", url, " Result: ", result, " Response code: ", response_code)
NetworkManager.complete_request(network_request.id, response_code, "Request failed", response_headers, response_body)
return ""
return body.get_string_from_utf8()
NetworkManager.complete_request(network_request.id, response_code, "OK", response_headers, response_body)
return response_body
func fetch_external_resource(url: String, base_url: String = "") -> String:
var resolved_url = URLUtils.resolve_url(base_url, url)
@@ -118,13 +153,15 @@ func fetch_gurt_resource(url: String) -> String:
if gurt_url.contains("localhost"):
gurt_url = gurt_url.replace("localhost", "127.0.0.1")
var network_request = NetworkManager.start_request(gurt_url, "GET", false)
var client = GurtProtocolClient.new()
for ca_cert in CertificateManager.trusted_ca_certificates:
client.add_ca_certificate(ca_cert)
if not client.create_client(30):
print("GURT resource error: Failed to create client")
if not client.create_client_with_dns(30, GurtProtocol.DNS_SERVER_IP, GurtProtocol.DNS_SERVER_PORT):
NetworkManager.fail_request(network_request.id, "Failed to create GURT client")
return ""
var host_domain = gurt_url
@@ -142,9 +179,16 @@ func fetch_gurt_resource(url: String) -> String:
if not response or not response.is_success:
var error_msg = "Failed to load GURT resource"
var status_code = 0
if response:
status_code = response.status_code
error_msg += ": " + str(response.status_code) + " " + response.status_message
print("GURT resource error: ", error_msg)
NetworkManager.complete_request(network_request.id, status_code, error_msg, {}, "")
return ""
return response.body.get_string_from_utf8()
var response_headers = response.headers if response.headers else {}
var response_body = response.body.get_string_from_utf8()
NetworkManager.complete_request(network_request.id, response.status_code, "OK", response_headers, response_body)
return response_body

View File

@@ -0,0 +1,124 @@
extends Node
signal request_started(request: NetworkRequest)
signal request_completed(request: NetworkRequest)
signal request_failed(request: NetworkRequest)
var active_requests: Dictionary = {} # request_id -> NetworkRequest
var all_requests: Array[NetworkRequest] = []
var dev_tools_network_tab: NetworkTab = null
func register_dev_tools_network_tab(network_tab: NetworkTab):
dev_tools_network_tab = network_tab
func start_request(url: String, method: String = "GET", is_from_lua: bool = false) -> NetworkRequest:
var request = NetworkRequest.new(url, method)
request.is_from_lua = is_from_lua
active_requests[request.id] = request
all_requests.append(request)
# Notify dev tools
if dev_tools_network_tab:
dev_tools_network_tab.add_network_request(request)
request_started.emit(request)
return request
func complete_request(request_id: String, status_code: int, status_text: String, headers: Dictionary, body: String, body_bytes: PackedByteArray = []):
var request = active_requests.get(request_id)
if not request:
return
request.set_response(status_code, status_text, headers, body, body_bytes)
active_requests.erase(request_id)
# Update dev tools UI
if dev_tools_network_tab:
dev_tools_network_tab.update_request_item(request)
if request.status == NetworkRequest.RequestStatus.SUCCESS:
request_completed.emit(request)
else:
request_failed.emit(request)
func fail_request(request_id: String, error_message: String):
var request = active_requests.get(request_id)
if not request:
return
request.set_error(error_message)
active_requests.erase(request_id)
# Update dev tools UI
if dev_tools_network_tab:
dev_tools_network_tab.update_request_item(request)
request_failed.emit(request)
func set_request_headers(request_id: String, headers: Dictionary):
var request = active_requests.get(request_id)
if request:
request.request_headers = headers
func set_request_body(request_id: String, body: String):
var request = active_requests.get(request_id)
if request:
request.request_body = body
func get_all_requests() -> Array[NetworkRequest]:
return all_requests
func clear_all_requests():
active_requests.clear()
all_requests.clear()
if dev_tools_network_tab:
dev_tools_network_tab.clear_all_requests()
func clear_all_requests_except(preserve_request_id: String):
# Remove from active_requests but preserve specific request
var preserved_active = null
if active_requests.has(preserve_request_id):
preserved_active = active_requests[preserve_request_id]
active_requests.clear()
if preserved_active:
active_requests[preserve_request_id] = preserved_active
# Remove from all_requests but preserve specific request
var preserved_request = null
for request in all_requests:
if request.id == preserve_request_id:
preserved_request = request
break
all_requests.clear()
if preserved_request:
all_requests.append(preserved_request)
if dev_tools_network_tab:
dev_tools_network_tab.clear_all_requests_except(preserve_request_id)
func get_request_stats() -> Dictionary:
var total_requests = all_requests.size()
var total_size = 0
var successful_requests = 0
var failed_requests = 0
var pending_requests = active_requests.size()
for request in all_requests:
total_size += request.size
match request.status:
NetworkRequest.RequestStatus.SUCCESS:
successful_requests += 1
NetworkRequest.RequestStatus.ERROR:
failed_requests += 1
return {
"total": total_requests,
"successful": successful_requests,
"failed": failed_requests,
"pending": pending_requests,
"total_size": total_size
}

View File

@@ -0,0 +1 @@
uid://ggm1sq7h64sr

View File

@@ -0,0 +1,207 @@
class_name NetworkRequest
extends RefCounted
enum RequestType {
FETCH,
DOC,
CSS,
LUA,
FONT,
IMG,
SOCKET,
OTHER
}
enum RequestStatus {
PENDING,
SUCCESS,
ERROR,
CANCELLED
}
var id: String
var name: String
var url: String
var method: String
var type: RequestType
var status: RequestStatus
var status_code: int
var status_text: String
var size: int
var time_ms: float
var start_time: float
var end_time: float
var request_headers: Dictionary = {}
var response_headers: Dictionary = {}
var request_body: String = ""
var response_body: String = ""
var response_body_bytes: PackedByteArray = []
var mime_type: String = ""
var is_from_lua: bool = false
func _init(request_url: String = "", request_method: String = "GET"):
id = generate_id()
url = request_url
method = request_method.to_upper()
name = extract_name_from_url(url)
type = determine_type_from_url(url)
status = RequestStatus.PENDING
status_code = 0
status_text = ""
size = 0
time_ms = 0.0
start_time = Time.get_ticks_msec()
end_time = 0.0
func generate_id() -> String:
return str(Time.get_ticks_msec()) + "_" + str(randi())
func extract_name_from_url(request_url: String) -> String:
if request_url.is_empty():
return "Unknown"
var parts = request_url.split("/")
if parts.size() > 0:
var filename = parts[-1]
if filename.is_empty() and parts.size() > 1:
filename = parts[-2]
if "?" in filename:
filename = filename.split("?")[0]
if "#" in filename:
filename = filename.split("#")[0]
return filename if not filename.is_empty() else "/"
return request_url
func determine_type_from_url(request_url: String) -> RequestType:
var lower_url = request_url.to_lower()
if lower_url.ends_with(".html") or lower_url.ends_with(".htm"):
return RequestType.DOC
elif lower_url.ends_with(".css"):
return RequestType.CSS
elif lower_url.ends_with(".lua") or lower_url.ends_with(".luau"):
return RequestType.LUA
elif lower_url.ends_with(".woff") or lower_url.ends_with(".woff2") or lower_url.ends_with(".ttf") or lower_url.ends_with(".otf"):
return RequestType.FONT
elif lower_url.ends_with(".png") or lower_url.ends_with(".jpg") or lower_url.ends_with(".jpeg") or lower_url.ends_with(".gif") or lower_url.ends_with(".webp") or lower_url.ends_with(".svg") or lower_url.ends_with(".bmp"):
return RequestType.IMG
elif lower_url.begins_with("ws://") or lower_url.begins_with("wss://"):
return RequestType.SOCKET
if not mime_type.is_empty():
var lower_mime = mime_type.to_lower()
if lower_mime.begins_with("text/html"):
return RequestType.DOC
elif lower_mime.begins_with("text/css"):
return RequestType.CSS
elif lower_mime.begins_with("image/"):
return RequestType.IMG
elif lower_mime.begins_with("font/") or lower_mime == "application/font-woff" or lower_mime == "application/font-woff2":
return RequestType.FONT
if is_from_lua:
return RequestType.FETCH
return RequestType.OTHER
func set_response(response_status_code: int, response_status_text: String, response_headers_dict: Dictionary, response_body_content: String, body_bytes: PackedByteArray = []):
end_time = Time.get_ticks_msec()
time_ms = end_time - start_time
status_code = response_status_code
status_text = response_status_text
response_headers = response_headers_dict
response_body = response_body_content
response_body_bytes = body_bytes if not body_bytes.is_empty() else response_body_content.to_utf8_buffer()
size = response_body_bytes.size()
for header_name in response_headers:
if header_name.to_lower() == "content-type":
mime_type = response_headers[header_name].split(";")[0].strip_edges()
break
type = determine_type_from_url(url)
if response_status_code >= 200 and response_status_code < 300:
status = RequestStatus.SUCCESS
else:
status = RequestStatus.ERROR
func set_error(error_message: String):
end_time = Time.get_ticks_msec()
time_ms = end_time - start_time
status = RequestStatus.ERROR
status_text = error_message
func get_status_display() -> String:
match status:
RequestStatus.PENDING:
return "Pending"
RequestStatus.SUCCESS:
return str(status_code)
RequestStatus.ERROR:
return str(status_code) if status_code > 0 else "Failed"
RequestStatus.CANCELLED:
return "Cancelled"
_:
return "Unknown"
func get_type_display() -> String:
match type:
RequestType.FETCH:
return "Fetch"
RequestType.DOC:
return "Doc"
RequestType.CSS:
return "CSS"
RequestType.LUA:
return "Lua"
RequestType.FONT:
return "Font"
RequestType.IMG:
return "Img"
RequestType.SOCKET:
return "Socket"
RequestType.OTHER:
return "Other"
_:
return "Unknown"
static func format_bytes(given_size: int) -> String:
if given_size < 1024:
return str(given_size) + " B"
elif given_size < 1024 * 1024:
return str(given_size / 1024) + " KB"
else:
return str(given_size / (1024 * 1024)) + " MB"
func get_time_display() -> String:
if status == RequestStatus.PENDING:
return "Pending"
if time_ms < 1000:
return str(int(time_ms)) + " ms"
else:
return "%.1f s" % (time_ms / 1000.0)
func get_icon_texture() -> Texture2D:
match type:
RequestType.FETCH:
return load("res://Assets/Icons/download.svg")
RequestType.DOC:
return load("res://Assets/Icons/file-text.svg")
RequestType.CSS:
return load("res://Assets/Icons/palette.svg")
RequestType.LUA:
return load("res://Assets/Icons/braces.svg")
RequestType.FONT:
return load("res://Assets/Icons/braces.svg")
RequestType.IMG:
return load("res://Assets/Icons/image.svg")
RequestType.SOCKET:
return load("res://Assets/Icons/arrow-down-up.svg")
_:
return load("res://Assets/Icons/search.svg")

View File

@@ -0,0 +1 @@
uid://r6h4tvud6yne

View File

@@ -11,24 +11,16 @@ extends PanelContainer
var request: NetworkRequest
var network_tab: NetworkTab
@onready var normal_style: StyleBox = get_meta("normal_style")
@onready var selected_style: StyleBox = get_meta("selected_style")
@onready var success_color: Color = status_label.get_meta("success_color")
@onready var error_color: Color = status_label.get_meta("error_color")
@onready var pending_color: Color = status_label.get_meta("pending_color")
signal item_clicked(request: NetworkRequest)
func _ready():
# Set up styles for different states
var style_normal = StyleBoxFlat.new()
style_normal.bg_color = Color.TRANSPARENT
style_normal.content_margin_left = 5
style_normal.content_margin_bottom = 5
style_normal.content_margin_right = 5
style_normal.content_margin_top = 5
style_normal.corner_radius_bottom_left = 8
style_normal.corner_radius_bottom_right = 8
style_normal.corner_radius_top_left = 8
style_normal.corner_radius_top_right = 8
add_theme_stylebox_override("panel", style_normal)
# Set up mouse handling
mouse_filter = Control.MOUSE_FILTER_PASS
gui_input.connect(_on_gui_input)
@@ -48,7 +40,7 @@ func update_display():
name_label.text = request.name
status_label.text = request.get_status_display()
type_label.text = request.get_type_display()
size_label.text = request.get_size_display()
size_label.text = NetworkRequest.format_bytes(request.size)
time_label.text = request.get_time_display()
# Color code status
@@ -67,32 +59,12 @@ func _on_gui_input(event: InputEvent):
func set_selected(selected: bool):
if selected:
var style_selected = StyleBoxFlat.new()
style_selected.bg_color = Color(0.2, 0.4, 0.8, 0.3)
style_selected.content_margin_left = 5
style_selected.content_margin_bottom = 5
style_selected.content_margin_right = 5
style_selected.content_margin_top = 5
style_selected.corner_radius_bottom_left = 8
style_selected.corner_radius_bottom_right = 8
style_selected.corner_radius_top_left = 8
style_selected.corner_radius_top_right = 8
add_theme_stylebox_override("panel", style_selected)
add_theme_stylebox_override("panel", selected_style)
else:
var style_normal = StyleBoxFlat.new()
style_normal.bg_color = Color.TRANSPARENT
style_normal.content_margin_left = 5
style_normal.content_margin_bottom = 5
style_normal.content_margin_right = 5
style_normal.content_margin_top = 5
style_normal.corner_radius_bottom_left = 8
style_normal.corner_radius_bottom_right = 8
style_normal.corner_radius_top_left = 8
style_normal.corner_radius_top_right = 8
add_theme_stylebox_override("panel", style_normal)
add_theme_stylebox_override("panel", normal_style)
func hide_columns(should_hide: bool):
status_label.visible = !should_hide
type_label.visible = !should_hide
size_label.visible = !should_hide
time_label.visible = !should_hide
time_label.visible = !should_hide

View File

@@ -0,0 +1 @@
uid://bcs7m624uvv3x

398
flumi/Scripts/NetworkTab.gd Normal file
View File

@@ -0,0 +1,398 @@
class_name NetworkTab
extends VBoxContainer
const NetworkRequestItemScene = preload("res://Scenes/NetworkRequestItem.tscn")
@onready var filter_dropdown: OptionButton = %FilterDropdown
# Details panel components
@onready var close_button: Button = %CloseButton
@onready var details_tab_container: TabContainer = $MainContainer/RightPanel/PanelContainer/HBoxContainer/TabContainer
@onready var headers_tab: VBoxContainer = $MainContainer/RightPanel/PanelContainer/HBoxContainer/TabContainer/Headers
@onready var preview_tab: Control = $MainContainer/RightPanel/PanelContainer/HBoxContainer/TabContainer/Preview
@onready var response_tab: Control = $MainContainer/RightPanel/PanelContainer/HBoxContainer/TabContainer/Response
# Header components
@onready var status_header: Label = %StatusHeader
@onready var type_header: Label = %TypeHeader
@onready var size_header: Label = %SizeHeader
@onready var time_header: Label = %TimeHeader
# Main components
@onready var main_container: HSplitContainer = $MainContainer
@onready var request_list: VBoxContainer = $MainContainer/LeftPanel/ScrollContainer/RequestList
@onready var scroll_container: ScrollContainer = $MainContainer/LeftPanel/ScrollContainer
@onready var details_panel: Control = $MainContainer/RightPanel
@onready var status_bar: HBoxContainer = $HBoxContainer/StatusBar
@onready var request_count_label: Label = $HBoxContainer/StatusBar/RequestCount
@onready var transfer_label: Label = $HBoxContainer/StatusBar/Transfer
@onready var loaded_label: Label = $HBoxContainer/StatusBar/Loaded
@onready var syntax_highlighter = preload("res://Resources/LuaSyntaxHighlighter.tres")
var network_requests: Array[NetworkRequest] = []
var current_filter: NetworkRequest.RequestType = -1 # -1 means all
var selected_request: NetworkRequest = null
var request_items: Dictionary = {}
signal request_selected(request: NetworkRequest)
func _ready():
details_panel.visible = false
if main_container and main_container.size.x > 0:
main_container.split_offset = int(main_container.size.x)
update_status_bar()
NetworkManager.register_dev_tools_network_tab(self)
func add_network_request(request: NetworkRequest):
network_requests.append(request)
var request_item = NetworkRequestItemScene.instantiate() as NetworkRequestItem
request_list.add_child(request_item)
request_item.init(request, self)
request_item.item_clicked.connect(_on_request_item_clicked)
request_items[request.id] = request_item
apply_filter()
update_status_bar()
func apply_filter():
for request in network_requests:
var item = request_items.get(request.id)
if item:
var should_show = (current_filter == -1) or (request.type == current_filter)
item.visible = should_show
func update_request_item(request: NetworkRequest):
var request_item = request_items.get(request.id) as NetworkRequestItem
if not request_item:
return
request_item.update_display()
apply_filter()
update_status_bar()
func update_details_panel(request: NetworkRequest):
clear_details_panel()
update_headers_tab(request)
update_preview_tab(request)
update_response_tab(request)
func clear_details_panel():
for child in headers_tab.get_children(): child.queue_free()
for child in preview_tab.get_children(): child.queue_free()
for child in response_tab.get_children(): child.queue_free()
func create_collapsible_section(title: String, expanded: bool = false) -> VBoxContainer:
var section = VBoxContainer.new()
# Header w/ toggle button
var header = HBoxContainer.new()
header.custom_minimum_size.y = 28
var toggle_button = Button.new()
toggle_button.text = "" if expanded else ""
toggle_button.custom_minimum_size = Vector2(20, 20)
toggle_button.flat = true
toggle_button.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
header.add_child(toggle_button)
var title_label = Label.new()
title_label.text = title
title_label.add_theme_font_size_override("font_size", 14)
title_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
header.add_child(title_label)
section.add_child(header)
# Content container
var content = VBoxContainer.new()
content.visible = expanded
section.add_child(content)
toggle_button.pressed.connect(func():
content.visible = !content.visible
toggle_button.text = "" if content.visible else ""
)
return section
func add_header_row(parent: VBoxContainer, header_name: String, value: String):
var row = HBoxContainer.new()
var name_label = Label.new()
name_label.text = header_name
name_label.custom_minimum_size.x = 200
name_label.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
name_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
name_label.clip_text = true
name_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
row.add_child(name_label)
var value_label = Label.new()
value_label.text = value
value_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
value_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
value_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
row.add_child(value_label)
parent.add_child(row)
func update_headers_tab(request: NetworkRequest):
var general_section = create_collapsible_section("General", true)
headers_tab.add_child(general_section)
var general_content = general_section.get_child(1)
add_header_row(general_content, "Request URL:", request.url)
add_header_row(general_content, "Request Method:", request.method)
add_header_row(general_content, "Status Code:", str(request.status_code) + " " + request.status_text)
# Request Headers section
if not request.request_headers.is_empty():
var request_headers_section = create_collapsible_section("Request Headers", false)
headers_tab.add_child(request_headers_section)
var request_headers_content = request_headers_section.get_child(1)
for header_name in request.request_headers:
add_header_row(request_headers_content, header_name + ":", str(request.request_headers[header_name]))
# Response Headers section
if not request.response_headers.is_empty():
var response_headers_section = create_collapsible_section("Response Headers", false)
headers_tab.add_child(response_headers_section)
var response_headers_content = response_headers_section.get_child(1)
for header_name in request.response_headers:
add_header_row(response_headers_content, header_name + ":", str(request.response_headers[header_name]))
func update_preview_tab(request: NetworkRequest):
# For images, show the image in the preview tab
if request.type == NetworkRequest.RequestType.IMG and request.status == NetworkRequest.RequestStatus.SUCCESS:
var image = Image.new()
var response_bytes = request.response_body_bytes
var load_error = ERR_UNAVAILABLE
load_error = image.load_png_from_buffer(response_bytes)
if load_error != OK:
load_error = image.load_jpg_from_buffer(response_bytes)
if load_error != OK:
load_error = image.load_webp_from_buffer(response_bytes)
if load_error != OK:
load_error = image.load_bmp_from_buffer(response_bytes)
if load_error != OK:
load_error = image.load_tga_from_buffer(response_bytes)
if load_error == OK:
var texture = ImageTexture.create_from_image(image)
var container = VBoxContainer.new()
container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
container.size_flags_vertical = Control.SIZE_SHRINK_CENTER
var texture_rect = TextureRect.new()
texture_rect.texture = texture
texture_rect.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
texture_rect.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
texture_rect.size_flags_vertical = Control.SIZE_SHRINK_CENTER
texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
var img_size = image.get_size()
var max_size = 200.0
var scale_factor = min(max_size / img_size.x, max_size / img_size.y, 1.0)
texture_rect.custom_minimum_size = Vector2(img_size.x * scale_factor, img_size.y * scale_factor)
container.add_child(texture_rect)
preview_tab.add_child(container)
return
else:
var label = Label.new()
label.text = "Failed to load image data (Error: " + str(load_error) + ")"
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
preview_tab.add_child(label)
return
# For non-images, show request body
if request.request_body.is_empty():
var label = Label.new()
label.text = "No request body"
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
preview_tab.add_child(label)
return
# CodeEdit for request body
# TODO: Syntax highlight based on Content-Type, we need a JSON, HTML and CSS highlighter too
var code_edit = CodeEditUtils.create_code_edit({
"text": request.request_body,
"editable": false,
"show_line_numbers": true,
"syntax_highlighter": syntax_highlighter.duplicate()
})
preview_tab.add_child(code_edit)
func update_response_tab(request: NetworkRequest):
if request.type == NetworkRequest.RequestType.IMG:
var label = Label.new()
label.text = "This response contains image data. See the \"Preview\" tab to view the image."
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
response_tab.add_child(label)
return
if request.response_body.is_empty():
var label = Label.new()
label.text = "No response body"
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
response_tab.add_child(label)
return
# Check if we can display the content
var can_display = true
if request.mime_type.begins_with("video/") or request.mime_type.begins_with("audio/"):
can_display = false
if not can_display:
var label = Label.new()
label.text = "Cannot preview this content type: " + request.mime_type
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
response_tab.add_child(label)
return
# Create CodeEdit for response body
var code_edit = CodeEditUtils.create_code_edit({
"text": request.response_body,
"editable": false,
"show_line_numbers": true,
"syntax_highlighter": syntax_highlighter.duplicate()
})
response_tab.add_child(code_edit)
func update_status_bar():
var total_requests = network_requests.size()
var total_size = 0
var loaded_resources = 0
for request in network_requests:
total_size += request.size
if request.status == NetworkRequest.RequestStatus.SUCCESS:
loaded_resources += 1
request_count_label.text = str(total_requests) + " requests"
transfer_label.text = NetworkRequest.format_bytes(total_size) + " transferred"
loaded_label.text = str(loaded_resources) + " resources loaded"
func hide_details_panel():
# Hide details panel and show columns again
details_panel.visible = false
hide_columns(false)
selected_request = null
if main_container.size.x > 0:
main_container.split_offset = int(main_container.size.x)
# Clear selection visual
for req_id in request_items:
var request_item = request_items[req_id] as NetworkRequestItem
request_item.set_selected(false)
func hide_columns(should_hide: bool):
# Hide/show header labels
status_header.visible = !should_hide
type_header.visible = !should_hide
size_header.visible = !should_hide
time_header.visible = !should_hide
# Hide/show status, type, size, time columns for all request items
for request_item in request_items.values():
var network_request_item = request_item as NetworkRequestItem
network_request_item.hide_columns(should_hide)
func clear_all_requests():
for item in request_items.values():
item.queue_free()
network_requests.clear()
request_items.clear()
selected_request = null
# Hide details panel and show columns again
details_panel.visible = false
hide_columns(false)
if main_container.size.x > 0:
main_container.split_offset = int(main_container.size.x)
update_status_bar()
func clear_all_requests_except(preserve_request_id: String):
# Remove all items except the preserved one
var preserved_request = null
var preserved_item = null
for request in network_requests:
if request.id == preserve_request_id:
preserved_request = request
preserved_item = request_items.get(preserve_request_id)
break
# Clear all items except preserved one
for item_id in request_items:
if item_id != preserve_request_id:
var item = request_items[item_id]
item.queue_free()
network_requests.clear()
request_items.clear()
# Re-add preserved request and item
if preserved_request and preserved_item:
network_requests.append(preserved_request)
request_items[preserve_request_id] = preserved_item
selected_request = null
# Hide details panel and show columns again
details_panel.visible = false
hide_columns(false)
if main_container.size.x > 0:
main_container.split_offset = int(main_container.size.x)
update_status_bar()
func _on_request_item_clicked(request: NetworkRequest):
if selected_request == request:
hide_details_panel()
return
selected_request = request
request_selected.emit(request)
for req_id in request_items:
var request_item = request_items[req_id] as NetworkRequestItem
request_item.set_selected(req_id == request.id)
details_panel.visible = true
if main_container.size.x > 0:
# Give 6/8 (3/4) of space to details panel, 2/8 (1/4) to left panel
main_container.split_offset = int(main_container.size.x * 0.25)
hide_columns(true)
update_details_panel(request)
func _on_filter_selected(index: int):
var filter_type = index - 1 # 0 -> -1 (All), 1 -> 0 (Fetch)...
if current_filter == filter_type:
return
current_filter = filter_type
apply_filter()

View File

@@ -0,0 +1 @@
uid://dh3jdrot4r7m3

View File

@@ -35,7 +35,7 @@ var loading_tween: Tween
var scroll_container: ScrollContainer = null
var website_container: VBoxContainer = null
var background_panel: PanelContainer = null
var main_hbox: HBoxContainer = null
var main_hbox: HSplitContainer = null
var dev_tools: Control = null
var dev_tools_visible: bool = false
var lua_apis: Array[LuaAPI] = []
@@ -150,7 +150,7 @@ func init_scene(parent_container: Control) -> void:
style_box.bg_color = Color(1, 1, 1, 1) # White background
background_panel.add_theme_stylebox_override("panel", style_box)
main_hbox = HBoxContainer.new()
main_hbox = HSplitContainer.new()
main_hbox.name = "Tab_MainHBox_" + str(get_instance_id())
main_hbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
main_hbox.size_flags_vertical = Control.SIZE_EXPAND_FILL

View File

@@ -0,0 +1,94 @@
class_name CodeEditUtils
static func create_code_edit(options: Dictionary = {}) -> CodeEdit:
var code_edit = CodeEdit.new()
# Default configuration
var defaults = {
"text": "",
"editable": true,
"size_flags_horizontal": Control.SIZE_EXPAND_FILL,
"size_flags_vertical": Control.SIZE_EXPAND_FILL,
"scroll_fit_content_height": false,
"context_menu_enabled": true,
"shortcut_keys_enabled": true,
"selecting_enabled": true,
"deselect_on_focus_loss_enabled": true,
"drag_and_drop_selection_enabled": false,
"virtual_keyboard_enabled": false,
"middle_mouse_paste_enabled": false,
"show_line_numbers": false,
"syntax_highlighter": null,
"transparent_background": false,
"block_editing_signals": false
}
# Merge user options with defaults
for key in defaults:
if options.has(key):
defaults[key] = options[key]
# Apply basic properties
code_edit.text = defaults.text
code_edit.size_flags_horizontal = defaults.size_flags_horizontal
code_edit.size_flags_vertical = defaults.size_flags_vertical
code_edit.scroll_fit_content_height = defaults.scroll_fit_content_height
code_edit.context_menu_enabled = defaults.context_menu_enabled
code_edit.shortcut_keys_enabled = defaults.shortcut_keys_enabled
code_edit.selecting_enabled = defaults.selecting_enabled
code_edit.deselect_on_focus_loss_enabled = defaults.deselect_on_focus_loss_enabled
code_edit.drag_and_drop_selection_enabled = defaults.drag_and_drop_selection_enabled
code_edit.virtual_keyboard_enabled = defaults.virtual_keyboard_enabled
code_edit.middle_mouse_paste_enabled = defaults.middle_mouse_paste_enabled
# Line numbers
if defaults.show_line_numbers:
code_edit.gutters_draw_line_numbers = true
# Syntax highlighter
if defaults.syntax_highlighter:
code_edit.syntax_highlighter = defaults.syntax_highlighter
# Transparent background styling
if defaults.transparent_background:
var code_style_normal = StyleBoxFlat.new()
code_style_normal.bg_color = Color.TRANSPARENT
code_style_normal.border_width_left = 0
code_style_normal.border_width_top = 0
code_style_normal.border_width_right = 0
code_style_normal.border_width_bottom = 0
code_style_normal.content_margin_bottom = 8
code_edit.add_theme_stylebox_override("normal", code_style_normal)
code_edit.add_theme_stylebox_override("focus", code_style_normal)
# Block editing
# This is because Godot applies some transparency when we simply set editable=false, which I cant be bothered to fix
if defaults.block_editing_signals and defaults.editable:
code_edit.gui_input.connect(_block_editing_input)
return code_edit
static func _block_editing_input(event: InputEvent):
# Block text modification events while allowing selection and copy
if event is InputEventKey:
var key_event = event as InputEventKey
# Allow Ctrl+C (copy), Ctrl+A (select all), arrow keys, etc.
if key_event.pressed:
# Allow copy operations
if key_event.ctrl_pressed and key_event.keycode == KEY_C:
return
if key_event.ctrl_pressed and key_event.keycode == KEY_A:
return
# Allow navigation
if key_event.keycode in [KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_END, KEY_PAGEUP, KEY_PAGEDOWN]:
return
# Block all other key inputs
if key_event.keycode != KEY_ESCAPE:
event.set_input_as_handled()
static func create_readonly_code_edit(text: String, options: Dictionary = {}) -> CodeEdit:
var readonly_options = options.duplicate()
readonly_options["text"] = text
readonly_options["editable"] = false
readonly_options["block_editing_signals"] = false
return create_code_edit(readonly_options)

View File

@@ -0,0 +1 @@
uid://cc7m0rc5oxjnv

View File

@@ -29,14 +29,14 @@ static func resolve_fetch_url(url: String) -> String:
return URLUtils.resolve_url(current_domain, url)
static func _lua_fetch_handler(vm: LuauVM) -> int:
var url: String = vm.luaL_checkstring(1)
var original_url: String = vm.luaL_checkstring(1)
var options: Dictionary = {}
if vm.lua_gettop() >= 2 and vm.lua_istable(2):
options = vm.lua_todictionary(2)
# Resolve relative URLs and default to gurt:// protocol
url = resolve_fetch_url(url)
var url = resolve_fetch_url(original_url)
# Default options
var method = options.get("method", "GET").to_upper()
@@ -296,7 +296,7 @@ static func get_or_create_gurt_client(domain: String) -> GurtProtocolClient:
for ca_cert in CertificateManager.trusted_ca_certificates:
gurt_client.add_ca_certificate(ca_cert)
if not gurt_client.create_client(10):
if not gurt_client.create_client_with_dns(10, GurtProtocol.DNS_SERVER_IP, GurtProtocol.DNS_SERVER_PORT):
gurt_client = null
current_domain = ""
return null
@@ -316,8 +316,6 @@ static func make_gurt_request(url: String, method: String, headers: PackedString
var domain_part = url.replace("gurt://", "")
if domain_part.contains("/"):
domain_part = domain_part.split("/")[0]
if domain_part.contains(":"):
domain_part = domain_part.split(":")[0]
var client = get_or_create_gurt_client(domain_part)
if client == null:

View File

@@ -36,6 +36,9 @@ const MIN_SIZE = Vector2i(750, 200)
var font_dependent_elements: Array = []
var current_domain = ""
var main_navigation_request: NetworkRequest = null
var network_start_time: float = 0.0
var network_end_time: float = 0.0
func should_group_as_inline(element: HTMLParser.HTMLElement) -> bool:
if element.tag_name == "input":
@@ -61,7 +64,7 @@ func _ready():
call_deferred("render")
func _input(event: InputEvent) -> void:
func _input(_event: InputEvent) -> void:
if Input.is_action_just_pressed("DevTools"):
_toggle_dev_tools()
get_viewport().set_input_as_handled()
@@ -104,6 +107,10 @@ func _on_search_submitted(url: String) -> void:
print("Non-GURT URL entered: ", url)
func fetch_gurt_content_async(gurt_url: String, tab: Tab, original_url: String) -> void:
main_navigation_request = NetworkManager.start_request(gurt_url, "GET", false)
main_navigation_request.type = NetworkRequest.RequestType.DOC
network_start_time = Time.get_ticks_msec()
var thread = Thread.new()
var request_data = {"gurt_url": gurt_url}
@@ -125,7 +132,7 @@ func _perform_gurt_request_threaded(request_data: Dictionary) -> Dictionary:
if not client.create_client_with_dns(30, GurtProtocol.DNS_SERVER_IP, GurtProtocol.DNS_SERVER_PORT):
client.disconnect()
return {"success": false, "error": "Failed to connect to GURT DNS server"}
return {"success": false, "error": "Failed to connect to GURT DNS server at " + GurtProtocol.DNS_SERVER_IP + ":" + str(GurtProtocol.DNS_SERVER_PORT)}
var response = client.request(gurt_url, {
"method": "GET"
@@ -136,7 +143,7 @@ func _perform_gurt_request_threaded(request_data: Dictionary) -> Dictionary:
var error_msg = "Connection failed"
if response:
error_msg = "GURT %d: %s" % [response.status_code, response.status_message]
elif not response:
else:
error_msg = "Request timed out or connection failed"
return {"success": false, "error": error_msg}
@@ -149,6 +156,7 @@ func _handle_gurt_result(result: Dictionary, tab: Tab, original_url: String, gur
return
var html_bytes = result.html_bytes
network_end_time = Time.get_ticks_msec()
current_domain = gurt_url
if not search_bar.has_focus():
@@ -156,6 +164,14 @@ func _handle_gurt_result(result: Dictionary, tab: Tab, original_url: String, gur
render_content(html_bytes)
if main_navigation_request:
main_navigation_request.end_time = network_end_time
main_navigation_request.time_ms = network_end_time - network_start_time
var headers = {"content-type": "text/html"}
var body_text = html_bytes.get_string_from_utf8()
NetworkManager.complete_request(main_navigation_request.id, 200, "OK", headers, body_text, html_bytes)
main_navigation_request = null
tab.stop_loading()
func handle_gurt_error(error_message: String, tab: Tab) -> void:
@@ -182,6 +198,11 @@ func render() -> void:
render_content(Constants.HTML_CONTENT)
func render_content(html_bytes: PackedByteArray) -> void:
if main_navigation_request:
NetworkManager.clear_all_requests_except(main_navigation_request.id)
else:
NetworkManager.clear_all_requests()
var active_tab = get_active_tab()
var target_container: Control