diff --git a/dns/frontend/script.lua b/dns/frontend/script.lua
index 896f664..f0fe8e3 100644
--- a/dns/frontend/script.lua
+++ b/dns/frontend/script.lua
@@ -8,7 +8,7 @@ local password_input = gurt.select('#password')
local log_output = gurt.select('#log-output')
function addLog(message)
- gurt.log(message)
+ trace.log(message)
log_output.text = log_output.text .. message .. '\\n'
end
diff --git a/dns/frontend/signup.lua b/dns/frontend/signup.lua
index 544c168..3c7d0ee 100644
--- a/dns/frontend/signup.lua
+++ b/dns/frontend/signup.lua
@@ -9,7 +9,7 @@ local confirm_password_input = gurt.select('#confirm-password')
local log_output = gurt.select('#log-output')
function addLog(message)
- gurt.log(message)
+ trace.log(message)
log_output.text = log_output.text .. message .. '\n'
end
diff --git a/flumi/Assets/Icons/eraser.svg b/flumi/Assets/Icons/eraser.svg
new file mode 100644
index 0000000..9c52339
--- /dev/null
+++ b/flumi/Assets/Icons/eraser.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flumi/Assets/Icons/eraser.svg.import b/flumi/Assets/Icons/eraser.svg.import
new file mode 100644
index 0000000..00f4e07
--- /dev/null
+++ b/flumi/Assets/Icons/eraser.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://custohlvwclqs"
+path="res://.godot/imported/eraser.svg-e6400a62a38066fbd6aaec113f0b7fb0.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://Assets/Icons/eraser.svg"
+dest_files=["res://.godot/imported/eraser.svg-e6400a62a38066fbd6aaec113f0b7fb0.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/flumi/Assets/Icons/funnel.svg b/flumi/Assets/Icons/funnel.svg
new file mode 100644
index 0000000..933a54c
--- /dev/null
+++ b/flumi/Assets/Icons/funnel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flumi/Assets/Icons/funnel.svg.import b/flumi/Assets/Icons/funnel.svg.import
new file mode 100644
index 0000000..6420d50
--- /dev/null
+++ b/flumi/Assets/Icons/funnel.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cqg4eny0nyojd"
+path="res://.godot/imported/funnel.svg-cd3708ed9907c314e118f9c06364d430.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://Assets/Icons/funnel.svg"
+dest_files=["res://.godot/imported/funnel.svg-cd3708ed9907c314e118f9c06364d430.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/flumi/Resources/LuaSyntaxHighlighter.tres b/flumi/Resources/LuaSyntaxHighlighter.tres
new file mode 100644
index 0000000..7ab4c9b
--- /dev/null
+++ b/flumi/Resources/LuaSyntaxHighlighter.tres
@@ -0,0 +1,16 @@
+[gd_resource type="SyntaxHighlighter" script_class="LuaSyntaxHighlighter" load_steps=2 format=3 uid="uid://d0aeuvwp0545i"]
+
+[ext_resource type="Script" uid="uid://qicpfnrmje1v" path="res://Scripts/LuaSyntaxHighlighter.gd" id="1_5xwa7"]
+
+[resource]
+script = ExtResource("1_5xwa7")
+font_color = Color(1, 1, 1, 1)
+keyword_color = Color(1, 0.584314, 0.772549, 1)
+gurt_globals_color = Color(0.584314, 1, 0.635294, 1)
+function_color = Color(0.584314, 0.839216, 1, 1)
+member_color = Color(1, 1, 0.584314, 1)
+number_color = Color(0.772549, 0.584314, 1, 1)
+string_color = Color(1, 0.772549, 0.584314, 1)
+comment_color = Color(0.490196, 0.54902, 0.545098, 1)
+symbol_color = Color(0.470588, 0.87451, 0.827451, 1)
+metadata/_custom_type_script = "uid://qicpfnrmje1v"
diff --git a/flumi/Scenes/DevTools.tscn b/flumi/Scenes/DevTools.tscn
new file mode 100644
index 0000000..ae30000
--- /dev/null
+++ b/flumi/Scenes/DevTools.tscn
@@ -0,0 +1,243 @@
+[gd_scene load_steps=19 format=3 uid="uid://cgav3xl2xgupb"]
+
+[ext_resource type="Script" uid="uid://vrobqac6makc" path="res://Scripts/DevToolsConsole.gd" id="2_3m6n9"]
+[ext_resource type="Texture2D" uid="uid://custohlvwclqs" path="res://Assets/Icons/eraser.svg" id="3_6hj4c"]
+[ext_resource type="Texture2D" uid="uid://cqg4eny0nyojd" path="res://Assets/Icons/funnel.svg" id="4_ynqb1"]
+[ext_resource type="SyntaxHighlighter" uid="uid://d0aeuvwp0545i" path="res://Resources/LuaSyntaxHighlighter.tres" id="5_xkykt"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_6hj4c"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_6hj4c"]
+content_margin_left = 2.0
+content_margin_top = 2.0
+content_margin_right = 2.0
+content_margin_bottom = 2.0
+bg_color = Color(0.105882, 0.105882, 0.105882, 1)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qb7hm"]
+content_margin_left = 15.0
+content_margin_top = 15.0
+content_margin_right = 15.0
+content_margin_bottom = 15.0
+bg_color = Color(0.137255, 0.137255, 0.137255, 1)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ynqb1"]
+content_margin_left = 8.0
+content_margin_top = 4.0
+content_margin_right = 8.0
+content_margin_bottom = 4.0
+bg_color = Color(0.105882, 0.105882, 0.105882, 1)
+border_width_bottom = 1
+border_color = Color(0.247059, 0.466667, 0.807843, 1)
+corner_radius_top_left = 10
+corner_radius_top_right = 10
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_8muo7"]
+content_margin_left = 8.0
+content_margin_top = 4.0
+content_margin_right = 8.0
+content_margin_bottom = 4.0
+bg_color = Color(0.137255, 0.137255, 0.137255, 1)
+corner_radius_top_left = 10
+corner_radius_top_right = 10
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_xkykt"]
+content_margin_left = 8.0
+content_margin_top = 4.0
+content_margin_right = 8.0
+content_margin_bottom = 4.0
+bg_color = Color(0.105882, 0.105882, 0.105882, 1)
+border_color = Color(0.8, 0.8, 1, 0)
+corner_radius_top_left = 10
+corner_radius_top_right = 10
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ynqb1"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_pqhy6"]
+bg_color = Color(0.168627, 0.168627, 0.168627, 1)
+corner_radius_top_left = 50
+corner_radius_top_right = 50
+corner_radius_bottom_right = 50
+corner_radius_bottom_left = 50
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_apu5o"]
+bg_color = Color(0.6, 0.6, 0.6, 0)
+draw_center = false
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dderp"]
+bg_color = Color(0.6, 0.6, 0.6, 0)
+draw_center = false
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ko37l"]
+content_margin_left = 8.0
+content_margin_right = 8.0
+bg_color = Color(0.168627, 0.168627, 0.168627, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0.247059, 0.466667, 0.807843, 1)
+corner_radius_top_left = 15
+corner_radius_top_right = 15
+corner_radius_bottom_right = 15
+corner_radius_bottom_left = 15
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_65n6c"]
+content_margin_left = 8.0
+content_margin_right = 8.0
+bg_color = Color(0.168627, 0.168627, 0.168627, 1)
+corner_radius_top_left = 15
+corner_radius_top_right = 15
+corner_radius_bottom_right = 15
+corner_radius_bottom_left = 15
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_up43w"]
+content_margin_left = 8.0
+content_margin_top = 8.0
+content_margin_bottom = 4.0
+bg_color = Color(0.168627, 0.168627, 0.168627, 1)
+corner_radius_top_left = 8
+corner_radius_top_right = 8
+corner_radius_bottom_right = 8
+corner_radius_bottom_left = 8
+expand_margin_left = 4.0
+expand_margin_right = 4.0
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qb3ke"]
+content_margin_left = 8.0
+content_margin_top = 8.0
+content_margin_bottom = 4.0
+bg_color = Color(0.168627, 0.168627, 0.168627, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0.247059, 0.466667, 0.807843, 1)
+corner_radius_top_left = 8
+corner_radius_top_right = 8
+corner_radius_bottom_right = 8
+corner_radius_bottom_left = 8
+expand_margin_left = 4.0
+expand_margin_right = 4.0
+
+[node name="DevTools" type="VBoxContainer"]
+custom_minimum_size = Vector2(450, 400)
+size_flags_vertical = 3
+
+[node name="TabContainer" type="TabContainer" parent="."]
+custom_minimum_size = Vector2(300, 0)
+layout_mode = 2
+size_flags_vertical = 3
+theme_override_colors/drop_mark_color = Color(0.247059, 0.466667, 0.807843, 1)
+theme_override_styles/tab_focus = SubResource("StyleBoxEmpty_6hj4c")
+theme_override_styles/tabbar_background = SubResource("StyleBoxFlat_6hj4c")
+theme_override_styles/panel = SubResource("StyleBoxFlat_qb7hm")
+theme_override_styles/tab_selected = SubResource("StyleBoxFlat_ynqb1")
+theme_override_styles/tab_hovered = SubResource("StyleBoxFlat_8muo7")
+theme_override_styles/tab_unselected = SubResource("StyleBoxFlat_xkykt")
+tab_alignment = 1
+current_tab = 0
+drag_to_rearrange_enabled = true
+
+[node name="Console" type="VBoxContainer" parent="TabContainer"]
+layout_mode = 2
+script = ExtResource("2_3m6n9")
+metadata/_tab_index = 0
+
+[node name="Toolbar" type="HBoxContainer" parent="TabContainer/Console"]
+layout_mode = 2
+theme_override_constants/separation = 8
+
+[node name="ClearButton" type="Button" parent="TabContainer/Console/Toolbar"]
+custom_minimum_size = Vector2(32, 32)
+layout_mode = 2
+theme_override_constants/icon_max_width = 20
+theme_override_styles/focus = SubResource("StyleBoxEmpty_ynqb1")
+theme_override_styles/hover = SubResource("StyleBoxFlat_pqhy6")
+theme_override_styles/pressed = SubResource("StyleBoxFlat_apu5o")
+theme_override_styles/normal = SubResource("StyleBoxFlat_dderp")
+icon = ExtResource("3_6hj4c")
+icon_alignment = 1
+
+[node name="LineEdit" type="LineEdit" parent="TabContainer/Console/Toolbar"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_styles/focus = SubResource("StyleBoxFlat_ko37l")
+theme_override_styles/normal = SubResource("StyleBoxFlat_65n6c")
+placeholder_text = "Filter"
+right_icon = ExtResource("4_ynqb1")
+
+[node name="Spacer2" type="Control" parent="TabContainer/Console"]
+custom_minimum_size = Vector2(0, 15)
+layout_mode = 2
+
+[node name="ScrollContainer" type="ScrollContainer" parent="TabContainer/Console"]
+custom_minimum_size = Vector2(0, 200)
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="LogContainer" type="VBoxContainer" parent="TabContainer/Console/ScrollContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="Spacer" type="Control" parent="TabContainer/Console"]
+custom_minimum_size = Vector2(0, 15)
+layout_mode = 2
+
+[node name="InputContainer" type="HBoxContainer" parent="TabContainer/Console"]
+layout_mode = 2
+theme_override_constants/separation = 4
+
+[node name="InputLine" type="CodeEdit" parent="TabContainer/Console/InputContainer"]
+clip_contents = false
+custom_minimum_size = Vector2(0, 35)
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_styles/normal = SubResource("StyleBoxFlat_up43w")
+theme_override_styles/focus = SubResource("StyleBoxFlat_qb3ke")
+text = "test"
+placeholder_text = "Enter Lua code..."
+scroll_fit_content_height = true
+caret_blink = true
+syntax_highlighter = ExtResource("5_xkykt")
+highlight_all_occurrences = true
+highlight_current_line = true
+symbol_tooltip_on_hover = true
+gutters_draw_line_numbers = true
+auto_brace_completion_enabled = true
+auto_brace_completion_highlight_matching = true
+
+[node name="PositioningTimer" type="Timer" parent="TabContainer/Console/InputContainer"]
+
+[node name="Elements" type="Label" parent="TabContainer"]
+visible = false
+layout_mode = 2
+text = "Elements tab - Coming soon"
+horizontal_alignment = 1
+vertical_alignment = 1
+metadata/_tab_index = 1
+
+[node name="Sources" type="Label" parent="TabContainer"]
+visible = false
+layout_mode = 2
+text = "Sources tab - Coming soon"
+horizontal_alignment = 1
+vertical_alignment = 1
+metadata/_tab_index = 2
+
+[node name="Network" type="Label" parent="TabContainer"]
+visible = false
+layout_mode = 2
+text = "Network tab - Coming soon"
+horizontal_alignment = 1
+vertical_alignment = 1
+metadata/_tab_index = 3
+
+[node name="Application" type="Label" parent="TabContainer"]
+visible = false
+layout_mode = 2
+text = "Application tab - Coming soon"
+horizontal_alignment = 1
+vertical_alignment = 1
+metadata/_tab_index = 4
diff --git a/flumi/Scripts/B9/Lua.gd b/flumi/Scripts/B9/Lua.gd
index affbfab..b22f155 100644
--- a/flumi/Scripts/B9/Lua.gd
+++ b/flumi/Scripts/B9/Lua.gd
@@ -29,7 +29,7 @@ func _init():
timeout_manager = LuaTimeoutManager.new()
threaded_vm = ThreadedLuaVM.new()
threaded_vm.script_completed.connect(_on_threaded_script_completed)
- threaded_vm.script_error.connect(func(e): print(e))
+ threaded_vm.script_error.connect(_on_threaded_script_error)
threaded_vm.dom_operation_request.connect(_handle_dom_operation)
threaded_vm.print_output.connect(_on_print_output)
@@ -645,8 +645,11 @@ func execute_lua_script(code: String):
func _on_threaded_script_completed(_result: Dictionary):
pass
-func _on_print_output(message: String):
- LuaPrintUtils.lua_print_direct(message)
+func _on_threaded_script_error(error_message: String):
+ Trace.trace_error("RuntimeError: " + error_message)
+
+func _on_print_output(message: Dictionary):
+ Trace.get_instance().log_message.emit(message, "lua", Time.get_ticks_msec() / 1000.0)
func kill_script_execution():
threaded_vm.stop_lua_thread()
diff --git a/flumi/Scripts/DevToolsConsole.gd b/flumi/Scripts/DevToolsConsole.gd
new file mode 100644
index 0000000..8f5b82a
--- /dev/null
+++ b/flumi/Scripts/DevToolsConsole.gd
@@ -0,0 +1,574 @@
+class_name DevToolsConsole
+extends VBoxContainer
+
+@onready var log_container: VBoxContainer = $ScrollContainer/LogContainer
+@onready var input_line: CodeEdit = $InputContainer/InputLine
+@onready var scroll_container: ScrollContainer = $ScrollContainer
+@onready var clear_button: Button = $Toolbar/ClearButton
+@onready var filter_input: LineEdit = $Toolbar/LineEdit
+
+var log_entries: Array[Dictionary] = []
+var current_filter: String = ""
+var last_log_item: Control = null
+var last_log_entry: Dictionary = {}
+var input_history: Array[String] = []
+var history_index: int = -1
+
+func _ready():
+ connect_signals()
+ initialize_filter()
+ load_existing_logs()
+
+ visibility_changed.connect(_on_visibility_changed)
+
+func initialize_filter() -> void:
+ filter_input.placeholder_text = "Filter"
+ 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)
+ filter_input.text_changed.connect(_on_filter_changed)
+
+func load_existing_logs() -> void:
+ var existing_messages = Trace.get_all_messages()
+ 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)
+
+func add_log_entry(message: Variant, level: String, timestamp: float) -> void:
+ var entry = {
+ "message": message,
+ "level": level,
+ "timestamp": timestamp,
+ "count": 1
+ }
+
+ if can_group_with_last_entry(entry):
+ last_log_entry.count += 1
+ last_log_entry.timestamp = timestamp
+ log_entries[log_entries.size() - 1] = last_log_entry
+
+ update_log_item_display(last_log_item, last_log_entry)
+ return
+
+ log_entries.append(entry)
+ last_log_entry = entry
+
+ var should_add_separator = false
+ if level == "log" or level == "lua" or level == "input":
+ if log_container.get_child_count() > 0:
+ var last_displayed_entry = get_last_displayed_entry()
+ if last_displayed_entry and last_displayed_entry.level != "warning" and last_displayed_entry.level != "error":
+ should_add_separator = true
+
+ if should_add_separator:
+ var separator = HSeparator.new()
+ separator.add_theme_color_override("separator", Color.GRAY * 0.3)
+ log_container.add_child(separator)
+
+ var log_item = create_log_item(entry)
+ log_container.add_child(log_item)
+ last_log_item = log_item
+
+ apply_filter_to_item(log_item, entry)
+
+ call_deferred("_scroll_to_bottom")
+
+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()
+
+ 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))
+ message_code_edit.text_changed.connect(_on_log_code_edit_text_changed.bind(message_code_edit, input_display_text))
+
+ return message_code_edit
+
+ if entry.level == "lua" and entry.message is Dictionary and entry.message.has("parts"):
+ return create_structured_log_item(entry)
+
+ var panel = PanelContainer.new()
+ var style_box = StyleBoxFlat.new()
+
+ match entry.level:
+ "warning":
+ style_box.bg_color = Color.YELLOW
+ style_box.bg_color.a = 0.2
+ style_box.corner_radius_top_left = 6
+ style_box.corner_radius_top_right = 6
+ style_box.corner_radius_bottom_left = 6
+ style_box.corner_radius_bottom_right = 6
+ style_box.content_margin_left = 8
+ style_box.content_margin_right = 8
+ style_box.content_margin_top = 4
+ style_box.content_margin_bottom = 4
+ "error":
+ style_box.bg_color = Color.RED
+ style_box.bg_color.a = 0.2
+ style_box.corner_radius_top_left = 6
+ style_box.corner_radius_top_right = 6
+ style_box.corner_radius_bottom_left = 6
+ style_box.corner_radius_bottom_right = 6
+ style_box.content_margin_left = 8
+ style_box.content_margin_right = 8
+ style_box.content_margin_top = 4
+ style_box.content_margin_bottom = 4
+ _:
+ style_box.bg_color = Color.TRANSPARENT
+
+ panel.add_theme_stylebox_override("panel", style_box)
+
+ var container: Control
+ if entry.level == "warning" or entry.level == "error":
+ var margin_container = MarginContainer.new()
+ margin_container.add_child(panel)
+ container = margin_container
+ else:
+ container = panel
+
+ var message_label = Label.new()
+ var display_text = get_display_text_for_entry(entry)
+ message_label.text = display_text
+ message_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ message_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
+
+ match entry.level:
+ "warning":
+ message_label.add_theme_color_override("font_color", Color.YELLOW)
+ "error":
+ message_label.add_theme_color_override("font_color", Color.WHITE)
+ _:
+ message_label.add_theme_color_override("font_color", Color.WHITE)
+
+ panel.add_child(message_label)
+ return container
+
+func _scroll_to_bottom() -> void:
+ if scroll_container:
+ scroll_container.scroll_vertical = int(scroll_container.get_v_scroll_bar().max_value)
+
+func _on_clear_pressed() -> void:
+ for child in log_container.get_children():
+ child.queue_free()
+ log_entries.clear()
+
+ last_log_item = null
+ last_log_entry = {}
+
+ Trace.clear_messages()
+
+func _on_input_gui_input(event: InputEvent) -> void:
+ if event is InputEventKey and event.pressed:
+ if event.keycode == KEY_ENTER and event.ctrl_pressed:
+ var text = input_line.text.strip_edges()
+ if not text.is_empty():
+ _on_input_submitted(text)
+ input_line.text = ""
+ get_viewport().set_input_as_handled()
+ elif event.keycode == KEY_UP and not event.ctrl_pressed and not event.shift_pressed and not event.alt_pressed:
+ if input_line.get_caret_column() == 0 and input_line.get_caret_line() == 0:
+ navigate_history_up()
+ get_viewport().set_input_as_handled()
+ elif event.keycode == KEY_DOWN and not event.ctrl_pressed and not event.shift_pressed and not event.alt_pressed:
+ var caret_pos = input_line.get_caret_column()
+ var last_line = input_line.get_line_count() - 1
+ var last_line_length = input_line.get_line(last_line).length()
+ if input_line.get_caret_line() == last_line and caret_pos == last_line_length:
+ navigate_history_down()
+ get_viewport().set_input_as_handled()
+
+func navigate_history_up() -> void:
+ if input_history.is_empty():
+ return
+ if history_index == -1:
+ history_index = input_history.size() - 1
+ elif history_index > 0:
+ history_index -= 1
+
+ if history_index >= 0 and history_index < input_history.size():
+ input_line.text = input_history[history_index]
+ call_deferred("move_caret_to_end")
+
+func navigate_history_down() -> void:
+ if input_history.is_empty() or history_index == -1:
+ return
+
+ history_index += 1
+
+ if history_index >= input_history.size():
+ history_index = -1
+ input_line.text = ""
+ else:
+ input_line.text = input_history[history_index]
+ call_deferred("move_caret_to_end")
+
+func move_caret_to_end() -> void:
+ var last_line = input_line.get_line_count() - 1
+ var last_line_length = input_line.get_line(last_line).length()
+ input_line.set_caret_line(last_line)
+ input_line.set_caret_column(last_line_length)
+
+func _on_input_submitted(text: String) -> void:
+ if text.strip_edges().is_empty():
+ return
+
+ if input_history.is_empty() or input_history[input_history.size() - 1] != text:
+ input_history.append(text)
+ if input_history.size() > 100:
+ input_history.pop_front()
+
+ history_index = -1
+
+ add_log_entry("> " + text, "input", Time.get_ticks_msec() / 1000.0)
+
+ execute_lua_command(text)
+
+func execute_lua_command(code: String) -> void:
+ var main_scene = Engine.get_main_loop().current_scene
+ if main_scene and main_scene.has_method("get_active_tab"):
+ var active_tab = main_scene.get_active_tab()
+ if active_tab and active_tab.lua_apis.size() > 0:
+ var lua_api = active_tab.lua_apis[0]
+ if lua_api:
+ var is_expression = is_likely_expression(code)
+ if is_expression:
+ var wrapped_code = "print(" + code + ")"
+ lua_api.execute_lua_script(wrapped_code)
+ else:
+ lua_api.execute_lua_script(code)
+ return
+
+ add_log_entry("No Lua context available", "error", Time.get_ticks_msec() / 1000.0)
+
+func is_likely_expression(code: String) -> bool:
+ var trimmed = code.strip_edges()
+ var statement_keywords = ["if", "for", "while", "do", "function", "local", "return", "break"]
+ for keyword in statement_keywords:
+ if trimmed.begins_with(keyword + " ") or trimmed.begins_with(keyword + "("):
+ return false
+ if "=" in trimmed and not ("==" in trimmed or "!=" in trimmed or ">=" in trimmed or "<=" in trimmed):
+ return false
+ if "print(" in trimmed or "console.log(" in trimmed or "_trace_" in trimmed:
+ return false
+ if trimmed.ends_with(")") or trimmed.ends_with("]") or not (" " in trimmed):
+ return true
+ return true
+
+func _on_filter_changed(new_text: String) -> void:
+ current_filter = new_text.strip_edges()
+ refresh_log_display()
+
+func refresh_log_display() -> void:
+ for i in range(log_container.get_child_count()):
+ var child = log_container.get_child(i)
+ if child is HSeparator:
+ child.visible = should_separator_be_visible(i)
+ else:
+ var entry_index = get_entry_index_for_child(i)
+ if entry_index >= 0 and entry_index < log_entries.size():
+ var entry = log_entries[entry_index]
+ apply_filter_to_item(child, entry)
+
+ call_deferred("_scroll_to_bottom")
+
+func apply_filter_to_item(item: Control, entry: Dictionary) -> void:
+ item.visible = entry_matches_filter(entry)
+
+func entry_matches_filter(entry: Dictionary) -> bool:
+ if current_filter.is_empty():
+ return true
+
+ var message_text = ""
+ if entry.message is Dictionary and entry.message.has("parts"):
+ message_text = get_display_text_for_entry(entry)
+ else:
+ message_text = str(entry.message)
+
+ if current_filter.to_lower() in message_text.to_lower():
+ return true
+
+ if current_filter.to_lower() == entry.level.to_lower():
+ return true
+
+ return false
+
+func get_last_displayed_entry() -> Dictionary:
+ for i in range(log_entries.size() - 2, -1, -1): # Start from second-to-last entry
+ var entry = log_entries[i]
+ if entry_matches_filter(entry):
+ return entry
+ return {}
+
+func should_separator_be_visible(separator_index: int) -> bool:
+ var before_visible = false
+ var after_visible = false
+
+ if separator_index > 0:
+ var before_child = log_container.get_child(separator_index - 1)
+ before_visible = before_child.visible
+
+
+ if separator_index < log_container.get_child_count() - 1:
+ var after_child = log_container.get_child(separator_index + 1)
+ after_visible = after_child.visible
+
+ return before_visible and after_visible
+
+func get_entry_index_for_child(child_index: int) -> int:
+ var entry_index = 0
+ for i in range(child_index):
+ var child = log_container.get_child(i)
+ if not child is HSeparator:
+ entry_index += 1
+ return entry_index
+
+func can_group_with_last_entry(entry: Dictionary) -> bool:
+ if last_log_entry.is_empty() or last_log_item == null:
+ return false
+
+ if entry.level != last_log_entry.level:
+ return false
+
+ var current_message_text = ""
+ var last_message_text = ""
+
+ if entry.message is Dictionary and entry.message.has("parts"):
+ current_message_text = get_display_text_for_entry(entry)
+ else:
+ current_message_text = str(entry.message)
+
+ if last_log_entry.message is Dictionary and last_log_entry.message.has("parts"):
+ last_message_text = get_display_text_for_entry(last_log_entry)
+ else:
+ last_message_text = str(last_log_entry.message)
+
+ if current_message_text != last_message_text:
+ return false
+
+ if entry.level == "input":
+ return false
+
+ return true
+
+func update_log_item_display(log_item: Control, entry: Dictionary) -> void:
+ if entry.level == "input":
+ var code_edit = log_item as CodeEdit
+ if code_edit:
+ code_edit.text = get_display_text_for_entry(entry)
+ else:
+ var label = find_message_label_in_item(log_item)
+ if label:
+ label.text = get_display_text_for_entry(entry)
+
+func find_message_code_edit_in_item(item: Control) -> CodeEdit:
+ if item is CodeEdit:
+ return item as CodeEdit
+
+ if item is MarginContainer:
+ var panel = item.get_child(0) as PanelContainer
+ if panel:
+ return panel.get_child(0) as CodeEdit
+ elif item is PanelContainer:
+ return item.get_child(0) as CodeEdit
+ return null
+
+func find_message_label_in_item(item: Control) -> Label:
+ if item is MarginContainer:
+ var panel = item.get_child(0) as PanelContainer
+ if panel:
+ return panel.get_child(0) as Label
+ elif item is PanelContainer:
+ return item.get_child(0) as Label
+ return null
+
+func _on_log_code_edit_gui_input(event: InputEvent) -> void:
+ if event is InputEventKey and event.pressed:
+ var key = event.keycode
+ if event.ctrl_pressed and (key == KEY_C or key == KEY_A):
+ return
+ if key in [KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_END, KEY_PAGEUP, KEY_PAGEDOWN]:
+ return
+ if event.shift_pressed and key in [KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_END]:
+ return
+ get_viewport().set_input_as_handled()
+ elif event is InputEventMouseButton:
+ return
+
+func _on_log_code_edit_focus_entered(code_edit: CodeEdit) -> void:
+ code_edit.release_focus()
+
+func _on_log_code_edit_text_changed(code_edit: CodeEdit, original_text: String) -> void:
+ if code_edit.text != original_text:
+ code_edit.text = original_text
+
+func get_display_text_for_entry(entry: Dictionary) -> String:
+ var count = entry.get("count", 1)
+ var message = entry.message
+
+ var base_text = ""
+ if message is Dictionary and message.has("parts"):
+ var parts = message.parts
+ var text_parts = []
+ for part in parts:
+ if part.type == "primitive":
+ text_parts.append(str(part.data))
+ elif part.type == "table":
+ var key_count = part.data.keys().size()
+ text_parts.append("Object {" + str(key_count) + "}")
+ base_text = "\t".join(text_parts)
+ else:
+ base_text = str(message)
+
+ if count > 1:
+ return "(" + str(count) + ") " + base_text
+ else:
+ return base_text
+
+func create_structured_log_item(entry) -> Control:
+ var container = VBoxContainer.new()
+ var parts = entry.message.parts
+ if parts.size() == 1 and parts[0].type == "primitive":
+ var simple_panel = PanelContainer.new()
+ var style_box = StyleBoxFlat.new()
+ style_box.bg_color = Color.TRANSPARENT
+ style_box.content_margin_left = 10
+ style_box.content_margin_top = 5
+ style_box.content_margin_right = 10
+ style_box.content_margin_bottom = 5
+ simple_panel.add_theme_stylebox_override("panel", style_box)
+ var label = Label.new()
+ label.text = str(parts[0].data)
+ label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
+ simple_panel.add_child(label)
+ container.add_child(simple_panel)
+ return container
+
+ for i in range(parts.size()):
+ var part = parts[i]
+
+ if part.type == "primitive":
+ var text_panel = PanelContainer.new()
+ var style_box = StyleBoxFlat.new()
+ style_box.bg_color = Color.TRANSPARENT
+ style_box.content_margin_left = 10
+ style_box.content_margin_top = 2
+ style_box.content_margin_right = 10
+ style_box.content_margin_bottom = 2
+ text_panel.add_theme_stylebox_override("panel", style_box)
+
+ var label = Label.new()
+ label.text = str(part.data)
+ label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
+ label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ text_panel.add_child(label)
+ container.add_child(text_panel)
+
+ elif part.type == "table":
+ var table_item = create_expandable_table_item(part.data)
+ container.add_child(table_item)
+
+ return container
+
+func create_expandable_table_item(table_data) -> Control:
+ var main_container = VBoxContainer.new()
+
+ var header_container = HBoxContainer.new()
+ header_container.custom_minimum_size.y = 24
+
+ var chevron_button = Button.new()
+ chevron_button.text = "▶"
+ chevron_button.custom_minimum_size = Vector2(20, 20)
+ chevron_button.flat = true
+ chevron_button.focus_mode = Control.FOCUS_NONE
+ header_container.add_child(chevron_button)
+
+ var summary_label = Label.new()
+ var key_count = table_data.keys().size()
+ if table_data.has("__type"):
+ summary_label.text = str(table_data.get("__type", "Object")) + " {" + str(key_count) + "}"
+ else:
+ summary_label.text = "Object {" + str(key_count) + "}"
+ summary_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
+ summary_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ header_container.add_child(summary_label)
+
+ main_container.add_child(header_container)
+
+ var content_container = VBoxContainer.new()
+ content_container.visible = false
+
+ for key in table_data.keys():
+ if key == "__type":
+ continue
+
+ var value = table_data[key]
+ var row_container = HBoxContainer.new()
+
+ var key_label = Label.new()
+ key_label.text = str(key) + ": "
+ key_label.custom_minimum_size.x = 80
+ key_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
+ row_container.add_child(key_label)
+
+ var value_label = Label.new()
+ if value is Dictionary:
+ var nested_count = value.keys().size()
+ value_label.text = "Object {" + str(nested_count) + "}"
+ value_label.modulate = Color.GRAY
+ elif value is Array:
+ value_label.text = "Array [" + str(value.size()) + "]"
+ value_label.modulate = Color.GRAY
+ else:
+ value_label.text = str(value)
+
+ value_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
+ value_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
+ value_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ row_container.add_child(value_label)
+
+ content_container.add_child(row_container)
+
+ main_container.add_child(content_container)
+
+ chevron_button.pressed.connect(func():
+ content_container.visible = !content_container.visible
+ chevron_button.text = "▼" if content_container.visible else "▶"
+ )
+
+ return main_container
+
+func _on_visibility_changed() -> void:
+ if visible:
+ input_line.grab_focus()
diff --git a/flumi/Scripts/DevToolsConsole.gd.uid b/flumi/Scripts/DevToolsConsole.gd.uid
new file mode 100644
index 0000000..5dc4c11
--- /dev/null
+++ b/flumi/Scripts/DevToolsConsole.gd.uid
@@ -0,0 +1 @@
+uid://vrobqac6makc
diff --git a/flumi/Scripts/LuaCodeEdit.gd.uid b/flumi/Scripts/LuaCodeEdit.gd.uid
new file mode 100644
index 0000000..1f1a4ba
--- /dev/null
+++ b/flumi/Scripts/LuaCodeEdit.gd.uid
@@ -0,0 +1 @@
+uid://cer1rniskhi24
diff --git a/flumi/Scripts/LuaSyntaxHighlighter.gd b/flumi/Scripts/LuaSyntaxHighlighter.gd
new file mode 100644
index 0000000..1b0a922
--- /dev/null
+++ b/flumi/Scripts/LuaSyntaxHighlighter.gd
@@ -0,0 +1,190 @@
+@tool
+class_name LuaSyntaxHighlighter
+extends SyntaxHighlighter
+
+@export_group("Colors")
+@export var font_color: Color = Color("#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)
+@export var member_color: Color = Color.from_string("#9cdcfe", Color.WHITE)
+@export var number_color: Color = Color.from_string("#b5cea8", Color.WHITE)
+@export var string_color: Color = Color.from_string("#ce9178", Color.WHITE)
+@export var comment_color: Color = Color.from_string("#6a9955", Color.WHITE)
+@export var symbol_color: Color = Color.from_string("#c586c0", Color.WHITE)
+
+enum State { DEFAULT, IN_MULTILINE_COMMENT, IN_MULTILINE_STRING }
+var _state_cache: Dictionary = {}
+
+var _keywords: Dictionary = {}
+var _built_in_functions: Dictionary = {}
+var _gurt_globals: Dictionary = {}
+var _member_keywords: Dictionary = {}
+
+func _init() -> void:
+ for k in ["and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while"]:
+ _keywords[k] = true
+
+ for f in ["assert", "collectgarbage", "dofile", "error", "getmetatable", "ipairs", "load", "loadfile", "next", "pairs", "pcall", "print", "rawequal", "rawget", "rawlen", "rawset", "require", "select", "setmetatable", "tonumber", "tostring", "type", "xpcall"]:
+ _built_in_functions[f] = true
+
+ for g in ["gurt", "trace", "JSON", "Time", "WebSocket", "Clipboard", "Regex", "setTimeout", "setInterval", "clearTimeout", "clearInterval", "fetch", "urlEncode", "urlDecode"]:
+ _gurt_globals[g] = true
+
+ var members = [
+ "select", "selectAll", "create", "body", "location", "href", "reload", "goto", "query", "get", "has", "getAll",
+ "log", "warn", "error", "text", "value", "visible", "children", "parent", "nextSibling", "previousSibling",
+ "firstChild", "lastChild", "classList", "on", "append", "remove", "insertBefore", "insertAfter", "replace",
+ "clone", "getAttribute", "setAttribute", "show", "hide", "focus", "unfocus", "createTween", "unsubscribe",
+ "add", "remove", "toggle", "contains", "item", "length", "to", "duration", "easing", "transition", "play",
+ "pause", "stop", "currentTime", "volume", "loop", "src", "playing", "paused", "withContext", "fillRect",
+ "strokeRect", "clearRect", "drawCircle", "drawText", "setFont", "measureText", "beginPath", "moveTo",
+ "lineTo", "closePath", "stroke", "fill", "arc", "quadraticCurveTo", "bezierCurveTo", "setStrokeStyle",
+ "setFillStyle", "setLineWidth", "save", "translate", "rotate", "scale", "restore", "source", "ok", "json",
+ "status", "statusText", "headers", "stringify", "parse", "now", "format", "date", "sleep", "benchmark",
+ "timer", "delay", "elapsed", "reset", "complete", "remaining", "new", "send", "close", "readyState",
+ "test", "match", "tostring", "replace", "replaceAll", "trim",
+ ]
+ for m in members:
+ _member_keywords[m] = true
+
+func _is_whitespace(char: String) -> bool:
+ return char == " " or char == "\t"
+
+
+func _clear_highlighting_cache():
+ _state_cache.clear()
+
+func _get_initial_state() -> int:
+ return State.DEFAULT
+
+func _get_line_state(p_line: int, p_state: int) -> int:
+ var current_state: int = p_state
+ var line_text: String = get_text_edit().get_line(p_line)
+ var line_len: int = line_text.length()
+ var i := 0
+ while i < line_len:
+ if current_state == State.DEFAULT:
+ if i + 3 < line_len and line_text.substr(i, 4) == "--[[":
+ current_state = State.IN_MULTILINE_COMMENT
+ i += 4
+ continue
+ if i + 1 < line_len and line_text.substr(i, 2) == "[[":
+ current_state = State.IN_MULTILINE_STRING
+ i += 2
+ continue
+ if line_text[i] == "'" or line_text[i] == "\"":
+ var quote = line_text[i]
+ i += 1
+ while i < line_len:
+ if line_text[i] == "\\": i += 2; continue
+ if line_text[i] == quote: break
+ i += 1
+ else:
+ var end_idx = line_text.find("]]", i)
+ if end_idx != -1:
+ current_state = State.DEFAULT
+ i = end_idx + 2
+ continue
+ else:
+ i = line_len
+ i += 1
+ _state_cache[p_line] = current_state
+ return current_state
+
+func _get_line_syntax_highlighting(p_line: int) -> Dictionary:
+ var color_map := {}
+ var start_state: int = _state_cache.get(p_line - 1, _get_initial_state())
+
+ var line_text: String = get_text_edit().get_line(p_line)
+ var line_len: int = line_text.length()
+
+ color_map[0] = {"color": font_color}
+
+ var i := 0
+ if start_state != State.DEFAULT:
+ var end_idx = line_text.find("]]")
+ var region_color = comment_color if start_state == State.IN_MULTILINE_COMMENT else string_color
+
+ color_map[0] = {"color": region_color}
+ if end_idx == -1:
+ return color_map
+ else:
+ i = end_idx + 2
+ if i < line_len:
+ color_map[i] = {"color": font_color}
+ start_state = State.DEFAULT
+ while i < line_len:
+ var start_col: int = i
+ var current_char: String = line_text[i]
+
+ if current_char == "-" and i + 1 < line_len and line_text[i+1] == "-":
+ if not (i + 3 < line_len and line_text.substr(i, 4) == "--[["):
+ color_map[i] = {"color": comment_color}
+ return color_map
+
+ if current_char == "\"" or current_char == "'":
+ var quote = current_char
+ var string_start = i
+ i += 1
+ while i < line_len:
+ if line_text[i] == "\\": i += 2; continue
+ if line_text[i] == quote: i += 1; break
+ i += 1
+ var string_end = i
+ color_map[string_start] = {"color": string_color}
+ if string_end < line_len:
+ color_map[string_end] = {"color": font_color}
+ continue
+
+ if current_char.is_valid_int() or (current_char == "." and i + 1 < line_len and line_text[i+1].is_valid_int()):
+ var is_hex = false
+ 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.-+")):
+ i += 1
+ else:
+ break
+ var number_end = i
+ color_map[start_col] = {"color": number_color}
+ if number_end < line_len:
+ color_map[number_end] = {"color": font_color}
+ continue
+
+ if current_char.is_valid_identifier() and not current_char.is_valid_int():
+ while i < line_len and line_text[i].is_valid_identifier():
+ i += 1
+ var word = line_text.substr(start_col, i - start_col)
+
+ var color = font_color
+ if _keywords.has(word): color = keyword_color
+ elif _gurt_globals.has(word): color = gurt_globals_color
+ elif _built_in_functions.has(word): color = function_color
+ else:
+ var prev_char_idx = start_col - 1
+ while prev_char_idx >= 0 and _is_whitespace(line_text[prev_char_idx]):
+ prev_char_idx -= 1
+ var next_char_idx = i
+ while next_char_idx < line_len and _is_whitespace(line_text[next_char_idx]):
+ next_char_idx += 1
+
+ var is_member = prev_char_idx >= 0 and line_text[prev_char_idx] in [".", ":"]
+ var is_function_call = next_char_idx < line_len and line_text[next_char_idx] == "("
+
+ if is_member and _member_keywords.has(word): color = member_color
+ elif is_function_call: color = function_color
+
+ if color != font_color: color_map[start_col] = {"color": color}
+ continue
+
+ if not _is_whitespace(current_char):
+ color_map[i] = {"color": symbol_color}
+ if i + 1 < line_len:
+ color_map[i + 1] = {"color": font_color}
+
+ i += 1
+
+ return color_map
diff --git a/flumi/Scripts/LuaSyntaxHighlighter.gd.uid b/flumi/Scripts/LuaSyntaxHighlighter.gd.uid
new file mode 100644
index 0000000..6e91b5b
--- /dev/null
+++ b/flumi/Scripts/LuaSyntaxHighlighter.gd.uid
@@ -0,0 +1 @@
+uid://qicpfnrmje1v
diff --git a/flumi/Scripts/Tab.gd b/flumi/Scripts/Tab.gd
index df7832a..7ef3ec0 100644
--- a/flumi/Scripts/Tab.gd
+++ b/flumi/Scripts/Tab.gd
@@ -35,6 +35,9 @@ 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 dev_tools: Control = null
+var dev_tools_visible: bool = false
var lua_apis: Array[LuaAPI] = []
var current_url: String = ""
var has_content: bool = false
@@ -131,6 +134,9 @@ func _exit_tree():
background_panel.get_parent().remove_child(background_panel)
background_panel.queue_free()
+ if dev_tools and is_instance_valid(dev_tools):
+ dev_tools.queue_free()
+
remove_from_group("tabs")
func init_scene(parent_container: Control) -> void:
@@ -144,9 +150,15 @@ 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.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
+
scroll_container = ScrollContainer.new()
scroll_container.name = "Tab_ScrollContainer_" + str(get_instance_id())
scroll_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
+ scroll_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Take 2/3 of space
website_container = VBoxContainer.new()
website_container.name = "Tab_WebsiteContainer_" + str(get_instance_id())
@@ -154,8 +166,15 @@ func init_scene(parent_container: Control) -> void:
website_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
website_container.add_theme_constant_override("separation", 22)
+ var dev_tools_scene = preload("res://Scenes/DevTools.tscn")
+ dev_tools = dev_tools_scene.instantiate()
+ dev_tools.name = "Tab_DevTools_" + str(get_instance_id())
+ dev_tools.visible = false
+
parent_container.call_deferred("add_child", background_panel)
- background_panel.call_deferred("add_child", scroll_container)
+ background_panel.call_deferred("add_child", main_hbox)
+ main_hbox.call_deferred("add_child", scroll_container)
+ main_hbox.call_deferred("add_child", dev_tools)
scroll_container.call_deferred("add_child", website_container)
background_panel.visible = is_active
@@ -215,3 +234,21 @@ func _on_close_button_pressed() -> void:
await close_tween.finished
tab_closed.emit()
queue_free()
+
+func toggle_dev_tools() -> void:
+ if not dev_tools:
+ return
+
+ dev_tools_visible = not dev_tools_visible
+ dev_tools.visible = dev_tools_visible
+
+ if dev_tools_visible:
+ scroll_container.size_flags_stretch_ratio = 2.0
+ dev_tools.size_flags_stretch_ratio = 1.0
+ else:
+ scroll_container.size_flags_stretch_ratio = 1.0
+
+func get_dev_tools_console() -> DevToolsConsole:
+ if dev_tools and dev_tools.has_method("get_console"):
+ return dev_tools.get_console()
+ return null
diff --git a/flumi/Scripts/Trace.gd.uid b/flumi/Scripts/Trace.gd.uid
new file mode 100644
index 0000000..bedf0a1
--- /dev/null
+++ b/flumi/Scripts/Trace.gd.uid
@@ -0,0 +1 @@
+uid://8tv4fx7krev1
diff --git a/flumi/Scripts/Utils/Lua/DOM.gd b/flumi/Scripts/Utils/Lua/DOM.gd
index 2197d70..f6324d7 100644
--- a/flumi/Scripts/Utils/Lua/DOM.gd
+++ b/flumi/Scripts/Utils/Lua/DOM.gd
@@ -272,7 +272,7 @@ static func render_new_element(element: HTMLParser.HTMLElement, parent_node: Nod
# Create the visual node for the element
var element_node = await main_scene.create_element_node(element, dom_parser)
if not element_node:
- LuaPrintUtils.lua_print_direct("Failed to create visual node for element: " + str(element))
+ Trace.trace_log("Failed to create visual node for element: " + str(element))
return
# Set metadata so ul/ol can detect dynamically added li elements
diff --git a/flumi/Scripts/Utils/Lua/Print.gd b/flumi/Scripts/Utils/Lua/Print.gd
index c4f4fed..959e82b 100644
--- a/flumi/Scripts/Utils/Lua/Print.gd
+++ b/flumi/Scripts/Utils/Lua/Print.gd
@@ -10,12 +10,9 @@ static func lua_print(vm: LuauVM) -> int:
message_parts.append(value_str)
var final_message = "\t".join(message_parts)
- lua_print_direct(final_message)
+ Trace.trace_log(final_message)
return 0
-static func lua_print_direct(msg) -> void:
- print("GURT LOG: ", msg)
-
static func lua_value_to_string(vm: LuauVM, index: int) -> String:
var lua_type = vm.lua_type(index)
diff --git a/flumi/Scripts/Utils/Lua/ThreadedVM.gd b/flumi/Scripts/Utils/Lua/ThreadedVM.gd
index cd03640..7b2fde5 100644
--- a/flumi/Scripts/Utils/Lua/ThreadedVM.gd
+++ b/flumi/Scripts/Utils/Lua/ThreadedVM.gd
@@ -3,7 +3,7 @@ extends RefCounted
signal script_completed(result: Dictionary)
signal script_error(error: String)
-signal print_output(message: String)
+signal print_output(print_data: Dictionary)
signal dom_operation_request(operation: Dictionary)
var lua_thread: Thread
@@ -238,23 +238,26 @@ func _print_handler(vm: LuauVM) -> int:
var num_args = vm.lua_gettop()
for i in range(1, num_args + 1):
- var arg_str = ""
- if vm.lua_isstring(i):
- arg_str = vm.lua_tostring(i)
- elif vm.lua_isnumber(i):
- arg_str = str(vm.lua_tonumber(i))
- elif vm.lua_isboolean(i):
- arg_str = "true" if vm.lua_toboolean(i) else "false"
- elif vm.lua_isnil(i):
- arg_str = "nil"
+ var lua_type = vm.lua_type(i)
+ if lua_type == vm.LUA_TTABLE:
+ var table_data = vm.lua_todictionary(i)
+ message_parts.append({
+ "type": "table",
+ "data": table_data
+ })
else:
- arg_str = vm.lua_typename(vm.lua_type(i))
-
- message_parts.append(arg_str)
+ var value_str = LuaPrintUtils.lua_value_to_string(vm, i)
+ message_parts.append({
+ "type": "primitive",
+ "data": value_str
+ })
- var final_message = "\t".join(message_parts)
+ var print_data = {
+ "parts": message_parts,
+ "count": message_parts.size()
+ }
- call_deferred("_emit_print_output", final_message)
+ call_deferred("_emit_print_output", print_data)
return 0
@@ -267,10 +270,35 @@ func _time_sleep_handler(vm: LuauVM) -> int:
return 0
+func _trace_log_handler(vm: LuauVM) -> int:
+ var message = vm.luaL_checkstring(1)
+ call_deferred("_emit_trace_message", message, "lua")
+ return 0
+
+func _trace_warning_handler(vm: LuauVM) -> int:
+ var message = vm.luaL_checkstring(1)
+ call_deferred("_emit_trace_message", message, "warning")
+ return 0
+
+func _trace_error_handler(vm: LuauVM) -> int:
+ var message = vm.luaL_checkstring(1)
+ call_deferred("_emit_trace_message", message, "error")
+ return 0
+
func _setup_threaded_gurt_api():
lua_vm.lua_pushcallable(_print_handler, "print")
lua_vm.lua_setglobal("print")
+ # Setup trace functions
+ lua_vm.lua_pushcallable(_trace_log_handler, "_trace_log")
+ lua_vm.lua_setglobal("_trace_log")
+
+ lua_vm.lua_pushcallable(_trace_warning_handler, "_trace_warning")
+ lua_vm.lua_setglobal("_trace_warning")
+
+ lua_vm.lua_pushcallable(_trace_error_handler, "_trace_error")
+ lua_vm.lua_setglobal("_trace_error")
+
LuaTimeUtils.setup_time_api(lua_vm)
lua_vm.lua_getglobal("Time")
@@ -281,9 +309,6 @@ func _setup_threaded_gurt_api():
lua_vm.lua_newtable()
- lua_vm.lua_pushcallable(_print_handler, "gurt.log")
- lua_vm.lua_setfield(-2, "log")
-
lua_vm.lua_pushcallable(_gurt_select_handler, "gurt.select")
lua_vm.lua_setfield(-2, "select")
@@ -357,6 +382,7 @@ func _setup_additional_lua_apis():
LuaCrumbsUtils.setup_crumbs_api(lua_vm)
LuaRegexUtils.setup_regex_api(lua_vm)
LuaURLUtils.setup_url_api(lua_vm)
+ Trace.setup_trace_api(lua_vm)
func _table_tostring_handler(vm: LuauVM) -> int:
vm.luaL_checktype(1, vm.LUA_TTABLE)
@@ -370,8 +396,8 @@ func _emit_script_completed(result: Dictionary):
func _emit_script_error(error: String):
script_error.emit(error)
-func _emit_print_output(message: String):
- print_output.emit(message)
+func _emit_print_output(print_data: Dictionary):
+ print_output.emit(print_data)
func _gurt_select_all_handler(vm: LuauVM) -> int:
var selector: String = vm.luaL_checkstring(1)
@@ -567,3 +593,12 @@ func _create_threaded_interval(interval_id: int, delay_ms: int):
timeout_info.timer = timer
lua_api.add_child(timer)
timer.start()
+
+func _emit_trace_message(message: String, level: String):
+ match level:
+ "lua", "log":
+ Trace.trace_log(message)
+ "warning":
+ Trace.trace_warning(message)
+ "error":
+ Trace.trace_error(message)
diff --git a/flumi/Scripts/Utils/Lua/Trace.gd b/flumi/Scripts/Utils/Lua/Trace.gd
new file mode 100644
index 0000000..f00c0cf
--- /dev/null
+++ b/flumi/Scripts/Utils/Lua/Trace.gd
@@ -0,0 +1,87 @@
+class_name Trace
+extends RefCounted
+
+signal log_message(message: String, level: String, timestamp: float)
+
+enum LogLevel {
+ LOG,
+ WARNING,
+ ERROR
+}
+
+static var _instance: Trace
+static var _messages: Array[Dictionary] = []
+
+static func get_instance() -> Trace:
+ if not _instance:
+ _instance = Trace.new()
+ return _instance
+
+static func trace_log(message: String) -> void:
+ _emit_message(message, "log")
+
+static func trace_warning(message: String) -> void:
+ _emit_message(message, "warning")
+
+static func trace_error(message: String) -> void:
+ _emit_message(message, "error")
+
+static func _emit_message(message: String, level: String) -> void:
+ var timestamp = Time.get_ticks_msec() / 1000.0
+ var log_entry = {
+ "message": message,
+ "level": level,
+ "timestamp": timestamp
+ }
+
+ _messages.append(log_entry)
+ get_instance().call_deferred("emit_signal", "log_message", message, level, timestamp)
+
+ match level:
+ "log":
+ print("TRACE LOG: ", message)
+ "warning":
+ print("TRACE WARNING: ", message)
+ "error":
+ print("TRACE ERROR: ", message)
+
+static func get_all_messages() -> Array[Dictionary]:
+ return _messages.duplicate()
+
+static func clear_messages() -> void:
+ _messages.clear()
+
+static func _lua_trace_log_handler(vm: LuauVM) -> int:
+ var message = vm.luaL_checkstring(1)
+ vm.lua_getglobal("_trace_log")
+ vm.lua_pushstring(message)
+ vm.lua_call(1, 0)
+ return 0
+
+static func _lua_trace_warn_handler(vm: LuauVM) -> int:
+ var message = vm.luaL_checkstring(1)
+ vm.lua_getglobal("_trace_warning")
+ vm.lua_pushstring(message)
+ vm.lua_call(1, 0)
+ return 0
+
+static func _lua_trace_error_handler(vm: LuauVM) -> int:
+ var message = vm.luaL_checkstring(1)
+ vm.lua_getglobal("_trace_error")
+ vm.lua_pushstring(message)
+ vm.lua_call(1, 0)
+ return 0
+
+static func setup_trace_api(vm: LuauVM) -> void:
+ vm.lua_newtable()
+
+ vm.lua_pushcallable(_lua_trace_log_handler, "trace.log")
+ vm.lua_setfield(-2, "log")
+
+ vm.lua_pushcallable(_lua_trace_warn_handler, "trace.warn")
+ vm.lua_setfield(-2, "warn")
+
+ vm.lua_pushcallable(_lua_trace_error_handler, "trace.error")
+ vm.lua_setfield(-2, "error")
+
+ vm.lua_setglobal("trace")
diff --git a/flumi/Scripts/Utils/Lua/Trace.gd.uid b/flumi/Scripts/Utils/Lua/Trace.gd.uid
new file mode 100644
index 0000000..c275c34
--- /dev/null
+++ b/flumi/Scripts/Utils/Lua/Trace.gd.uid
@@ -0,0 +1 @@
+uid://k87sfbjp7myx
diff --git a/flumi/Scripts/main.gd b/flumi/Scripts/main.gd
index 532b2f3..1e07ef1 100644
--- a/flumi/Scripts/main.gd
+++ b/flumi/Scripts/main.gd
@@ -61,6 +61,16 @@ func _ready():
call_deferred("render")
+func _input(event: InputEvent) -> void:
+ if Input.is_action_just_pressed("DevTools"):
+ _toggle_dev_tools()
+ get_viewport().set_input_as_handled()
+
+func _toggle_dev_tools() -> void:
+ var active_tab = get_active_tab()
+ if active_tab:
+ active_tab.toggle_dev_tools()
+
func resolve_url(href: String) -> String:
return URLUtils.resolve_url(current_domain, href)
@@ -280,12 +290,16 @@ func render_content(html_bytes: PackedByteArray) -> void:
parser.register_dom_node(body, target_container)
var scripts = parser.find_all("script")
- var lua_api = null
- if scripts.size() > 0:
- lua_api = LuaAPI.new()
- add_child(lua_api)
- if active_tab:
- active_tab.lua_apis.append(lua_api)
+
+ var lua_api = LuaAPI.new()
+ add_child(lua_api)
+ if active_tab:
+ active_tab.lua_apis.append(lua_api)
+
+ lua_api.dom_parser = parser
+
+ if lua_api.threaded_vm:
+ lua_api.threaded_vm.dom_parser = parser
var i = 0
if body:
@@ -694,3 +708,9 @@ func get_active_website_container() -> Control:
if active_tab:
return active_tab.website_container
return website_container # fallback to original container
+
+func get_dev_tools_console() -> DevToolsConsole:
+ var active_tab = get_active_tab()
+ if active_tab:
+ return active_tab.get_dev_tools_console()
+ return null
diff --git a/flumi/project.godot b/flumi/project.godot
index df340cc..97c3a98 100644
--- a/flumi/project.godot
+++ b/flumi/project.godot
@@ -72,3 +72,8 @@ FocusSearch={
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":76,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
+DevTools={
+"deadzone": 0.2,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":73,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
diff --git a/tests/add-remove-child.html b/tests/add-remove-child.html
index 0f340a8..ddd22e4 100644
--- a/tests/add-remove-child.html
+++ b/tests/add-remove-child.html
@@ -19,7 +19,7 @@
local list = gurt.select('#item-list')
local counter = 1
- gurt.log('List manipulation script started.')
+ trace.log('List manipulation script started.')
add_button:on('click', function()
local new_item = gurt.create('li', {
diff --git a/tests/attribute.html b/tests/attribute.html
index b59ba0c..57da9c2 100644
--- a/tests/attribute.html
+++ b/tests/attribute.html
@@ -32,7 +32,7 @@
local infoBox = gurt.select('#info-box')
local clickCounter = gurt.select('#click-counter')
- gurt.log('Button attribute demo script started.')
+ trace.log('Button attribute demo script started.')
local clickCount = 0
@@ -64,7 +64,7 @@
targetButton:on('click', function()
clickCount = clickCount + 1
clickCounter.text = 'Button clicked ' .. clickCount .. ' times!'
- gurt.log('Target button clicked! Count:', clickCount)
+ trace.log('Target button clicked! Count:', clickCount)
updateStatus()
end)
@@ -72,7 +72,7 @@
enableBtn:on('click', function()
targetButton:setAttribute('disabled', '') -- Remove disabled attribute
targetButton:setAttribute('data-value', 'enabled')
- gurt.log('Target button enabled via setAttribute')
+ trace.log('Target button enabled via setAttribute')
updateStatus()
end)
@@ -80,7 +80,7 @@
disableBtn:on('click', function()
targetButton:setAttribute('disabled', 'true')
targetButton:setAttribute('data-value', 'disabled')
- gurt.log('Target button disabled via setAttribute')
+ trace.log('Target button disabled via setAttribute')
updateStatus()
end)
@@ -92,12 +92,12 @@
-- Currently disabled, so enable it
targetButton:setAttribute('disabled', '')
targetButton:setAttribute('data-value', 'toggled-enabled')
- gurt.log('Target button toggled to enabled state')
+ trace.log('Target button toggled to enabled state')
else
-- Currently enabled, so disable it
targetButton:setAttribute('disabled', 'true')
targetButton:setAttribute('data-value', 'toggled-disabled')
- gurt.log('Target button toggled to disabled state')
+ trace.log('Target button toggled to disabled state')
end
updateStatus()
@@ -109,12 +109,12 @@
local type = targetButton:getAttribute('type')
local dataValue = targetButton:getAttribute('data-value')
- gurt.log('=== BUTTON STATUS CHECK ===')
- gurt.log('Disabled attribute:', disabled or 'not set')
- gurt.log('Type attribute:', type or 'not set')
- gurt.log('Data-value attribute:', dataValue or 'not set')
- gurt.log('Click count:', clickCount)
- gurt.log('===========================')
+ trace.log('=== BUTTON STATUS CHECK ===')
+ trace.log('Disabled attribute:', disabled or 'not set')
+ trace.log('Type attribute:', type or 'not set')
+ trace.log('Data-value attribute:', dataValue or 'not set')
+ trace.log('Click count:', clickCount)
+ trace.log('===========================')
-- Demonstrate style setAttribute
local randomColors = {'bg-red-500', 'bg-green-500', 'bg-purple-500', 'bg-orange-500', 'bg-pink-500'}
diff --git a/tests/canvas.html b/tests/canvas.html
index f6f906e..924ad96 100644
--- a/tests/canvas.html
+++ b/tests/canvas.html
@@ -18,7 +18,7 @@
diff --git a/tests/clipboard.html b/tests/clipboard.html
index ff46aad..9383a0f 100644
--- a/tests/clipboard.html
+++ b/tests/clipboard.html
@@ -17,12 +17,12 @@
diff --git a/tests/interval-and-network.html b/tests/interval-and-network.html
index 5735c67..535e3a1 100644
--- a/tests/interval-and-network.html
+++ b/tests/interval-and-network.html
@@ -26,7 +26,7 @@
local loadImageBtn = gurt.select('#load-image-btn')
local imageContainer = gurt.select('#image-container')
- gurt.log('setInterval & Network Image demo script started.')
+ trace.log('setInterval & Network Image demo script started.')
local logMessages = {}
local counter = 0
diff --git a/tests/lua-api.html b/tests/lua-api.html
index 687afea..9633505 100644
--- a/tests/lua-api.html
+++ b/tests/lua-api.html
@@ -17,7 +17,7 @@
local mouse = gurt.select('#mouse')
local btnmouse = gurt.select('#btnmouse')
- gurt.log('Starting Lua script execution...')
+ trace.log('Starting Lua script execution...')
gurt.body:on('keypress', function(el)
typing.text = table.tostring(el)
@@ -68,13 +68,13 @@
subscription:unsubscribe()
end)
- gurt.log('Event listener attached to button with subscription ID')
+ trace.log('Event listener attached to button with subscription ID')
else
- gurt.log('Could not find button or event log element')
+ trace.log('Could not find button or event log element')
end
-- DOM Manipulation Demo
- gurt.log('Testing DOM manipulation...')
+ trace.log('Testing DOM manipulation...')
-- Create a new div with styling
local new_div = gurt.create('div', { style = 'bg-red-500 p-4 rounded-lg mb-4' })
diff --git a/tests/network-and-json.html b/tests/network-and-json.html
index 2abc129..62e8d41 100644
--- a/tests/network-and-json.html
+++ b/tests/network-and-json.html
@@ -28,7 +28,7 @@
local urlInput = gurt.select('#url-input')
local jsonInput = gurt.select('#json-input')
- gurt.log('Network & JSON API demo script started.')
+ trace.log('Network & JSON API demo script started.')
local logMessages = {}
diff --git a/tests/signal.html b/tests/signal.html
index 22c8e38..148e799 100644
--- a/tests/signal.html
+++ b/tests/signal.html
@@ -38,7 +38,7 @@
local fireDataBtn = gurt.select('#fire-data-btn')
local clearLogBtn = gurt.select('#clear-log-btn')
- gurt.log('Signal API demo script started.')
+ trace.log('Signal API demo script started.')
local logMessages = {}
local connectionCount = 0
diff --git a/tests/snake.html b/tests/snake.html
index d55e96c..1bb15ec 100644
--- a/tests/snake.html
+++ b/tests/snake.html
@@ -201,7 +201,7 @@
math.randomseed(Time.now())
render()
- gurt.log('Snake game initialized!')
+ trace.log('Snake game initialized!')
diff --git a/tests/tween.html b/tests/tween.html
index bfe9ccf..9fdd23c 100644
--- a/tests/tween.html
+++ b/tests/tween.html
@@ -53,7 +53,7 @@
local moveBox = gurt.select('#move-box')
local comboBox = gurt.select('#combo-box')
- gurt.log('Tween animation demo started!')
+ trace.log('Tween animation demo started!')
-- Fade Animation
gurt.select('#fade-btn'):on('click', function()
@@ -143,7 +143,7 @@
-- Callback example
gurt.select('#callback-btn'):on('click', function()
fadeBox:createTween():to('opacity', 0.3):duration(1.0):easing('inout'):callback(function()
- gurt.log('Fade animation completed!')
+ trace.log('Fade animation completed!')
end):play()
end)
diff --git a/tests/websocket.html b/tests/websocket.html
index 0244340..08983d3 100644
--- a/tests/websocket.html
+++ b/tests/websocket.html
@@ -33,7 +33,7 @@
local urlInput = gurt.select('#url-input')
local messageInput = gurt.select('#message-input')
- gurt.log('WebSocket API demo script started.')
+ trace.log('WebSocket API demo script started.')
local logMessages = {}
local socket = nil