From ae8954c52c99bfbd29b367a6c59294de9f4ad72b Mon Sep 17 00:00:00 2001
From: Face <69168154+face-hh@users.noreply.github.com>
Date: Sat, 16 Aug 2025 13:57:14 +0300
Subject: [PATCH] external resource loading, window.location, tab icon fix
---
dns/Cargo.lock | 4 +-
dns/frontend/dashboard.html | 10 ++
dns/frontend/index.html | 51 +------
dns/frontend/script.lua | 52 +++++++
dns/frontend/style.css | 0
dns/migrations/001_initial.sql | 44 ++++--
dns/migrations/002_remove_email.sql | 3 -
dns/migrations/003_fix_timestamp_types.sql | 4 -
.../004_add_domain_invite_codes.sql | 14 --
dns/migrations/005_add_dns_records.sql | 23 ----
dns/src/discord_bot.rs | 1 -
dns/src/http.rs | 3 +-
dns/src/http/models.rs | 1 -
dns/src/http/routes.rs | 2 +
flumi/Scripts/B9/HTMLParser.gd | 40 +++++-
flumi/Scripts/B9/Lua.gd | 29 ++++
flumi/Scripts/Network.gd | 57 ++++++++
flumi/Scripts/Tab.gd | 47 +++----
flumi/Scripts/Utils/Lua/ThreadedVM.gd | 129 +++++++++---------
flumi/Scripts/Utils/URLUtils.gd | 62 +++++++++
flumi/Scripts/Utils/URLUtils.gd.uid | 1 +
flumi/Scripts/main.gd | 83 +++++------
22 files changed, 413 insertions(+), 247 deletions(-)
create mode 100644 dns/frontend/dashboard.html
create mode 100644 dns/frontend/script.lua
delete mode 100644 dns/frontend/style.css
delete mode 100644 dns/migrations/002_remove_email.sql
delete mode 100644 dns/migrations/003_fix_timestamp_types.sql
delete mode 100644 dns/migrations/004_add_domain_invite_codes.sql
delete mode 100644 dns/migrations/005_add_dns_records.sql
create mode 100644 flumi/Scripts/Utils/URLUtils.gd
create mode 100644 flumi/Scripts/Utils/URLUtils.gd.uid
diff --git a/dns/Cargo.lock b/dns/Cargo.lock
index 92660ec..512d1d5 100644
--- a/dns/Cargo.lock
+++ b/dns/Cargo.lock
@@ -1560,9 +1560,9 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
name = "macros-rs"
-version = "1.2.1"
+version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb8136cf4bdbfd10cdf683dab195f53892cb3b6433397ef48b211556b9c49dcb"
+checksum = "9cfca1250b52a785fbe49de29612471f59592b6b659159dcfcb976af08c803b4"
dependencies = [
"termcolor",
]
diff --git a/dns/frontend/dashboard.html b/dns/frontend/dashboard.html
new file mode 100644
index 0000000..2e75510
--- /dev/null
+++ b/dns/frontend/dashboard.html
@@ -0,0 +1,10 @@
+
+ Login
+
+
+
+
+
+
+ todo
+
\ No newline at end of file
diff --git a/dns/frontend/index.html b/dns/frontend/index.html
index 812d7f8..eadc2f2 100644
--- a/dns/frontend/index.html
+++ b/dns/frontend/index.html
@@ -38,56 +38,7 @@
#log-output { text-white p-4 rounded-md mt-4 font-mono max-h-40 }
-
+
diff --git a/dns/frontend/script.lua b/dns/frontend/script.lua
new file mode 100644
index 0000000..b9f4bdd
--- /dev/null
+++ b/dns/frontend/script.lua
@@ -0,0 +1,52 @@
+local submitBtn = gurt.select('#submit')
+local username_input = gurt.select('#username')
+local password_input = gurt.select('#password')
+local log_output = gurt.select('#log-output')
+
+function addLog(message)
+ gurt.log(message)
+ log_output.text = log_output.text .. message .. '\\n'
+end
+
+print(gurt.location.href)
+submitBtn:on('submit', function(event)
+ local username = event.data.username
+ local password = event.data.password
+
+ local request_body = JSON.stringify({
+ username = username,
+ password = password
+ })
+ print(request_body)
+ local url = 'http://localhost:8080/auth/login'
+ local headers = {
+ ['Content-Type'] = 'application/json'
+ }
+
+ addLog('Attempting to log in with username: ' .. username)
+ log_output.text = ''
+
+ local response = fetch(url, {
+ method = 'POST',
+ headers = headers,
+ body = request_body
+ })
+
+ addLog('Response Status: ' .. response.status .. ' ' .. response.statusText)
+
+ if response:ok() then
+ addLog('Login successful!')
+ local jsonData = response:json()
+ if jsonData then
+ addLog('Logged in as user: ' .. jsonData.user.username)
+ addLog('Token: ' .. jsonData.token:sub(1, 20) .. '...')
+
+ -- TODO: store as cookie
+ gurt.location.goto("/dashboard.html")
+ end
+ else
+ addLog('Request failed with status: ' .. response.status)
+ local error_data = response:text()
+ addLog('Error response: ' .. error_data)
+ end
+end)
\ No newline at end of file
diff --git a/dns/frontend/style.css b/dns/frontend/style.css
deleted file mode 100644
index e69de29..0000000
diff --git a/dns/migrations/001_initial.sql b/dns/migrations/001_initial.sql
index d895b98..4508380 100644
--- a/dns/migrations/001_initial.sql
+++ b/dns/migrations/001_initial.sql
@@ -1,21 +1,38 @@
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
- email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
registrations_remaining INTEGER DEFAULT 3,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ domain_invite_codes INTEGER DEFAULT 3,
+ created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
+CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
+
CREATE TABLE IF NOT EXISTS invite_codes (
id SERIAL PRIMARY KEY,
code VARCHAR(32) UNIQUE NOT NULL,
created_by INTEGER REFERENCES users(id),
used_by INTEGER REFERENCES users(id),
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- used_at TIMESTAMP
+ created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
+ used_at TIMESTAMPTZ
);
+CREATE INDEX IF NOT EXISTS idx_invite_codes_code ON invite_codes(code);
+
+CREATE TABLE IF NOT EXISTS domain_invite_codes (
+ id SERIAL PRIMARY KEY,
+ code VARCHAR(32) UNIQUE NOT NULL,
+ created_by INTEGER REFERENCES users(id),
+ used_by INTEGER REFERENCES users(id),
+ created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
+ used_at TIMESTAMPTZ
+);
+
+CREATE INDEX IF NOT EXISTS idx_domain_invite_codes_code ON domain_invite_codes(code);
+CREATE INDEX IF NOT EXISTS idx_domain_invite_codes_created_by ON domain_invite_codes(created_by);
+CREATE INDEX IF NOT EXISTS idx_domain_invite_codes_used_by ON domain_invite_codes(used_by);
+
CREATE TABLE IF NOT EXISTS domains (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
@@ -24,13 +41,24 @@ CREATE TABLE IF NOT EXISTS domains (
user_id INTEGER REFERENCES users(id),
status VARCHAR(20) DEFAULT 'pending',
denial_reason TEXT,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE(name, tld)
);
CREATE INDEX IF NOT EXISTS idx_domains_name_tld ON domains(name, tld);
CREATE INDEX IF NOT EXISTS idx_domains_user_id ON domains(user_id);
CREATE INDEX IF NOT EXISTS idx_domains_status ON domains(status);
-CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
-CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
-CREATE INDEX IF NOT EXISTS idx_invite_codes_code ON invite_codes(code);
\ No newline at end of file
+
+CREATE TABLE IF NOT EXISTS dns_records (
+ id SERIAL PRIMARY KEY,
+ domain_id INTEGER NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
+ record_type VARCHAR(10) NOT NULL CHECK (record_type IN ('A', 'AAAA', 'CNAME', 'TXT', 'MX', 'NS', 'SRV')),
+ name VARCHAR(255) NOT NULL DEFAULT '@', -- @ for root, or subdomain name
+ value VARCHAR(1000) NOT NULL,
+ ttl INTEGER DEFAULT 3600,
+ priority INTEGER, -- For MX records
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE INDEX IF NOT EXISTS idx_dns_records_domain_type ON dns_records(domain_id, record_type);
+CREATE INDEX IF NOT EXISTS idx_dns_records_name ON dns_records(name);
\ No newline at end of file
diff --git a/dns/migrations/002_remove_email.sql b/dns/migrations/002_remove_email.sql
deleted file mode 100644
index b325bf9..0000000
--- a/dns/migrations/002_remove_email.sql
+++ /dev/null
@@ -1,3 +0,0 @@
-ALTER TABLE users DROP COLUMN IF EXISTS email;
-
-DROP INDEX IF EXISTS idx_users_email;
\ No newline at end of file
diff --git a/dns/migrations/003_fix_timestamp_types.sql b/dns/migrations/003_fix_timestamp_types.sql
deleted file mode 100644
index 44b23b8..0000000
--- a/dns/migrations/003_fix_timestamp_types.sql
+++ /dev/null
@@ -1,4 +0,0 @@
-ALTER TABLE users ALTER COLUMN created_at TYPE TIMESTAMPTZ;
-ALTER TABLE invite_codes ALTER COLUMN created_at TYPE TIMESTAMPTZ;
-ALTER TABLE invite_codes ALTER COLUMN used_at TYPE TIMESTAMPTZ;
-ALTER TABLE domains ALTER COLUMN created_at TYPE TIMESTAMPTZ;
\ No newline at end of file
diff --git a/dns/migrations/004_add_domain_invite_codes.sql b/dns/migrations/004_add_domain_invite_codes.sql
deleted file mode 100644
index da0af0f..0000000
--- a/dns/migrations/004_add_domain_invite_codes.sql
+++ /dev/null
@@ -1,14 +0,0 @@
-ALTER TABLE users ADD COLUMN domain_invite_codes INTEGER DEFAULT 3;
-
-CREATE TABLE IF NOT EXISTS domain_invite_codes (
- id SERIAL PRIMARY KEY,
- code VARCHAR(32) UNIQUE NOT NULL,
- created_by INTEGER REFERENCES users(id),
- used_by INTEGER REFERENCES users(id),
- created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
- used_at TIMESTAMPTZ
-);
-
-CREATE INDEX IF NOT EXISTS idx_domain_invite_codes_code ON domain_invite_codes(code);
-CREATE INDEX IF NOT EXISTS idx_domain_invite_codes_created_by ON domain_invite_codes(created_by);
-CREATE INDEX IF NOT EXISTS idx_domain_invite_codes_used_by ON domain_invite_codes(used_by);
\ No newline at end of file
diff --git a/dns/migrations/005_add_dns_records.sql b/dns/migrations/005_add_dns_records.sql
deleted file mode 100644
index d37a381..0000000
--- a/dns/migrations/005_add_dns_records.sql
+++ /dev/null
@@ -1,23 +0,0 @@
-CREATE TABLE dns_records (
- id SERIAL PRIMARY KEY,
- domain_id INTEGER NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
- record_type VARCHAR(10) NOT NULL CHECK (record_type IN ('A', 'AAAA', 'CNAME', 'TXT', 'MX', 'NS', 'SRV')),
- name VARCHAR(255) NOT NULL DEFAULT '@', -- @ for root, or subdomain name
- value VARCHAR(1000) NOT NULL,
- ttl INTEGER DEFAULT 3600,
- priority INTEGER, -- For MX records
- created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
-);
-
-CREATE INDEX idx_dns_records_domain_type ON dns_records(domain_id, record_type);
-CREATE INDEX idx_dns_records_name ON dns_records(name);
-
-INSERT INTO dns_records (domain_id, record_type, name, value, ttl)
-SELECT id, 'A', '@', ip, 3600
-FROM domains
-WHERE status = 'approved';
-
-INSERT INTO dns_records (domain_id, record_type, name, value, ttl, priority)
-SELECT id, 'SRV', '_gurt._tcp', '0 5 4878 @', 3600, 0
-FROM domains
-WHERE status = 'approved';
\ No newline at end of file
diff --git a/dns/src/discord_bot.rs b/dns/src/discord_bot.rs
index ee8f32d..06e2454 100644
--- a/dns/src/discord_bot.rs
+++ b/dns/src/discord_bot.rs
@@ -1,6 +1,5 @@
use serenity::async_trait;
use serenity::all::*;
-use serenity::prelude::*;
use sqlx::PgPool;
pub struct DiscordBot {
diff --git a/dns/src/http.rs b/dns/src/http.rs
index 7e14bd7..d0f4bdc 100644
--- a/dns/src/http.rs
+++ b/dns/src/http.rs
@@ -6,9 +6,8 @@ mod routes;
use crate::{auth::jwt_middleware, config::Config, discord_bot};
use actix_governor::{Governor, GovernorConfigBuilder};
-use actix_web::{http::Method, web, web::Data, App, HttpRequest, HttpServer};
+use actix_web::{http::Method, web, web::Data, App, HttpServer};
use actix_web_httpauth::middleware::HttpAuthentication;
-use anyhow::{anyhow, Error};
use colored::Colorize;
use macros_rs::fmt::{crashln, string};
use ratelimit::RealIpKeyExtractor;
diff --git a/dns/src/http/models.rs b/dns/src/http/models.rs
index ff8347b..05b0c0b 100644
--- a/dns/src/http/models.rs
+++ b/dns/src/http/models.rs
@@ -1,7 +1,6 @@
use super::helpers::deserialize_lowercase;
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, types::chrono::{DateTime, Utc}};
-use chrono;
#[derive(Clone, Debug, Deserialize, Serialize, FromRow)]
pub struct Domain {
diff --git a/dns/src/http/routes.rs b/dns/src/http/routes.rs
index 80219d2..3cc8c1a 100644
--- a/dns/src/http/routes.rs
+++ b/dns/src/http/routes.rs
@@ -242,6 +242,7 @@ pub(crate) async fn get_domain(path: web::Path<(String, String)>, app: Data HttpResponse::NotFound().finish(),
Err(_) => HttpResponse::InternalServerError().finish(),
@@ -378,6 +379,7 @@ pub(crate) async fn get_domains(query: web::Query, app: Data void:
# Check if it's an external stylesheet
var src = style_element.get_attribute("src")
if src.length() > 0:
- # TODO: Handle external CSS loading when Network module is available
+ if not parse_result.external_css:
+ parse_result.external_css = []
+ parse_result.external_css.append(src)
return
# Handle inline CSS - we'll get the text content when parsing is complete
@@ -128,6 +132,24 @@ func process_styles() -> void:
parse_result.css_parser.css_text = css_content
parse_result.css_parser.parse()
+func process_external_styles(base_url: String = "") -> void:
+ if not parse_result.external_css or parse_result.external_css.is_empty():
+ return
+
+ if not parse_result.css_parser:
+ parse_result.css_parser = CSSParser.new()
+ parse_result.css_parser.init()
+
+ var combined_css = parse_result.css_parser.css_text if parse_result.css_parser.css_text else Constants.DEFAULT_CSS
+
+ for css_url in parse_result.external_css:
+ var css_content = await Network.fetch_external_resource(css_url, base_url)
+ if not css_content.is_empty():
+ combined_css += "\n" + css_content
+
+ parse_result.css_parser.css_text = combined_css
+ parse_result.css_parser.parse()
+
func get_element_styles_with_inheritance(element: HTMLElement, event: String = "", visited_elements: Array = []) -> Dictionary:
if !parse_result.css_parser:
return {}
@@ -362,11 +384,23 @@ func process_scripts(lua_api: LuaAPI, lua_vm) -> void:
var inline_code = script_element.text_content.strip_edges()
if not src.is_empty():
- # TODO: add support for external Lua script
- pass
+ if not parse_result.external_scripts:
+ parse_result.external_scripts = []
+ parse_result.external_scripts.append(src)
elif not inline_code.is_empty():
lua_api.execute_lua_script(inline_code, lua_vm)
+func process_external_scripts(lua_api: LuaAPI, lua_vm, base_url: String = "") -> void:
+ if not lua_api or not parse_result.external_scripts or parse_result.external_scripts.is_empty():
+ return
+
+ lua_api.dom_parser = self
+
+ for script_url in parse_result.external_scripts:
+ var script_content = await Network.fetch_external_resource(script_url, base_url)
+ if not script_content.is_empty():
+ lua_api.execute_lua_script(script_content, lua_vm)
+
func get_all_stylesheets() -> Array[String]:
return get_attribute_values("style", "src")
diff --git a/flumi/Scripts/B9/Lua.gd b/flumi/Scripts/B9/Lua.gd
index 5f786d2..21d6064 100644
--- a/flumi/Scripts/B9/Lua.gd
+++ b/flumi/Scripts/B9/Lua.gd
@@ -153,6 +153,35 @@ func _gurt_clear_interval_handler(vm: LuauVM) -> int:
_ensure_timeout_manager()
return timeout_manager.clear_interval_handler(vm)
+# Location API handlers
+func _gurt_location_reload_handler(vm: LuauVM) -> int:
+ call_deferred("_reload_current_page")
+ return 0
+
+func _gurt_location_goto_handler(vm: LuauVM) -> int:
+ var url: String = vm.luaL_checkstring(1)
+ call_deferred("_navigate_to_url", url)
+ return 0
+
+func _gurt_location_get_href_handler(vm: LuauVM) -> int:
+ var main_node = Engine.get_main_loop().current_scene
+ if main_node and main_node.has_method("get_current_url"):
+ var current_url = main_node.get_current_url()
+ vm.lua_pushstring(current_url)
+ else:
+ vm.lua_pushstring("")
+ return 1
+
+func _reload_current_page():
+ var main_node = Engine.get_main_loop().current_scene
+ if main_node and main_node.has_method("reload_current_page"):
+ main_node.reload_current_page()
+
+func _navigate_to_url(url: String):
+ var main_node = Engine.get_main_loop().current_scene
+ if main_node and main_node.has_method("navigate_to_url"):
+ main_node.navigate_to_url(url)
+
# Event system handlers
func _element_on_event_handler(vm: LuauVM) -> int:
vm.luaL_checktype(1, vm.LUA_TTABLE)
diff --git a/flumi/Scripts/Network.gd b/flumi/Scripts/Network.gd
index deecf10..b0105a5 100644
--- a/flumi/Scripts/Network.gd
+++ b/flumi/Scripts/Network.gd
@@ -65,3 +65,60 @@ func fetch_image(url: String) -> ImageTexture:
var texture = ImageTexture.create_from_image(image)
return texture
+
+func fetch_text(url: String) -> String:
+ var http_request = HTTPRequest.new()
+ add_child(http_request)
+
+ if url.is_empty():
+ http_request.queue_free()
+ return ""
+
+ var request_headers = PackedStringArray()
+ request_headers.append("User-Agent: " + UserAgent.get_user_agent())
+
+ var error = http_request.request(url, request_headers)
+ if error != OK:
+ print("Error making HTTP request for text resource: ", url, " Error: ", error)
+ http_request.queue_free()
+ return ""
+
+ var response = await http_request.request_completed
+
+ 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()
+
+ if result != HTTPRequest.RESULT_SUCCESS or response_code != 200:
+ print("Failed to fetch text resource. URL: ", url, " Result: ", result, " Response code: ", response_code)
+ return ""
+
+ return body.get_string_from_utf8()
+
+func fetch_external_resource(url: String, base_url: String = "") -> String:
+ var resolved_url = URLUtils.resolve_url(base_url, url)
+
+ if resolved_url.begins_with("http://") or resolved_url.begins_with("https://"):
+ return await fetch_text(resolved_url)
+ elif resolved_url.begins_with("gurt://"):
+ return await fetch_gurt_resource(resolved_url)
+ else:
+ return ""
+
+func fetch_gurt_resource(url: String) -> String:
+ if not GurtProtocol.is_gurt_domain(url):
+ return ""
+
+ var result = await GurtProtocol.handle_gurt_domain(url)
+
+ if result.has("error"):
+ print("GURT resource error: ", result.error)
+ return ""
+
+ if result.has("html"):
+ return result.html.get_string_from_utf8()
+
+ return ""
diff --git a/flumi/Scripts/Tab.gd b/flumi/Scripts/Tab.gd
index 62181fb..b422344 100644
--- a/flumi/Scripts/Tab.gd
+++ b/flumi/Scripts/Tab.gd
@@ -48,31 +48,18 @@ func set_icon(new_icon: Texture) -> void:
func update_icon_from_url(icon_url: String) -> void:
if icon_url.is_empty():
- stop_loading()
+ const GLOBE_ICON = preload("res://Assets/Icons/globe.svg")
+ set_icon(GLOBE_ICON)
return
- const LOADER_CIRCLE = preload("res://Assets/Icons/loader-circle.svg")
-
- loading_tween = create_tween()
-
- set_icon(LOADER_CIRCLE)
-
- loading_tween.set_loops()
-
- icon.pivot_offset = Vector2(11.5, 11.5)
- loading_tween.tween_method(func(angle):
- if !is_instance_valid(icon):
- if loading_tween: loading_tween.kill()
- return
- icon.rotation = angle
- , 0.0, TAU, 1.0)
-
+ # Load the icon in the background
var icon_resource = await Network.fetch_image(icon_url)
-
- # Only update if tab still exists
- if is_instance_valid(self):
+
+ if is_instance_valid(self) and icon_resource:
set_icon(icon_resource)
- stop_loading()
+ elif is_instance_valid(self):
+ const GLOBE_ICON = preload("res://Assets/Icons/globe.svg")
+ set_icon(GLOBE_ICON)
func _on_button_mouse_entered() -> void:
mouse_over_tab = true
@@ -89,16 +76,18 @@ func start_loading() -> void:
stop_loading()
- loading_tween = create_tween()
set_icon(LOADER_CIRCLE)
- loading_tween.set_loops()
icon.pivot_offset = Vector2(11.5, 11.5)
- loading_tween.tween_method(func(angle):
- if !is_instance_valid(icon):
- if loading_tween: loading_tween.kill()
- return
- icon.rotation = angle
- , 0.0, TAU, 1.0)
+
+ loading_tween = create_tween()
+ if loading_tween:
+ loading_tween.set_loops(0)
+ loading_tween.tween_method(func(angle):
+ if !is_instance_valid(icon):
+ if loading_tween: loading_tween.kill()
+ return
+ icon.rotation = angle
+ , 0.0, TAU, 1.0)
func stop_loading() -> void:
if loading_tween:
diff --git a/flumi/Scripts/Utils/Lua/ThreadedVM.gd b/flumi/Scripts/Utils/Lua/ThreadedVM.gd
index a39c038..36b4efb 100644
--- a/flumi/Scripts/Utils/Lua/ThreadedVM.gd
+++ b/flumi/Scripts/Utils/Lua/ThreadedVM.gd
@@ -113,12 +113,12 @@ func _lua_thread_worker():
lua_vm.LUA_COROUTINE_LIB, lua_vm.LUA_MATH_LIB, lua_vm.LUA_UTF8_LIB,
lua_vm.LUA_TABLE_LIB, lua_vm.LUA_STRING_LIB, lua_vm.LUA_VECTOR_LIB])
- lua_vm.lua_pushcallable(_threaded_print_handler, "print")
+ lua_vm.lua_pushcallable(_print_handler, "print")
lua_vm.lua_setglobal("print")
# Setup threaded Time.sleep function
lua_vm.lua_newtable()
- lua_vm.lua_pushcallable(_threaded_time_sleep_handler, "Time.sleep")
+ lua_vm.lua_pushcallable(_time_sleep_handler, "Time.sleep")
lua_vm.lua_setfield(-2, "sleep")
lua_vm.lua_setglobal("Time")
@@ -232,7 +232,7 @@ func _execute_timeout_in_thread(timeout_id: int):
lua_vm.lua_pop(1) # Pop the table
-func _threaded_print_handler(vm: LuauVM) -> int:
+func _print_handler(vm: LuauVM) -> int:
var message_parts: Array = []
var num_args = vm.lua_gettop()
@@ -258,7 +258,7 @@ func _threaded_print_handler(vm: LuauVM) -> int:
return 0
-func _threaded_time_sleep_handler(vm: LuauVM) -> int:
+func _time_sleep_handler(vm: LuauVM) -> int:
vm.luaL_checknumber(1)
var seconds = vm.lua_tonumber(1)
@@ -268,55 +268,65 @@ func _threaded_time_sleep_handler(vm: LuauVM) -> int:
return 0
func _setup_threaded_gurt_api():
- lua_vm.lua_pushcallable(_threaded_print_handler, "print")
+ lua_vm.lua_pushcallable(_print_handler, "print")
lua_vm.lua_setglobal("print")
LuaTimeUtils.setup_time_api(lua_vm)
lua_vm.lua_getglobal("Time")
if not lua_vm.lua_isnil(-1):
- lua_vm.lua_pushcallable(_threaded_time_sleep_handler, "Time.sleep")
+ lua_vm.lua_pushcallable(_time_sleep_handler, "Time.sleep")
lua_vm.lua_setfield(-2, "sleep")
lua_vm.lua_pop(1)
lua_vm.lua_newtable()
- lua_vm.lua_pushcallable(_threaded_print_handler, "gurt.log")
+ lua_vm.lua_pushcallable(_print_handler, "gurt.log")
lua_vm.lua_setfield(-2, "log")
- lua_vm.lua_pushcallable(_threaded_gurt_select_handler, "gurt.select")
+ lua_vm.lua_pushcallable(_gurt_select_handler, "gurt.select")
lua_vm.lua_setfield(-2, "select")
- lua_vm.lua_pushcallable(_threaded_gurt_select_all_handler, "gurt.selectAll")
+ lua_vm.lua_pushcallable(_gurt_select_all_handler, "gurt.selectAll")
lua_vm.lua_setfield(-2, "selectAll")
- lua_vm.lua_pushcallable(_threaded_gurt_create_handler, "gurt.create")
+ lua_vm.lua_pushcallable(_gurt_create_handler, "gurt.create")
lua_vm.lua_setfield(-2, "create")
- lua_vm.lua_pushcallable(_threaded_set_timeout_handler, "gurt.setTimeout")
+ lua_vm.lua_pushcallable(_set_timeout_handler, "gurt.setTimeout")
lua_vm.lua_setfield(-2, "setTimeout")
- lua_vm.lua_pushcallable(_threaded_clear_timeout_handler, "gurt.clearTimeout")
+ lua_vm.lua_pushcallable(lua_api._gurt_clear_timeout_handler, "gurt.clearTimeout")
lua_vm.lua_setfield(-2, "clearTimeout")
- lua_vm.lua_pushcallable(_threaded_set_interval_handler, "gurt.setInterval")
+ lua_vm.lua_pushcallable(_set_interval_handler, "gurt.setInterval")
lua_vm.lua_setfield(-2, "setInterval")
- lua_vm.lua_pushcallable(_threaded_clear_interval_handler, "gurt.clearInterval")
+ lua_vm.lua_pushcallable(lua_api._gurt_clear_interval_handler, "gurt.clearInterval")
lua_vm.lua_setfield(-2, "clearInterval")
- # Add body element access
+ lua_vm.lua_newtable()
+ lua_vm.lua_pushcallable(lua_api._gurt_location_reload_handler, "gurt.location.reload")
+ lua_vm.lua_setfield(-2, "reload")
+ lua_vm.lua_pushcallable(lua_api._gurt_location_goto_handler, "gurt.location.goto")
+ lua_vm.lua_setfield(-2, "goto")
+
+ var current_href = get_current_href()
+ lua_vm.lua_pushstring(current_href)
+ lua_vm.lua_setfield(-2, "href")
+
+ lua_vm.lua_setfield(-2, "location")
+
var body_element = dom_parser.find_first("body")
if body_element:
LuaDOMUtils.create_element_wrapper(lua_vm, body_element, lua_api)
- lua_vm.lua_pushcallable(_threaded_body_on_handler, "body.on")
+ lua_vm.lua_pushcallable(_body_on_handler, "body.on")
lua_vm.lua_setfield(-2, "on")
lua_vm.lua_setfield(-2, "body")
lua_vm.lua_setglobal("gurt")
func _setup_additional_lua_apis():
- # Add table.tostring utility that's needed in callbacks
lua_vm.lua_getglobal("table")
if lua_vm.lua_isnil(-1):
lua_vm.lua_pop(1)
@@ -324,9 +334,9 @@ func _setup_additional_lua_apis():
lua_vm.lua_setglobal("table")
lua_vm.lua_getglobal("table")
- lua_vm.lua_pushcallable(_threaded_table_tostring_handler, "table.tostring")
+ lua_vm.lua_pushcallable(_table_tostring_handler, "table.tostring")
lua_vm.lua_setfield(-2, "tostring")
- lua_vm.lua_pop(1) # Pop table from stack
+ lua_vm.lua_pop(1)
LuaSignalUtils.setup_signal_api(lua_vm)
LuaClipboardUtils.setup_clipboard_api(lua_vm)
@@ -335,7 +345,7 @@ func _setup_additional_lua_apis():
LuaWebSocketUtils.setup_websocket_api(lua_vm)
LuaAudioUtils.setup_audio_api(lua_vm)
-func _threaded_table_tostring_handler(vm: LuauVM) -> int:
+func _table_tostring_handler(vm: LuauVM) -> int:
vm.luaL_checktype(1, vm.LUA_TTABLE)
var table_string = LuaPrintUtils.table_to_string(vm, 1)
vm.lua_pushstring(table_string)
@@ -350,14 +360,24 @@ func _emit_script_error(error: String):
func _emit_print_output(message: String):
print_output.emit(message)
-func _threaded_gurt_select_all_handler(vm: LuauVM) -> int:
- # For threaded mode, selectAll is complex as it requires DOM access
- # Return empty array for now, or implement via main thread operation
+func _gurt_select_all_handler(vm: LuauVM) -> int:
+ var selector: String = vm.luaL_checkstring(1)
+
+ if not dom_parser or not dom_parser.parse_result:
+ vm.lua_newtable()
+ return 1
+
+ var elements = SelectorUtils.find_all_matching(selector, dom_parser.parse_result.all_elements)
+
vm.lua_newtable()
+ for i in range(elements.size()):
+ vm.lua_pushinteger(i + 1)
+ LuaDOMUtils.create_element_wrapper(vm, elements[i], lua_api)
+ vm.lua_rawset(-3)
+
return 1
-func _threaded_gurt_create_handler(vm: LuauVM) -> int:
- # Create new HTML element using existing system
+func _gurt_create_handler(vm: LuauVM) -> int:
var tag_name: String = vm.luaL_checkstring(1)
var attributes = {}
@@ -365,36 +385,29 @@ func _threaded_gurt_create_handler(vm: LuauVM) -> int:
vm.luaL_checktype(2, vm.LUA_TTABLE)
attributes = vm.lua_todictionary(2)
- # Create HTML element using existing HTMLParser
var new_element = HTMLParser.HTMLElement.new(tag_name)
- # Apply attributes and content
for attr_name in attributes:
if attr_name == "text":
- # Set text content directly on the HTML element
new_element.text_content = str(attributes[attr_name])
else:
new_element.set_attribute(attr_name, str(attributes[attr_name]))
- # Assign a unique ID
var element_id = lua_api.get_or_assign_element_id(new_element)
new_element.set_attribute("id", element_id)
- # Add to parser's element collection
dom_parser.parse_result.all_elements.append(new_element)
LuaDOMUtils.create_element_wrapper(vm, new_element, lua_api)
return 1
-func _threaded_set_timeout_handler(vm: LuauVM) -> int:
+func _set_timeout_handler(vm: LuauVM) -> int:
vm.luaL_checktype(1, vm.LUA_TFUNCTION)
var delay_ms: int = vm.luaL_checkint(2)
- # Generate a unique timeout ID
var timeout_id = lua_api.timeout_manager.next_timeout_id
lua_api.timeout_manager.next_timeout_id += 1
- # Store the callback in THIS threaded VM's registry
vm.lua_pushstring("GURT_THREADED_TIMEOUTS")
vm.lua_rawget(vm.LUA_REGISTRYINDEX)
if vm.lua_isnil(-1):
@@ -405,29 +418,25 @@ func _threaded_set_timeout_handler(vm: LuauVM) -> int:
vm.lua_rawset(vm.LUA_REGISTRYINDEX)
vm.lua_pushinteger(timeout_id)
- vm.lua_pushvalue(1) # Copy the callback function
+ vm.lua_pushvalue(1)
vm.lua_rawset(-3)
vm.lua_pop(1)
- # Create timeout info and send timer creation command to main thread
call_deferred("_create_threaded_timeout", timeout_id, delay_ms)
vm.lua_pushinteger(timeout_id)
return 1
-func _threaded_clear_timeout_handler(vm: LuauVM) -> int:
- # Delegate to Lua API timeout system
+func _clear_timeout_handler(vm: LuauVM) -> int:
return lua_api._gurt_clear_timeout_handler(vm)
-func _threaded_set_interval_handler(vm: LuauVM) -> int:
+func _set_interval_handler(vm: LuauVM) -> int:
vm.luaL_checktype(1, vm.LUA_TFUNCTION)
var delay_ms: int = vm.luaL_checkint(2)
- # Generate a unique interval ID
var interval_id = lua_api.timeout_manager.next_timeout_id
lua_api.timeout_manager.next_timeout_id += 1
- # Store the callback in THIS threaded VM's registry (same as timeout)
vm.lua_pushstring("GURT_THREADED_TIMEOUTS")
vm.lua_rawget(vm.LUA_REGISTRYINDEX)
if vm.lua_isnil(-1):
@@ -438,48 +447,40 @@ func _threaded_set_interval_handler(vm: LuauVM) -> int:
vm.lua_rawset(vm.LUA_REGISTRYINDEX)
vm.lua_pushinteger(interval_id)
- vm.lua_pushvalue(1) # Copy the callback function
+ vm.lua_pushvalue(1)
vm.lua_rawset(-3)
vm.lua_pop(1)
- # Create interval info and send timer creation command to main thread
call_deferred("_create_threaded_interval", interval_id, delay_ms)
vm.lua_pushinteger(interval_id)
return 1
-func _threaded_clear_interval_handler(vm: LuauVM) -> int:
- # Delegate to Lua API timeout system (clearInterval works same as clearTimeout)
- return lua_api._gurt_clear_interval_handler(vm)
+func get_current_href() -> String:
+ var main_node = Engine.get_main_loop().current_scene
+
+ return main_node.current_domain
-func _threaded_gurt_select_handler(vm: LuauVM) -> int:
+func _gurt_select_handler(vm: LuauVM) -> int:
var selector: String = vm.luaL_checkstring(1)
if not dom_parser or not dom_parser.parse_result:
vm.lua_pushnil()
return 1
- # Find the element using the existing SelectorUtils
var element = SelectorUtils.find_first_matching(selector, dom_parser.parse_result.all_elements)
if element:
- # Use DOM.gd element wrapper
LuaDOMUtils.create_element_wrapper(vm, element, lua_api)
return 1
else:
- # Return nil if element not found
vm.lua_pushnil()
return 1
-# All element handlers now use DOM.gd wrappers
-
-func _threaded_body_on_handler(vm: LuauVM) -> int:
- # Handle body event registration in threaded mode
- # Arguments: (self, event_name, callback) due to colon syntax
- vm.luaL_checktype(1, vm.LUA_TTABLE) # self (body table)
- var event_name: String = vm.luaL_checkstring(2) # event name
- vm.luaL_checktype(3, vm.LUA_TFUNCTION) # callback function
+func _body_on_handler(vm: LuauVM) -> int:
+ vm.luaL_checktype(1, vm.LUA_TTABLE)
+ var event_name: String = vm.luaL_checkstring(2)
+ vm.luaL_checktype(3, vm.LUA_TFUNCTION)
- # Store callback in registry
vm.lua_pushstring("THREADED_CALLBACKS")
vm.lua_rawget(vm.LUA_REGISTRYINDEX)
if vm.lua_isnil(-1):
@@ -492,16 +493,14 @@ func _threaded_body_on_handler(vm: LuauVM) -> int:
var callback_ref = lua_api.next_callback_ref
lua_api.next_callback_ref += 1
- # Get a proper subscription ID
var subscription_id = lua_api.next_subscription_id
lua_api.next_subscription_id += 1
vm.lua_pushinteger(callback_ref)
- vm.lua_pushvalue(3) # Copy the callback function (3rd argument)
+ vm.lua_pushvalue(3)
vm.lua_rawset(-3)
vm.lua_pop(1)
- # Queue DOM operation for main thread (body events)
var operation = {
"type": "register_body_event",
"event_name": event_name,
@@ -511,7 +510,6 @@ func _threaded_body_on_handler(vm: LuauVM) -> int:
call_deferred("_emit_dom_operation_request", operation)
- # Return subscription with unsubscribe method
vm.lua_newtable()
vm.lua_pushinteger(subscription_id)
vm.lua_setfield(-2, "_subscription_id")
@@ -524,15 +522,12 @@ func _emit_dom_operation_request(operation: Dictionary):
dom_operation_request.emit(operation)
func _create_threaded_timeout(timeout_id: int, delay_ms: int):
- # Ensure timeout manager exists
lua_api._ensure_timeout_manager()
- # Create timeout info for threaded execution
var timeout_info = lua_api.timeout_manager.TimeoutInfo.new(timeout_id, timeout_id, lua_vm, lua_api.timeout_manager, false, delay_ms)
lua_api.timeout_manager.active_timeouts[timeout_id] = timeout_info
lua_api.timeout_manager.threaded_vm = self
- # Create and start timer on main thread
var timer = Timer.new()
timer.wait_time = delay_ms / 1000.0
timer.one_shot = true
@@ -543,17 +538,15 @@ func _create_threaded_timeout(timeout_id: int, delay_ms: int):
timer.start()
func _create_threaded_interval(interval_id: int, delay_ms: int):
- # Ensure timeout manager exists
lua_api._ensure_timeout_manager()
- # Create interval info for threaded execution
var timeout_info = lua_api.timeout_manager.TimeoutInfo.new(interval_id, interval_id, lua_vm, lua_api.timeout_manager, true, delay_ms)
lua_api.timeout_manager.active_timeouts[interval_id] = timeout_info
lua_api.timeout_manager.threaded_vm = self
var timer = Timer.new()
timer.wait_time = delay_ms / 1000.0
- timer.one_shot = false # Repeating timer for intervals
+ timer.one_shot = false
timer.timeout.connect(lua_api.timeout_manager._on_timeout_triggered.bind(timeout_info))
timeout_info.timer = timer
diff --git a/flumi/Scripts/Utils/URLUtils.gd b/flumi/Scripts/Utils/URLUtils.gd
new file mode 100644
index 0000000..31527a1
--- /dev/null
+++ b/flumi/Scripts/Utils/URLUtils.gd
@@ -0,0 +1,62 @@
+class_name URLUtils
+extends RefCounted
+
+static func resolve_url(base_url: String, relative_url: String) -> String:
+ # If relative_url is already absolute, return it as-is
+ if relative_url.begins_with("http://") or relative_url.begins_with("https://") or relative_url.begins_with("gurt://"):
+ return relative_url
+
+ # If empty, treat as relative to current domain
+ if base_url.is_empty():
+ return relative_url
+
+ var clean_base = base_url.rstrip("/")
+
+ # Parse scheme and host
+ var scheme_end = clean_base.find("://")
+ if scheme_end == -1:
+ return relative_url
+
+ var scheme = clean_base.substr(0, scheme_end + 3)
+ var remainder = clean_base.substr(scheme_end + 3)
+
+ # Split remainder into host and path
+ var first_slash = remainder.find("/")
+ var host = ""
+ var current_path_parts = []
+
+ if first_slash == -1:
+ # No path in base URL, just host
+ host = remainder
+ else:
+ host = remainder.substr(0, first_slash)
+ var path = remainder.substr(first_slash + 1)
+ if not path.is_empty():
+ current_path_parts = path.split("/")
+
+ var final_path_parts = []
+
+ if relative_url.begins_with("/"):
+ # Absolute path from root
+ var href_path = relative_url.substr(1) if relative_url.length() > 1 else ""
+ if not href_path.is_empty():
+ final_path_parts = href_path.split("/")
+ else:
+ # Relative path
+ final_path_parts = current_path_parts.duplicate()
+
+ var href_parts = relative_url.split("/")
+ for part in href_parts:
+ if part == "..":
+ if final_path_parts.size() > 0:
+ final_path_parts.pop_back()
+ elif part == "." or part == "":
+ continue
+ else:
+ final_path_parts.append(part)
+
+ var result = scheme + host
+ if final_path_parts.size() > 0:
+ result += "/" + "/".join(final_path_parts)
+
+ return result
\ No newline at end of file
diff --git a/flumi/Scripts/Utils/URLUtils.gd.uid b/flumi/Scripts/Utils/URLUtils.gd.uid
new file mode 100644
index 0000000..d0b09a5
--- /dev/null
+++ b/flumi/Scripts/Utils/URLUtils.gd.uid
@@ -0,0 +1 @@
+uid://bpdeuavthgjxt
diff --git a/flumi/Scripts/main.gd b/flumi/Scripts/main.gd
index 27623bc..5795395 100644
--- a/flumi/Scripts/main.gd
+++ b/flumi/Scripts/main.gd
@@ -71,42 +71,7 @@ func recalculate_percentage_elements(node: Node):
var current_domain = "" # Store current domain for display
func resolve_url(href: String) -> String:
- if href.begins_with("http://") or href.begins_with("https://") or href.begins_with("gurt://"):
- return href
-
- if current_domain.is_empty():
- return href
-
- var clean_domain = current_domain.rstrip("/")
-
- var current_parts = clean_domain.split("/")
- var host = current_parts[0]
- var current_path_parts = Array(current_parts.slice(1)) if current_parts.size() > 1 else []
-
- var final_path_parts = []
-
- if href.begins_with("/"):
- var href_path = href.substr(1) if href.length() > 1 else ""
- if not href_path.is_empty():
- final_path_parts = href_path.split("/")
- else:
- final_path_parts = current_path_parts.duplicate()
-
- var href_parts = href.split("/")
- for part in href_parts:
- if part == "..":
- if final_path_parts.size() > 0:
- final_path_parts.pop_back()
- elif part == "." or part == "":
- continue
- else:
- final_path_parts.append(part)
-
- var result = "gurt://" + host
- if final_path_parts.size() > 0:
- result += "/" + "/".join(final_path_parts)
-
- return result
+ return URLUtils.resolve_url(current_domain, href)
func handle_link_click(meta: Variant) -> void:
var href = str(meta)
@@ -134,25 +99,36 @@ func _on_search_submitted(url: String) -> void:
const GLOBE_ICON = preload("res://Assets/Icons/globe.svg")
tab.stop_loading()
tab.set_icon(GLOBE_ICON)
+ return
var html_bytes = result.html
if result.has("display_url"):
current_domain = result.display_url
+ if not current_domain.begins_with("gurt://"):
+ current_domain = "gurt://" + current_domain
if not search_bar.has_focus():
- search_bar.text = current_domain
+ search_bar.text = result.display_url # Show clean version in search bar
+ else:
+ current_domain = url
render_content(html_bytes)
+
+ # Stop loading spinner after successful render
+ tab.stop_loading()
else:
print("Non-GURT URL entered: ", url)
func _on_search_focus_entered() -> void:
if not current_domain.is_empty():
- search_bar.text = "gurt://" + current_domain
+ search_bar.text = current_domain
func _on_search_focus_exited() -> void:
if not current_domain.is_empty():
- search_bar.text = current_domain
+ var display_text = current_domain
+ if display_text.begins_with("gurt://"):
+ display_text = display_text.substr(7)
+ search_bar.text = display_text
func render() -> void:
render_content(Constants.HTML_CONTENT)
@@ -163,6 +139,19 @@ func render_content(html_bytes: PackedByteArray) -> void:
for child in website_container.get_children():
child.queue_free()
+ var current_parent = website_container.get_parent()
+ if current_parent and current_parent.name == "BodyMarginContainer":
+ var original_parent = current_parent.get_parent()
+ var container_index = current_parent.get_index()
+
+ current_parent.remove_child(website_container)
+
+ original_parent.remove_child(current_parent)
+ current_parent.queue_free()
+
+ original_parent.add_child(website_container)
+ original_parent.move_child(website_container, container_index)
+
font_dependent_elements.clear()
FontManager.clear_fonts()
FontManager.set_refresh_callback(refresh_fonts)
@@ -172,6 +161,9 @@ func render_content(html_bytes: PackedByteArray) -> void:
parser.process_styles()
+ if parse_result.external_css and not parse_result.external_css.is_empty():
+ await parser.process_external_styles(current_domain)
+
# Process and load all custom fonts defined in tags
parser.process_fonts()
FontManager.load_all_fonts()
@@ -257,6 +249,8 @@ func render_content(html_bytes: PackedByteArray) -> void:
if scripts.size() > 0 and lua_api:
parser.process_scripts(lua_api, null)
+ if parse_result.external_scripts and not parse_result.external_scripts.is_empty():
+ await parser.process_external_scripts(lua_api, null, current_domain)
static func safe_add_child(parent: Node, child: Node) -> void:
if child.get_parent():
@@ -513,3 +507,14 @@ func refresh_fonts(font_name: String) -> void:
if styles.has("font-family") and styles["font-family"] == font_name:
if is_instance_valid(label):
StyleManager.apply_styles_to_label(label, styles, element, parser)
+
+func get_current_url() -> String:
+ return current_domain if not current_domain.is_empty() else ""
+
+func reload_current_page() -> void:
+ if not current_domain.is_empty():
+ _on_search_submitted(current_domain)
+
+func navigate_to_url(url: String) -> void:
+ var resolved_url = resolve_url(url)
+ _on_search_submitted(resolved_url)