Merge branch 'outpoot:main' into main
This commit is contained in:
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: "[BUG] "
|
||||||
|
labels: bug
|
||||||
|
assignees: face-hh
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
Or code example:
|
||||||
|
```html
|
||||||
|
<h1>Hello</h1>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: "[FEATURE]"
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
[Website](https://gurted.com/) | [Docs](https://docs.gurted.com/) | [License](LICENSE) | [YouTube video](https://www.youtube.com)
|
[Website](https://gurted.com/) | [Docs](https://docs.gurted.com/) | [License](LICENSE) | [YouTube video](https://www.youtube.com)
|
||||||
|
|
||||||
Gurted is an ecosystem similar to the World Wide Web, it features:
|
Gurted is an ecosystem similar to the World Wide Web, it features:
|
||||||
- ⚡ A custom protocol (TCP-based) named `GURT://` with mendatory TLS secutity with a [spec](docs.gurted.com)
|
- ⚡ A custom protocol (TCP-based) named `GURT://` with mandatory TLS security with a [spec](docs.gurted.com)
|
||||||
- 🌐 A custom **wayfinder** (browser) written in Rust and GDScript with [Godot](https://godotengine.org/)
|
- 🌐 A custom **wayfinder** (browser) written in Rust and GDScript with [Godot](https://godotengine.org/)
|
||||||
- 📄 A custom engine for HTML, CSS, and ***Lua*** (no JavaScript)
|
- 📄 A custom engine for HTML, CSS, and ***Lua*** (no JavaScript)
|
||||||
- 🏷️ A custom **DNS** that allows users to create domains with TLDs such as `.based`, `.aura`, `.twin`, and many more
|
- 🏷️ A custom **DNS** that allows users to create domains with TLDs such as `.based`, `.aura`, `.twin`, and many more
|
||||||
|
|||||||
4
dns/Cargo.lock
generated
4
dns/Cargo.lock
generated
@@ -972,7 +972,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gurt"
|
name = "gurtlib"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
@@ -3410,7 +3410,7 @@ dependencies = [
|
|||||||
"clap-verbosity-flag",
|
"clap-verbosity-flag",
|
||||||
"colored",
|
"colored",
|
||||||
"futures",
|
"futures",
|
||||||
"gurt",
|
"gurtlib",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"log",
|
"log",
|
||||||
"macros-rs",
|
"macros-rs",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ anyhow = "1.0.86"
|
|||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
macros-rs = "1.2.1"
|
macros-rs = "1.2.1"
|
||||||
prettytable = "0.10.0"
|
prettytable = "0.10.0"
|
||||||
gurt = { path = "../protocol/library" }
|
gurtlib = { path = "../protocol/library" }
|
||||||
pretty_env_logger = "0.5.0"
|
pretty_env_logger = "0.5.0"
|
||||||
clap-verbosity-flag = "2.2.0"
|
clap-verbosity-flag = "2.2.0"
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use gurt::prelude::*;
|
use gurtlib::prelude::*;
|
||||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation, Algorithm};
|
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation, Algorithm};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use bcrypt::{hash, verify, DEFAULT_COST};
|
use bcrypt::{hash, verify, DEFAULT_COST};
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ use openssl::rsa::Rsa;
|
|||||||
use openssl::x509::X509Req;
|
use openssl::x509::X509Req;
|
||||||
use openssl::x509::X509Name;
|
use openssl::x509::X509Name;
|
||||||
use openssl::hash::MessageDigest;
|
use openssl::hash::MessageDigest;
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
pub fn generate_ca_cert() -> Result<(String, String)> {
|
pub fn generate_ca_cert() -> Result<(String, String)> {
|
||||||
let rsa = Rsa::generate(4096)?;
|
let rsa = Rsa::generate(4096)?;
|
||||||
@@ -60,7 +59,8 @@ pub fn sign_csr_with_ca(
|
|||||||
csr_pem: &str,
|
csr_pem: &str,
|
||||||
ca_cert_pem: &str,
|
ca_cert_pem: &str,
|
||||||
ca_key_pem: &str,
|
ca_key_pem: &str,
|
||||||
domain: &str
|
domain: &str,
|
||||||
|
client_ip: Option<&str>
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let ca_cert = openssl::x509::X509::from_pem(ca_cert_pem.as_bytes())?;
|
let ca_cert = openssl::x509::X509::from_pem(ca_cert_pem.as_bytes())?;
|
||||||
let ca_key = PKey::private_key_from_pem(ca_key_pem.as_bytes())?;
|
let ca_key = PKey::private_key_from_pem(ca_key_pem.as_bytes())?;
|
||||||
@@ -92,8 +92,11 @@ pub fn sign_csr_with_ca(
|
|||||||
.dns("localhost")
|
.dns("localhost")
|
||||||
.ip("127.0.0.1");
|
.ip("127.0.0.1");
|
||||||
|
|
||||||
if let Ok(public_ip) = get_public_ip() {
|
if let Some(ip) = client_ip {
|
||||||
san_builder.ip(&public_ip);
|
if is_valid_ip(ip) && ip != "127.0.0.1" {
|
||||||
|
san_builder.ip(ip);
|
||||||
|
println!("Added client IP {} to certificate for {}", ip, domain);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let subject_alt_name = san_builder.build(&context)?;
|
let subject_alt_name = san_builder.build(&context)?;
|
||||||
@@ -119,50 +122,6 @@ pub fn sign_csr_with_ca(
|
|||||||
Ok(String::from_utf8(cert_pem)?)
|
Ok(String::from_utf8(cert_pem)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_public_ip() -> Result<String, Box<dyn std::error::Error>> {
|
|
||||||
// Method 1: Check if we can get it from environment or interface
|
|
||||||
if let Ok(output) = Command::new("curl")
|
|
||||||
.args(&["-s", "--max-time", "5", "https://api.ipify.org"])
|
|
||||||
.output()
|
|
||||||
{
|
|
||||||
if output.status.success() {
|
|
||||||
let ip = String::from_utf8(output.stdout)?.trim().to_string();
|
|
||||||
if is_valid_ip(&ip) {
|
|
||||||
return Ok(ip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method 2: Try ifconfig.me
|
|
||||||
if let Ok(output) = Command::new("curl")
|
|
||||||
.args(&["-s", "--max-time", "5", "https://ifconfig.me/ip"])
|
|
||||||
.output()
|
|
||||||
{
|
|
||||||
if output.status.success() {
|
|
||||||
let ip = String::from_utf8(output.stdout)?.trim().to_string();
|
|
||||||
if is_valid_ip(&ip) {
|
|
||||||
return Ok(ip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method 3: Try to get from network interfaces
|
|
||||||
if let Ok(output) = Command::new("hostname")
|
|
||||||
.args(&["-I"])
|
|
||||||
.output()
|
|
||||||
{
|
|
||||||
if output.status.success() {
|
|
||||||
let ips = String::from_utf8(output.stdout)?;
|
|
||||||
for ip in ips.split_whitespace() {
|
|
||||||
if is_valid_ip(ip) && !ip.starts_with("127.") && !ip.starts_with("192.168.") && !ip.starts_with("10.") {
|
|
||||||
return Ok(ip.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err("Could not determine public IP".into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_valid_ip(ip: &str) -> bool {
|
fn is_valid_ip(ip: &str) -> bool {
|
||||||
ip.split('.')
|
ip.split('.')
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ use crate::{auth::jwt_middleware_gurt, config::Config, discord_bot};
|
|||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use macros_rs::fmt::{crashln, string};
|
use macros_rs::fmt::{crashln, string};
|
||||||
use std::{sync::Arc, collections::HashMap};
|
use std::{sync::Arc, collections::HashMap};
|
||||||
use gurt::prelude::*;
|
use gurtlib::prelude::*;
|
||||||
use warp::Filter;
|
use warp::Filter;
|
||||||
use gurt::{GurtStatusCode, Route};
|
use gurtlib::{GurtStatusCode, Route};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct CertificateError;
|
struct CertificateError;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::{models::*, AppState};
|
use super::{models::*, AppState};
|
||||||
use crate::auth::*;
|
use crate::auth::*;
|
||||||
use gurt::prelude::*;
|
use gurtlib::prelude::*;
|
||||||
use gurt::GurtStatusCode;
|
use gurtlib::GurtStatusCode;
|
||||||
use sqlx::Row;
|
use sqlx::Row;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use super::{models::*, AppState};
|
|||||||
use crate::auth::Claims;
|
use crate::auth::Claims;
|
||||||
use crate::discord_bot::{send_domain_approval_request, DomainRegistration};
|
use crate::discord_bot::{send_domain_approval_request, DomainRegistration};
|
||||||
use base64::{engine::general_purpose, Engine as _};
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
use gurt::prelude::*;
|
use gurtlib::prelude::*;
|
||||||
use rand::{rngs::OsRng, Rng};
|
use rand::{rngs::OsRng, Rng};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
@@ -1056,7 +1056,7 @@ pub(crate) async fn get_certificate(
|
|||||||
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
||||||
|
|
||||||
if txt_records.is_empty() {
|
if txt_records.is_empty() {
|
||||||
return Ok(GurtResponse::new(gurt::GurtStatusCode::Accepted)
|
return Ok(GurtResponse::new(gurtlib::GurtStatusCode::Accepted)
|
||||||
.with_string_body("Challenge not completed yet"));
|
.with_string_body("Challenge not completed yet"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1072,6 +1072,7 @@ pub(crate) async fn get_certificate(
|
|||||||
&ca_cert.ca_cert_pem,
|
&ca_cert.ca_cert_pem,
|
||||||
&ca_cert.ca_key_pem,
|
&ca_cert.ca_key_pem,
|
||||||
&domain,
|
&domain,
|
||||||
|
Some(&ctx.client_ip().to_string()),
|
||||||
)
|
)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
log::error!("Failed to sign certificate: {}", e);
|
log::error!("Failed to sign certificate: {}", e);
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ The GURT client library (for Rust) provides a high-level, HTTP-like interface fo
|
|||||||
|
|
||||||
Install via Cargo:
|
Install via Cargo:
|
||||||
```bash
|
```bash
|
||||||
cargo add gurt
|
cargo add gurtlib
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use gurt::prelude::*;
|
use gurtlib::prelude::*;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
@@ -223,7 +223,7 @@ The client extracts:
|
|||||||
### Error Types
|
### Error Types
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use gurt::GurtError;
|
use gurtlib::GurtError;
|
||||||
|
|
||||||
match client.get("gurt://invalid-url").await {
|
match client.get("gurt://invalid-url").await {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
@@ -268,7 +268,7 @@ We expect the community to implement bindings for other languages, such as Pytho
|
|||||||
## Example: Building a GURT API Client
|
## Example: Building a GURT API Client
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use gurt::prelude::*;
|
use gurtlib::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Add the GURT library to your `Cargo.toml`:
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gurt = "0.1"
|
gurtlib = "0.1"
|
||||||
tokio = { version = "1.0", features = ["full"] }
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
@@ -22,7 +22,7 @@ serde_json = "1.0"
|
|||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use gurt::prelude::*;
|
use gurtlib::prelude::*;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -442,7 +442,7 @@ server.listen("[::1]:4878").await?;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use gurt::GurtClient;
|
use gurtlib::GurtClient;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_server() {
|
async fn test_server() {
|
||||||
|
|||||||
@@ -215,21 +215,18 @@ local shaderCtx = canvas:withContext('shader')
|
|||||||
|
|
||||||
shaderCtx:source([[
|
shaderCtx:source([[
|
||||||
shader_type canvas_item;
|
shader_type canvas_item;
|
||||||
|
|
||||||
uniform float time : hint_range(0.0, 10.0) = 1.0;
|
|
||||||
uniform vec2 resolution;
|
|
||||||
|
|
||||||
void fragment() {
|
void fragment() {
|
||||||
vec2 uv = UV;
|
vec2 uv = UV;
|
||||||
|
|
||||||
// Create animated rainbow effect
|
// Create animated rainbow effect
|
||||||
vec3 color = vec3(
|
vec3 color = vec3(
|
||||||
0.5 + 0.5 * cos(time + uv.x * 6.0),
|
0.5 + 0.5 * cos(TIME + uv.x * 6.0),
|
||||||
0.5 + 0.5 * cos(time + uv.y * 6.0 + 2.0),
|
0.5 + 0.5 * cos(TIME + uv.y * 6.0 + 2.0),
|
||||||
0.5 + 0.5 * cos(time + (uv.x + uv.y) * 6.0 + 4.0)
|
0.5 + 0.5 * cos(TIME + (uv.x + uv.y) * 6.0 + 4.0)
|
||||||
);
|
);
|
||||||
|
|
||||||
COLOR = vec4(color, 1.0);
|
COLOR = vec4(color, 1.0);
|
||||||
}
|
}
|
||||||
]])
|
]])
|
||||||
```
|
```
|
||||||
|
|||||||
132
docs/docs/lua/crumbs.md
Normal file
132
docs/docs/lua/crumbs.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 9
|
||||||
|
---
|
||||||
|
|
||||||
|
# Crumbs
|
||||||
|
|
||||||
|
The Crumbs API provides **client-side storage** similar to browser cookies. Crumbs are stored locally and can have optional expiration times. They are domain-specific, meaning each GURT domain has its own isolated storage.
|
||||||
|
|
||||||
|
Storage location: `%APPDATA%/Flumi/crumbs/[domain].json`
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### gurt.crumbs.set(options)
|
||||||
|
|
||||||
|
Sets a crumb with the specified name and value.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `options` (table) - Configuration object with the following fields:
|
||||||
|
- `name` (string, required) - The crumb name/key
|
||||||
|
- `value` (string, required) - The crumb value
|
||||||
|
- `lifetime` (number, optional) - Lifetime in seconds. If omitted, the crumb persists indefinitely
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Set a permanent crumb
|
||||||
|
gurt.crumbs.set({
|
||||||
|
name = "username",
|
||||||
|
value = "gurted_user"
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Set a temporary crumb (expires in 10 seconds)
|
||||||
|
gurt.crumbs.set({
|
||||||
|
name = "session_token",
|
||||||
|
value = "abc123def456",
|
||||||
|
lifetime = 10
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Set a short-lived crumb (expires in 30 seconds)
|
||||||
|
gurt.crumbs.set({
|
||||||
|
name = "temp_data",
|
||||||
|
value = "temporary_value",
|
||||||
|
lifetime = 30
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### gurt.crumbs.get(name)
|
||||||
|
|
||||||
|
Retrieves a crumb value by name. Returns `nil` if the crumb doesn't exist or has expired.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `name` (string) - The crumb name to retrieve
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `string` - The crumb value, or `nil` if not found/expired
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Get a crumb value
|
||||||
|
local username = gurt.crumbs.get("username")
|
||||||
|
if username then
|
||||||
|
trace.log("Welcome back, " .. username .. "!")
|
||||||
|
else
|
||||||
|
trace.log("No username found")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### gurt.crumbs.delete(name)
|
||||||
|
|
||||||
|
Deletes a crumb by name.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `name` (string) - The crumb name to delete
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `boolean` - `true` if the crumb existed and was deleted, `false` if it didn't exist
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local wasDeleted = gurt.crumbs.delete("session_token")
|
||||||
|
if wasDeleted then
|
||||||
|
trace.log("Session token removed")
|
||||||
|
else
|
||||||
|
trace.log("Session token was not found")
|
||||||
|
end
|
||||||
|
|
||||||
|
gurt.crumbs.delete("temp_data")
|
||||||
|
```
|
||||||
|
|
||||||
|
### gurt.crumbs.getAll()
|
||||||
|
|
||||||
|
Retrieves all non-expired crumbs for the current domain.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `table` - A table where keys are crumb names and values are crumb objects
|
||||||
|
|
||||||
|
Each crumb object contains:
|
||||||
|
- `name` (string) - The crumb name
|
||||||
|
- `value` (string) - The crumb value
|
||||||
|
- `expiry` (number, optional) - Unix timestamp when the crumb expires (only present for temporary crumbs)
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local allCrumbs = gurt.crumbs.getAll()
|
||||||
|
|
||||||
|
for name, crumb in pairs(allCrumbs) do
|
||||||
|
trace.log("Crumb: " .. name .. " = " .. crumb.value)
|
||||||
|
|
||||||
|
if crumb.expiry then
|
||||||
|
local remaining = crumb.expiry - (Time.now() / 1000)
|
||||||
|
trace.log(" Expires in " .. math.floor(remaining) .. " seconds")
|
||||||
|
else
|
||||||
|
trace.log(" Permanent crumb")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Storage Format
|
||||||
|
|
||||||
|
Crumbs are stored in JSON files:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"value": "alice",
|
||||||
|
"created_at": 1672531200.0,
|
||||||
|
"lifespan": -1.0
|
||||||
|
},
|
||||||
|
"session_token": {
|
||||||
|
"name": "session_token",
|
||||||
|
"value": "abc123",
|
||||||
|
"created_at": 1672531200.0,
|
||||||
|
"lifespan": 3600.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -55,6 +55,39 @@ for i = 1, #children do
|
|||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### element.size
|
||||||
|
|
||||||
|
Gets the size of an element in pixels.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local box = gurt.select('#my-box')
|
||||||
|
local size = box.size
|
||||||
|
|
||||||
|
trace.log('Width: ' .. size.width .. 'px')
|
||||||
|
trace.log('Height: ' .. size.height .. 'px')
|
||||||
|
|
||||||
|
if size.width == size.height then
|
||||||
|
trace.log('Element is square')
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### element.position
|
||||||
|
|
||||||
|
Gets the position of an element relative to its parent.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local element = gurt.select('#positioned-element')
|
||||||
|
local pos = element.position
|
||||||
|
|
||||||
|
trace.log('X position: ' .. pos.x .. 'px')
|
||||||
|
trace.log('Y position: ' .. pos.y .. 'px')
|
||||||
|
|
||||||
|
-- Check if element is at origin
|
||||||
|
if pos.x == 0 and pos.y == 0 then
|
||||||
|
trace.log('Element is at origin')
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
## DOM Traversal
|
## DOM Traversal
|
||||||
|
|
||||||
### element.parent
|
### element.parent
|
||||||
@@ -96,11 +129,19 @@ Adds an event listener. Returns a subscription object.
|
|||||||
local button = gurt.select('#my-button')
|
local button = gurt.select('#my-button')
|
||||||
|
|
||||||
-- Click event
|
-- Click event
|
||||||
local subscription = button:on('click', function()
|
local subscription = button:on('click', function(event)
|
||||||
trace.log('Button clicked!')
|
trace.log('Button clicked at: ' .. event.x .. ', ' .. event.y)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- Mouse events
|
-- Mouse events
|
||||||
|
button:on('mousedown', function(event)
|
||||||
|
trace.log('Mouse down at: ' .. event.x .. ', ' .. event.y)
|
||||||
|
end)
|
||||||
|
|
||||||
|
button:on('mouseup', function(event)
|
||||||
|
trace.log('Mouse up at: ' .. event.x .. ', ' .. event.y)
|
||||||
|
end)
|
||||||
|
|
||||||
button:on('mouseenter', function()
|
button:on('mouseenter', function()
|
||||||
button.classList:add('hover-effect')
|
button.classList:add('hover-effect')
|
||||||
end)
|
end)
|
||||||
@@ -128,6 +169,30 @@ end)
|
|||||||
subscription:unsubscribe()
|
subscription:unsubscribe()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Mouse Event Positioning
|
||||||
|
|
||||||
|
Mouse events (`click`, `mousedown`, `mouseup`) provide position information relative to the element:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local element = gurt.select('#interactive-element')
|
||||||
|
|
||||||
|
element:on('click', function(event)
|
||||||
|
-- event.x and event.y are relative to the element's top-left corner
|
||||||
|
local elementX = event.x
|
||||||
|
local elementY = event.y
|
||||||
|
|
||||||
|
trace.log('Clicked at (' .. elementX .. ', ' .. elementY .. ') within element')
|
||||||
|
|
||||||
|
local size = element.size
|
||||||
|
local pos = element.position
|
||||||
|
|
||||||
|
local xPercent = (elementX / size.width) * 100
|
||||||
|
local yPercent = (elementY / size.height) * 100
|
||||||
|
|
||||||
|
trace.log('Clicked at ' .. xPercent .. '% horizontally, ' .. yPercent .. '% vertically')
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
### element:append(childElement)
|
### element:append(childElement)
|
||||||
|
|
||||||
Adds a child element.
|
Adds a child element.
|
||||||
|
|||||||
@@ -112,6 +112,25 @@ end
|
|||||||
-- Get all values for a parameter (for repeated params)
|
-- Get all values for a parameter (for repeated params)
|
||||||
local tags = gurt.location.query.getAll('tag')
|
local tags = gurt.location.query.getAll('tag')
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### gurt.width()
|
||||||
|
|
||||||
|
Gets the available width of the site page viewport in pixels.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local pageWidth = gurt.width()
|
||||||
|
trace.log('Page width: ' .. pageWidth .. ' pixels')
|
||||||
|
```
|
||||||
|
|
||||||
|
### gurt.height()
|
||||||
|
|
||||||
|
Gets the available height of the site page viewport in pixels.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local pageHeight = gurt.height()
|
||||||
|
trace.log('Page height: ' .. pageHeight .. ' pixels')
|
||||||
|
```
|
||||||
|
|
||||||
## Global: trace
|
## Global: trace
|
||||||
|
|
||||||
The global trace table for logging messages to the console.
|
The global trace table for logging messages to the console.
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ const sidebars: SidebarsConfig = {
|
|||||||
items: [
|
items: [
|
||||||
'lua/intro',
|
'lua/intro',
|
||||||
'lua/elements',
|
'lua/elements',
|
||||||
|
'lua/crumbs',
|
||||||
'lua/audio',
|
'lua/audio',
|
||||||
'lua/canvas',
|
'lua/canvas',
|
||||||
'lua/network',
|
'lua/network',
|
||||||
|
|||||||
@@ -1,2 +1,28 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEvjCCAyagAwIBAgIRAOgNdrOTI5GLs9YoNMGXILkwDQYJKoZIhvcNAQELBQAw
|
||||||
|
dzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSYwJAYDVQQLDB1zbWFy
|
||||||
|
dGNvZGVyQG5peG9zIChTbWFydGNvZGVyKTEtMCsGA1UEAwwkbWtjZXJ0IHNtYXJ0
|
||||||
|
Y29kZXJAbml4b3MgKFNtYXJ0Y29kZXIpMB4XDTI1MDkwODA4MDIxN1oXDTM1MDkw
|
||||||
|
ODA4MDIxN1owdzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSYwJAYD
|
||||||
|
VQQLDB1zbWFydGNvZGVyQG5peG9zIChTbWFydGNvZGVyKTEtMCsGA1UEAwwkbWtj
|
||||||
|
ZXJ0IHNtYXJ0Y29kZXJAbml4b3MgKFNtYXJ0Y29kZXIpMIIBojANBgkqhkiG9w0B
|
||||||
|
AQEFAAOCAY8AMIIBigKCAYEAqkhX/N1Mjzoq9CgdREOFkW+TPeo5yshLHVFqWiY/
|
||||||
|
8gxmuFd6kK+TfnsUKeDKK2z/KdZWlWjSxFdvZSn19TPsZ1OLgn0SbWXvUiFjjlJk
|
||||||
|
/bgVU7bwAR+TvEOMJ/TTK/T9LkWLK4pQov5+LtnuLh5s0aLUW/eV1OcYNdQGuEhD
|
||||||
|
IzN8ITp7vZeKTAB0TGu+hVG+xsVdnRPsmugP0EQy7jBtB4KjK+CCxkMYQRS17h22
|
||||||
|
RyDDwvtLIGWWHfza6M1MkEqfTevzBR/3fAt4kFKt19p+pVG7bHuVxHaVdiMLPq0h
|
||||||
|
vR52ELxNb3v8j7a0ZTQG3sym+1J0Avr5z5onuPx0rxsaLoX7NvzPQZk+hxEYynOP
|
||||||
|
f0lgoMOJEcMPXcj+dEBpB5Q5igR9OEs53wINYvN5lOw6X466DZH8ofr/h8uAtYBp
|
||||||
|
y9DXdbOddTFSIHfYDlQsdK8txgrW0kgs1raoS8h2EI9CETCjKzeuID6bp1/6K96n
|
||||||
|
G5pk5aat+ElBkWa4o4OCiRWRAgMBAAGjRTBDMA4GA1UdDwEB/wQEAwICBDASBgNV
|
||||||
|
HRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBQysYpcKzkvcIPrQQDo/olzWnKGoTAN
|
||||||
|
BgkqhkiG9w0BAQsFAAOCAYEAcjstUc9k2kgwodhyh+R35p+SZRDGuWgBZVn7wHFS
|
||||||
|
4W7TuSyOw7RLHlGOtJ7SxzqKoOQmMaC0IhFN50znE9FocjcHOXMcRQpc/ql18Ohh
|
||||||
|
hVIqli5GnR3N+JRWGo234BZU73SPkskQnP4xw6gZKwwTiMjMltuY5KuRuCQpk4/w
|
||||||
|
Avn9k8fgrFidYu6PAYzx+r/JyVYISzLLoowgK8hK4bNWIujESvF87NxcNGwV/+j/
|
||||||
|
q1sI+yKplw35Jjvhg0UBNrRAbujBjIthL6rPLrqk1e/modoOE2shT/QelmkJUtOZ
|
||||||
|
aAZVMSQfKn6zsqypHqeBdJg8djDT9YqQZDu2l6yOQSiGb81pJxHyPYso17JkjvKA
|
||||||
|
SyluR0RtiTeib7VQCVIHjgcfFyQP1jBELk2Yq9HeYMg89M1U1AThI4JMCA6ukYVR
|
||||||
|
oktSBQJdPEQYu1Geve/UU04g5JzstBMrET9EsRlyfg2/B3o2/TbZIlRPbAIRdVhn
|
||||||
|
XsC+LWBb5waKWhq6Ti+6CNuv
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|||||||
@@ -213,7 +213,6 @@ size_flags_horizontal = 3
|
|||||||
theme_override_constants/separation = 0
|
theme_override_constants/separation = 0
|
||||||
|
|
||||||
[node name="ShortcutsPanel" type="VBoxContainer" parent="HSplitContainer/Content/ScrollContainer/ContentStack"]
|
[node name="ShortcutsPanel" type="VBoxContainer" parent="HSplitContainer/Content/ScrollContainer/ContentStack"]
|
||||||
visible = false
|
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_constants/separation = 20
|
theme_override_constants/separation = 20
|
||||||
|
|
||||||
@@ -304,6 +303,18 @@ theme = ExtResource("2_theme")
|
|||||||
theme_override_colors/font_color = Color(0.7, 0.7, 0.7, 1)
|
theme_override_colors/font_color = Color(0.7, 0.7, 0.7, 1)
|
||||||
text = "Focus address bar"
|
text = "Focus address bar"
|
||||||
|
|
||||||
|
[node name="Label6" type="Label" parent="HSplitContainer/Content/ScrollContainer/ContentStack/ShortcutsPanel/NavigationSection/VBoxContainer/GridContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme = ExtResource("2_theme")
|
||||||
|
theme_override_colors/font_color = Color(0.8, 0.8, 0.8, 1)
|
||||||
|
text = "Ctrl+R"
|
||||||
|
|
||||||
|
[node name="Description6" type="Label" parent="HSplitContainer/Content/ScrollContainer/ContentStack/ShortcutsPanel/NavigationSection/VBoxContainer/GridContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme = ExtResource("2_theme")
|
||||||
|
theme_override_colors/font_color = Color(0.7, 0.7, 0.7, 1)
|
||||||
|
text = "Refresh tab"
|
||||||
|
|
||||||
[node name="WindowSection" type="PanelContainer" parent="HSplitContainer/Content/ScrollContainer/ContentStack/ShortcutsPanel"]
|
[node name="WindowSection" type="PanelContainer" parent="HSplitContainer/Content/ScrollContainer/ContentStack/ShortcutsPanel"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_styles/panel = SubResource("StyleBoxFlat_section")
|
theme_override_styles/panel = SubResource("StyleBoxFlat_section")
|
||||||
@@ -461,6 +472,7 @@ text = "GURT is a content delivery protocol similar to HTTPS. It's the core of h
|
|||||||
autowrap_mode = 3
|
autowrap_mode = 3
|
||||||
|
|
||||||
[node name="ScriptingPanel" type="VBoxContainer" parent="HSplitContainer/Content/ScrollContainer/ContentStack"]
|
[node name="ScriptingPanel" type="VBoxContainer" parent="HSplitContainer/Content/ScrollContainer/ContentStack"]
|
||||||
|
visible = false
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_constants/separation = 20
|
theme_override_constants/separation = 20
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
[ext_resource type="Script" uid="uid://08xpof853sfh" path="res://Scripts/Tags/canvas.gd" id="1_canvas"]
|
[ext_resource type="Script" uid="uid://08xpof853sfh" path="res://Scripts/Tags/canvas.gd" id="1_canvas"]
|
||||||
|
|
||||||
[node name="canvas" type="ColorRect"]
|
[node name="canvas" type="ColorRect"]
|
||||||
custom_minimum_size = Vector2(300, 150)
|
custom_minimum_size = Vector2(0, 0)
|
||||||
offset_right = 300.0
|
offset_right = 300.0
|
||||||
offset_bottom = 150.0
|
offset_bottom = 150.0
|
||||||
size_flags_horizontal = 0
|
size_flags_horizontal = 0
|
||||||
|
|||||||
@@ -215,7 +215,8 @@ func get_element_styles_with_inheritance(element: HTMLElement, event: String = "
|
|||||||
if element in visited_elements:
|
if element in visited_elements:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
visited_elements.append(element)
|
var new_visited_for_styles = visited_elements.duplicate()
|
||||||
|
new_visited_for_styles.append(element)
|
||||||
|
|
||||||
var styles = {}
|
var styles = {}
|
||||||
|
|
||||||
@@ -403,7 +404,7 @@ func get_icon() -> String:
|
|||||||
var icon_element = find_first("icon")
|
var icon_element = find_first("icon")
|
||||||
return icon_element.get_attribute("src") if icon_element != null else ""
|
return icon_element.get_attribute("src") if icon_element != null else ""
|
||||||
|
|
||||||
func process_fonts() -> void:
|
func process_fonts(base_url: String = "") -> void:
|
||||||
var font_elements = find_all("font")
|
var font_elements = find_all("font")
|
||||||
|
|
||||||
for font_element in font_elements:
|
for font_element in font_elements:
|
||||||
@@ -412,7 +413,8 @@ func process_fonts() -> void:
|
|||||||
var weight = font_element.get_attribute("weight", "400")
|
var weight = font_element.get_attribute("weight", "400")
|
||||||
|
|
||||||
if name_str and src:
|
if name_str and src:
|
||||||
FontManager.register_font(name_str, src, weight)
|
var resolved_src = URLUtils.resolve_url(base_url, src)
|
||||||
|
FontManager.register_font(name_str, resolved_src, weight)
|
||||||
|
|
||||||
func get_meta_content(name_: String) -> String:
|
func get_meta_content(name_: String) -> String:
|
||||||
var meta_elements = find_all("meta", "name")
|
var meta_elements = find_all("meta", "name")
|
||||||
@@ -449,7 +451,7 @@ func process_scripts(lua_api: LuaAPI, _lua_vm) -> void:
|
|||||||
parse_result.external_scripts = []
|
parse_result.external_scripts = []
|
||||||
parse_result.external_scripts.append(src)
|
parse_result.external_scripts.append(src)
|
||||||
elif not inline_code.is_empty():
|
elif not inline_code.is_empty():
|
||||||
lua_api.execute_lua_script(inline_code)
|
lua_api.execute_lua_script(inline_code, "<inline script>")
|
||||||
|
|
||||||
func process_external_scripts(lua_api: LuaAPI, _lua_vm, base_url: String = "") -> void:
|
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():
|
if not lua_api or not parse_result.external_scripts or parse_result.external_scripts.is_empty():
|
||||||
@@ -460,7 +462,7 @@ func process_external_scripts(lua_api: LuaAPI, _lua_vm, base_url: String = "") -
|
|||||||
for script_url in parse_result.external_scripts:
|
for script_url in parse_result.external_scripts:
|
||||||
var script_content = await Network.fetch_external_resource(script_url, base_url)
|
var script_content = await Network.fetch_external_resource(script_url, base_url)
|
||||||
if not script_content.is_empty():
|
if not script_content.is_empty():
|
||||||
lua_api.execute_lua_script(script_content)
|
lua_api.execute_lua_script(script_content, script_url)
|
||||||
|
|
||||||
func process_postprocess() -> HTMLParser.HTMLElement:
|
func process_postprocess() -> HTMLParser.HTMLElement:
|
||||||
var postprocess_elements = find_all("postprocess")
|
var postprocess_elements = find_all("postprocess")
|
||||||
@@ -561,7 +563,7 @@ static func get_bbcode_with_styles(element: HTMLElement, styles: Dictionary, par
|
|||||||
for child in element.children:
|
for child in element.children:
|
||||||
var child_styles = styles
|
var child_styles = styles
|
||||||
if parser != null:
|
if parser != null:
|
||||||
child_styles = parser.get_element_styles_with_inheritance(child, "", new_visited)
|
child_styles = parser.get_element_styles_with_inheritance(child, "", [])
|
||||||
var child_content = HTMLParser.get_bbcode_with_styles(child, child_styles, parser, new_visited)
|
var child_content = HTMLParser.get_bbcode_with_styles(child, child_styles, parser, new_visited)
|
||||||
child_content = apply_element_bbcode_formatting(child, child_styles, child_content)
|
child_content = apply_element_bbcode_formatting(child, child_styles, child_content)
|
||||||
text += child_content
|
text += child_content
|
||||||
|
|||||||
@@ -359,7 +359,8 @@ func _on_gui_input_click(event: InputEvent, subscription: EventSubscription) ->
|
|||||||
if event is InputEventMouseButton:
|
if event is InputEventMouseButton:
|
||||||
var mouse_event = event as InputEventMouseButton
|
var mouse_event = event as InputEventMouseButton
|
||||||
if mouse_event.button_index == MOUSE_BUTTON_LEFT and mouse_event.pressed:
|
if mouse_event.button_index == MOUSE_BUTTON_LEFT and mouse_event.pressed:
|
||||||
_execute_lua_callback(subscription)
|
var mouse_info = _get_element_relative_mouse_position(mouse_event, subscription.element_id)
|
||||||
|
_execute_lua_callback(subscription, [mouse_info])
|
||||||
|
|
||||||
func _on_gui_input_mouse_universal(event: InputEvent, signal_node: Node) -> void:
|
func _on_gui_input_mouse_universal(event: InputEvent, signal_node: Node) -> void:
|
||||||
if event is InputEventMouseButton:
|
if event is InputEventMouseButton:
|
||||||
@@ -376,7 +377,8 @@ func _on_gui_input_mouse_universal(event: InputEvent, signal_node: Node) -> void
|
|||||||
should_trigger = true
|
should_trigger = true
|
||||||
|
|
||||||
if should_trigger:
|
if should_trigger:
|
||||||
_execute_lua_callback(subscription)
|
var mouse_info = _get_element_relative_mouse_position(mouse_event, subscription.element_id)
|
||||||
|
_execute_lua_callback(subscription, [mouse_info])
|
||||||
|
|
||||||
# Event callback handlers
|
# Event callback handlers
|
||||||
func _on_gui_input_mousemove(event: InputEvent, subscription: EventSubscription) -> void:
|
func _on_gui_input_mousemove(event: InputEvent, subscription: EventSubscription) -> void:
|
||||||
@@ -441,6 +443,31 @@ func _input(event: InputEvent) -> void:
|
|||||||
}
|
}
|
||||||
_execute_lua_callback(subscription, [key_info])
|
_execute_lua_callback(subscription, [key_info])
|
||||||
|
|
||||||
|
elif event is InputEventMouseButton:
|
||||||
|
var mouse_event = event as InputEventMouseButton
|
||||||
|
for subscription_id in event_subscriptions:
|
||||||
|
var subscription = event_subscriptions[subscription_id]
|
||||||
|
if subscription.element_id == "body" and subscription.connected_signal == "input":
|
||||||
|
var should_trigger = false
|
||||||
|
match subscription.event_name:
|
||||||
|
"mousedown":
|
||||||
|
should_trigger = mouse_event.pressed
|
||||||
|
"mouseup":
|
||||||
|
should_trigger = not mouse_event.pressed
|
||||||
|
|
||||||
|
if should_trigger:
|
||||||
|
var mouse_info = {"x": 0, "y": 0, "button": mouse_event.button_index}
|
||||||
|
var body_container = _get_body_container()
|
||||||
|
|
||||||
|
if body_container:
|
||||||
|
var control = body_container as Control
|
||||||
|
var global_pos = mouse_event.global_position
|
||||||
|
var element_rect = control.get_global_rect()
|
||||||
|
mouse_info["x"] = global_pos.x - element_rect.position.x
|
||||||
|
mouse_info["y"] = global_pos.y - element_rect.position.y
|
||||||
|
|
||||||
|
_execute_lua_callback(subscription, [mouse_info])
|
||||||
|
|
||||||
elif event is InputEventMouseMotion:
|
elif event is InputEventMouseMotion:
|
||||||
var mouse_event = event as InputEventMouseMotion
|
var mouse_event = event as InputEventMouseMotion
|
||||||
for subscription_id in event_subscriptions:
|
for subscription_id in event_subscriptions:
|
||||||
@@ -449,29 +476,67 @@ func _input(event: InputEvent) -> void:
|
|||||||
if subscription.event_name == "mousemove":
|
if subscription.event_name == "mousemove":
|
||||||
_handle_mousemove_event(mouse_event, subscription)
|
_handle_mousemove_event(mouse_event, subscription)
|
||||||
|
|
||||||
func _handle_mousemove_event(mouse_event: InputEventMouseMotion, subscription: EventSubscription) -> void:
|
func _get_element_relative_mouse_position(mouse_event: InputEvent, element_id: String) -> Dictionary:
|
||||||
# TODO: pass reference instead of hardcoded path
|
var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null)
|
||||||
var body_container = Engine.get_main_loop().current_scene.website_container
|
if not dom_node or not dom_node is Control:
|
||||||
|
return {"x": 0, "y": 0}
|
||||||
if body_container.get_parent() is MarginContainer:
|
|
||||||
body_container = body_container.get_parent()
|
|
||||||
|
|
||||||
|
var control = dom_node as Control
|
||||||
|
var global_pos: Vector2
|
||||||
|
|
||||||
|
if mouse_event is InputEventMouseButton:
|
||||||
|
global_pos = (mouse_event as InputEventMouseButton).global_position
|
||||||
|
elif mouse_event is InputEventMouseMotion:
|
||||||
|
global_pos = (mouse_event as InputEventMouseMotion).global_position
|
||||||
|
else:
|
||||||
|
return {"x": 0, "y": 0}
|
||||||
|
|
||||||
|
var element_rect = control.get_global_rect()
|
||||||
|
var local_x = global_pos.x - element_rect.position.x
|
||||||
|
var local_y = global_pos.y - element_rect.position.y
|
||||||
|
|
||||||
|
return {
|
||||||
|
"x": local_x,
|
||||||
|
"y": local_y
|
||||||
|
}
|
||||||
|
|
||||||
|
func _handle_mousemove_event(mouse_event: InputEventMouseMotion, subscription: EventSubscription) -> void:
|
||||||
|
var body_container = _get_body_container()
|
||||||
if not body_container:
|
if not body_container:
|
||||||
return
|
return
|
||||||
|
|
||||||
var container_rect = body_container.get_global_rect()
|
var control = body_container as Control
|
||||||
var local_x = mouse_event.global_position.x - container_rect.position.x
|
var global_pos = mouse_event.global_position
|
||||||
var local_y = mouse_event.global_position.y - container_rect.position.y
|
var element_rect = control.get_global_rect()
|
||||||
|
var local_x = global_pos.x - element_rect.position.x
|
||||||
|
var local_y = global_pos.y - element_rect.position.y
|
||||||
|
|
||||||
# Only provide coordinates if mouse is within the container bounds
|
var mouse_info = {
|
||||||
if local_x >= 0 and local_y >= 0 and local_x <= container_rect.size.x and local_y <= container_rect.size.y:
|
"x": local_x,
|
||||||
var mouse_info = {
|
"y": local_y,
|
||||||
"x": local_x,
|
"deltaX": mouse_event.relative.x,
|
||||||
"y": local_y,
|
"deltaY": mouse_event.relative.y
|
||||||
"deltaX": mouse_event.relative.x,
|
}
|
||||||
"deltaY": mouse_event.relative.y
|
_execute_lua_callback(subscription, [mouse_info])
|
||||||
}
|
|
||||||
_execute_lua_callback(subscription, [mouse_info])
|
func _get_body_container() -> Control:
|
||||||
|
# Try to get body from DOM registry first
|
||||||
|
var body_container = dom_parser.parse_result.dom_nodes.get("body", null)
|
||||||
|
|
||||||
|
# We fallback to finding the active website container, as it seems theres a bug where body can be null in this context
|
||||||
|
if not body_container:
|
||||||
|
var main_scene = Engine.get_main_loop().current_scene
|
||||||
|
if main_scene and main_scene.has_method("get_active_website_container"):
|
||||||
|
body_container = main_scene.get_active_website_container()
|
||||||
|
else:
|
||||||
|
body_container = Engine.get_main_loop().current_scene.website_container
|
||||||
|
if body_container and body_container.get_parent() is MarginContainer:
|
||||||
|
body_container = body_container.get_parent()
|
||||||
|
|
||||||
|
if body_container and body_container is Control:
|
||||||
|
return body_container as Control
|
||||||
|
|
||||||
|
return null
|
||||||
|
|
||||||
# Input event handlers
|
# Input event handlers
|
||||||
func _on_input_text_changed(new_text: String, subscription: EventSubscription) -> void:
|
func _on_input_text_changed(new_text: String, subscription: EventSubscription) -> void:
|
||||||
@@ -634,13 +699,13 @@ func get_dom_node(node: Node, purpose: String = "general") -> Node:
|
|||||||
return node
|
return node
|
||||||
|
|
||||||
# Main execution function
|
# Main execution function
|
||||||
func execute_lua_script(code: String):
|
func execute_lua_script(code: String, chunk_name: String = "dostring"):
|
||||||
if not threaded_vm.lua_thread or not threaded_vm.lua_thread.is_alive():
|
if not threaded_vm.lua_thread or not threaded_vm.lua_thread.is_alive():
|
||||||
# Start the thread if it's not running
|
# Start the thread if it's not running
|
||||||
threaded_vm.start_lua_thread(dom_parser, self)
|
threaded_vm.start_lua_thread(dom_parser, self)
|
||||||
|
|
||||||
script_start_time = Time.get_ticks_msec() / 1000.0
|
script_start_time = Time.get_ticks_msec() / 1000.0
|
||||||
threaded_vm.execute_script_async(code)
|
threaded_vm.execute_script_async(code, chunk_name)
|
||||||
|
|
||||||
func _on_threaded_script_completed(_result: Dictionary):
|
func _on_threaded_script_completed(_result: Dictionary):
|
||||||
pass
|
pass
|
||||||
@@ -649,7 +714,14 @@ func _on_threaded_script_error(error_message: String):
|
|||||||
Trace.trace_error("RuntimeError: " + error_message)
|
Trace.trace_error("RuntimeError: " + error_message)
|
||||||
|
|
||||||
func _on_print_output(message: Dictionary):
|
func _on_print_output(message: Dictionary):
|
||||||
Trace.get_instance().log_message.emit(message, "lua", Time.get_ticks_msec() / 1000.0)
|
var message_strings: Array[String] = []
|
||||||
|
for part in message.parts:
|
||||||
|
if part.type == "table":
|
||||||
|
message_strings.append(str(part.data))
|
||||||
|
else:
|
||||||
|
message_strings.append(part.data)
|
||||||
|
var formatted_message = "\t".join(message_strings)
|
||||||
|
Trace.get_instance().log_message.emit(formatted_message, "lua", Time.get_ticks_msec() / 1000.0)
|
||||||
|
|
||||||
func kill_script_execution():
|
func kill_script_execution():
|
||||||
threaded_vm.stop_lua_thread()
|
threaded_vm.stop_lua_thread()
|
||||||
@@ -1003,3 +1075,31 @@ func _handle_download_request(operation: Dictionary):
|
|||||||
|
|
||||||
var main_node = Engine.get_main_loop().current_scene
|
var main_node = Engine.get_main_loop().current_scene
|
||||||
main_node.download_manager.handle_download_request(download_data)
|
main_node.download_manager.handle_download_request(download_data)
|
||||||
|
|
||||||
|
func _get_element_size_sync(result: Array, element_id: String):
|
||||||
|
var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null)
|
||||||
|
if dom_node and dom_node is Control:
|
||||||
|
var control = dom_node as Control
|
||||||
|
result[0] = control.size.x
|
||||||
|
result[1] = control.size.y
|
||||||
|
result[2] = true # completion flag
|
||||||
|
return
|
||||||
|
|
||||||
|
# Fallback
|
||||||
|
result[0] = 0.0
|
||||||
|
result[1] = 0.0
|
||||||
|
result[2] = true # completion flag
|
||||||
|
|
||||||
|
func _get_element_position_sync(result: Array, element_id: String):
|
||||||
|
var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null)
|
||||||
|
if dom_node and dom_node is Control:
|
||||||
|
var control = dom_node as Control
|
||||||
|
result[0] = control.position.x
|
||||||
|
result[1] = control.position.y
|
||||||
|
result[2] = true # completion flag
|
||||||
|
return
|
||||||
|
|
||||||
|
# Fallback
|
||||||
|
result[0] = 0.0
|
||||||
|
result[1] = 0.0
|
||||||
|
result[2] = true # completion flag
|
||||||
|
|||||||
@@ -245,9 +245,9 @@ func execute_lua_command(code: String) -> void:
|
|||||||
var is_expression = is_likely_expression(code)
|
var is_expression = is_likely_expression(code)
|
||||||
if is_expression:
|
if is_expression:
|
||||||
var wrapped_code = "print(" + code + ")"
|
var wrapped_code = "print(" + code + ")"
|
||||||
lua_api.execute_lua_script(wrapped_code)
|
lua_api.execute_lua_script(wrapped_code, "<console>")
|
||||||
else:
|
else:
|
||||||
lua_api.execute_lua_script(code)
|
lua_api.execute_lua_script(code, "<console>")
|
||||||
return
|
return
|
||||||
|
|
||||||
add_log_entry("No Lua context available", "error", Time.get_ticks_msec() / 1000.0)
|
add_log_entry("No Lua context available", "error", Time.get_ticks_msec() / 1000.0)
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ func _exit_tree():
|
|||||||
|
|
||||||
if background_panel and is_instance_valid(background_panel):
|
if background_panel and is_instance_valid(background_panel):
|
||||||
if background_panel.get_parent():
|
if background_panel.get_parent():
|
||||||
background_panel.get_parent().remove_child(background_panel)
|
background_panel.get_parent().remove_child.call_deferred(background_panel)
|
||||||
background_panel.queue_free()
|
background_panel.queue_free()
|
||||||
|
|
||||||
if dev_tools and is_instance_valid(dev_tools):
|
if dev_tools and is_instance_valid(dev_tools):
|
||||||
|
|||||||
@@ -249,6 +249,8 @@ func _input(_event: InputEvent) -> void:
|
|||||||
if Input.is_action_just_pressed("FocusSearch"):
|
if Input.is_action_just_pressed("FocusSearch"):
|
||||||
main.search_bar.grab_focus()
|
main.search_bar.grab_focus()
|
||||||
main.search_bar.select_all()
|
main.search_bar.select_all()
|
||||||
|
if Input.is_action_just_pressed("ReloadPage"):
|
||||||
|
main.reload_current_page()
|
||||||
|
|
||||||
func _on_new_tab_button_pressed() -> void:
|
func _on_new_tab_button_pressed() -> void:
|
||||||
create_tab()
|
create_tab()
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ static func load_font(font_info: Dictionary) -> void:
|
|||||||
|
|
||||||
if src.begins_with("http://") or src.begins_with("https://"):
|
if src.begins_with("http://") or src.begins_with("https://"):
|
||||||
load_web_font(font_info)
|
load_web_font(font_info)
|
||||||
|
elif src.begins_with("gurt://"):
|
||||||
|
load_gurt_font(font_info)
|
||||||
|
else:
|
||||||
|
load_local_font(font_info)
|
||||||
|
|
||||||
static func load_web_font(font_info: Dictionary) -> void:
|
static func load_web_font(font_info: Dictionary) -> void:
|
||||||
var src = font_info["src"]
|
var src = font_info["src"]
|
||||||
@@ -48,13 +52,8 @@ static func load_web_font(font_info: Dictionary) -> void:
|
|||||||
font_info["font_resource"] = font
|
font_info["font_resource"] = font
|
||||||
loaded_fonts[name] = font
|
loaded_fonts[name] = font
|
||||||
|
|
||||||
# Trigger font refresh if callback is available
|
|
||||||
if refresh_callback.is_valid():
|
if refresh_callback.is_valid():
|
||||||
refresh_callback.call(name)
|
refresh_callback.call(name)
|
||||||
else:
|
|
||||||
print("FontManager: Empty font data received for ", name)
|
|
||||||
else:
|
|
||||||
print("FontManager: Failed to load font ", name, " - HTTP ", response_code)
|
|
||||||
|
|
||||||
if is_instance_valid(temp_parent):
|
if is_instance_valid(temp_parent):
|
||||||
temp_parent.queue_free()
|
temp_parent.queue_free()
|
||||||
@@ -65,6 +64,50 @@ static func load_web_font(font_info: Dictionary) -> void:
|
|||||||
|
|
||||||
http_request.request(src, headers)
|
http_request.request(src, headers)
|
||||||
|
|
||||||
|
static func load_local_font(font_info: Dictionary) -> void:
|
||||||
|
var src = font_info["src"]
|
||||||
|
var name = font_info["name"]
|
||||||
|
|
||||||
|
if not FileAccess.file_exists(src):
|
||||||
|
return
|
||||||
|
|
||||||
|
var file = FileAccess.open(src, FileAccess.READ)
|
||||||
|
if file == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
var font_data = file.get_buffer(file.get_length())
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
if font_data.size() == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
var font = FontFile.new()
|
||||||
|
font.data = font_data
|
||||||
|
|
||||||
|
font_info["font_resource"] = font
|
||||||
|
loaded_fonts[name] = font
|
||||||
|
|
||||||
|
if refresh_callback.is_valid():
|
||||||
|
refresh_callback.call(name)
|
||||||
|
|
||||||
|
static func load_gurt_font(font_info: Dictionary) -> void:
|
||||||
|
var src = font_info["src"]
|
||||||
|
var name = font_info["name"]
|
||||||
|
|
||||||
|
var font_data = Network.fetch_gurt_resource(src, true)
|
||||||
|
|
||||||
|
if font_data.size() == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
var font = FontFile.new()
|
||||||
|
font.data = font_data
|
||||||
|
|
||||||
|
font_info["font_resource"] = font
|
||||||
|
loaded_fonts[name] = font
|
||||||
|
|
||||||
|
if refresh_callback.is_valid():
|
||||||
|
refresh_callback.call(name)
|
||||||
|
|
||||||
static func get_font(family_name: String) -> Font:
|
static func get_font(family_name: String) -> Font:
|
||||||
if family_name == "sans-serif":
|
if family_name == "sans-serif":
|
||||||
var sys_font = SystemFont.new()
|
var sys_font = SystemFont.new()
|
||||||
|
|||||||
@@ -68,13 +68,18 @@ static func apply_element_styles(node: Control, element: HTMLParser.HTMLElement,
|
|||||||
if width == "100%":
|
if width == "100%":
|
||||||
node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
node.custom_minimum_size.x = 0
|
node.custom_minimum_size.x = 0
|
||||||
|
if node is PanelContainer and node.get_child_count() > 0:
|
||||||
|
var vbox = node.get_child(0)
|
||||||
|
if vbox is VBoxContainer:
|
||||||
|
vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
node.set_meta("size_flags_set_by_style_manager", true)
|
||||||
else:
|
else:
|
||||||
# For other percentages, convert to viewport-relative size
|
# For other percentages, convert to viewport-relative size
|
||||||
var percent = float(width.replace("%", "")) / 100.0
|
var percent = float(width.replace("%", "")) / 100.0
|
||||||
var viewport_width = node.get_viewport().get_visible_rect().size.x if node.get_viewport() else 800
|
var viewport_width = node.get_viewport().get_visible_rect().size.x if node.get_viewport() else 800
|
||||||
node.custom_minimum_size.x = viewport_width * percent
|
node.custom_minimum_size.x = viewport_width * percent
|
||||||
node.set_meta("size_flags_set_by_style_manager", true)
|
node.set_meta("size_flags_set_by_style_manager", true)
|
||||||
node.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
node.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||||
else:
|
else:
|
||||||
node.custom_minimum_size.x = width
|
node.custom_minimum_size.x = width
|
||||||
node.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
node.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||||
@@ -402,7 +407,7 @@ static func apply_margin_styles_to_container(margin_container: MarginContainer,
|
|||||||
if margin_val != null:
|
if margin_val != null:
|
||||||
margin_container.add_theme_constant_override(theme_key, margin_val)
|
margin_container.add_theme_constant_override(theme_key, margin_val)
|
||||||
|
|
||||||
static func apply_styles_to_label(label: Control, styles: Dictionary, element: HTMLParser.HTMLElement, parser, text_override: String = "") -> void:
|
static func apply_styles_to_label(label: Control, styles: Dictionary, element: HTMLParser.HTMLElement, parser, text_override: String = "", is_refresh: bool = false) -> void:
|
||||||
if label is Button:
|
if label is Button:
|
||||||
apply_font_to_button(label, styles)
|
apply_font_to_button(label, styles)
|
||||||
return
|
return
|
||||||
@@ -410,6 +415,10 @@ static func apply_styles_to_label(label: Control, styles: Dictionary, element: H
|
|||||||
if not label is RichTextLabel:
|
if not label is RichTextLabel:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not is_refresh and styles.has("font-family") and styles["font-family"] not in ["sans-serif", "serif", "monospace"]:
|
||||||
|
var main_node = Engine.get_main_loop().current_scene
|
||||||
|
main_node.register_font_dependent_element(label, styles, element, parser)
|
||||||
|
|
||||||
var text = text_override if text_override != "" else (element.get_preserved_text() if element.tag_name == "pre" else element.get_bbcode_formatted_text(parser))
|
var text = text_override if text_override != "" else (element.get_preserved_text() if element.tag_name == "pre" else element.get_bbcode_formatted_text(parser))
|
||||||
|
|
||||||
var font_size = 24 # default
|
var font_size = 24 # default
|
||||||
@@ -418,15 +427,15 @@ static func apply_styles_to_label(label: Control, styles: Dictionary, element: H
|
|||||||
var font_family = styles["font-family"]
|
var font_family = styles["font-family"]
|
||||||
var font_resource = FontManager.get_font(font_family)
|
var font_resource = FontManager.get_font(font_family)
|
||||||
|
|
||||||
# set a sans-serif fallback first
|
|
||||||
if font_family not in ["sans-serif", "serif", "monospace"]:
|
if font_family not in ["sans-serif", "serif", "monospace"]:
|
||||||
if not FontManager.loaded_fonts.has(font_family):
|
if FontManager.loaded_fonts.has(font_family) and font_resource:
|
||||||
# Font not loaded yet, use sans-serif as fallback
|
apply_font_to_label(label, font_resource, styles)
|
||||||
|
else:
|
||||||
var fallback_font = FontManager.get_font("sans-serif")
|
var fallback_font = FontManager.get_font("sans-serif")
|
||||||
apply_font_to_label(label, fallback_font, styles)
|
apply_font_to_label(label, fallback_font, styles)
|
||||||
|
else:
|
||||||
if font_resource:
|
if font_resource:
|
||||||
apply_font_to_label(label, font_resource, styles)
|
apply_font_to_label(label, font_resource, styles)
|
||||||
else:
|
else:
|
||||||
# No custom font family, but check if we need to apply font weight
|
# No custom font family, but check if we need to apply font weight
|
||||||
if styles.has("font-thin") or styles.has("font-extralight") or styles.has("font-light") or styles.has("font-normal") or styles.has("font-medium") or styles.has("font-semibold") or styles.has("font-extrabold") or styles.has("font-black"):
|
if styles.has("font-thin") or styles.has("font-extralight") or styles.has("font-light") or styles.has("font-normal") or styles.has("font-medium") or styles.has("font-semibold") or styles.has("font-extrabold") or styles.has("font-black"):
|
||||||
@@ -567,50 +576,64 @@ static func parse_radius(radius_str: String) -> int:
|
|||||||
return SizeUtils.parse_radius(radius_str)
|
return SizeUtils.parse_radius(radius_str)
|
||||||
|
|
||||||
static func apply_font_to_label(label: RichTextLabel, font_resource: Font, styles: Dictionary = {}) -> void:
|
static func apply_font_to_label(label: RichTextLabel, font_resource: Font, styles: Dictionary = {}) -> void:
|
||||||
# Create normal font with appropriate weight
|
if font_resource is FontFile:
|
||||||
var normal_font = SystemFont.new()
|
label.add_theme_font_override("normal_font", font_resource)
|
||||||
normal_font.font_names = font_resource.font_names if font_resource is SystemFont else ["Arial"]
|
label.add_theme_font_override("bold_font", font_resource)
|
||||||
|
label.add_theme_font_override("italics_font", font_resource)
|
||||||
|
|
||||||
# Set weight based on styles
|
elif font_resource is SystemFont:
|
||||||
var font_weight = 400 # Default normal weight
|
var font_weight = 400
|
||||||
if styles.has("font-thin"):
|
if styles.has("font-thin"):
|
||||||
font_weight = 100
|
font_weight = 100
|
||||||
elif styles.has("font-extralight"):
|
elif styles.has("font-extralight"):
|
||||||
font_weight = 200
|
font_weight = 200
|
||||||
elif styles.has("font-light"):
|
elif styles.has("font-light"):
|
||||||
font_weight = 300
|
font_weight = 300
|
||||||
elif styles.has("font-normal"):
|
elif styles.has("font-normal"):
|
||||||
font_weight = 400
|
font_weight = 400
|
||||||
elif styles.has("font-medium"):
|
elif styles.has("font-medium"):
|
||||||
font_weight = 500
|
font_weight = 500
|
||||||
elif styles.has("font-semibold"):
|
elif styles.has("font-semibold"):
|
||||||
font_weight = 600
|
font_weight = 600
|
||||||
elif styles.has("font-bold"):
|
elif styles.has("font-bold"):
|
||||||
font_weight = 700
|
font_weight = 700
|
||||||
elif styles.has("font-extrabold"):
|
elif styles.has("font-extrabold"):
|
||||||
font_weight = 800
|
font_weight = 800
|
||||||
elif styles.has("font-black"):
|
elif styles.has("font-black"):
|
||||||
font_weight = 900
|
font_weight = 900
|
||||||
|
|
||||||
|
var normal_font = SystemFont.new()
|
||||||
|
normal_font.font_names = font_resource.font_names
|
||||||
|
normal_font.font_weight = font_weight
|
||||||
|
label.add_theme_font_override("normal_font", normal_font)
|
||||||
|
|
||||||
|
# Create bold variant
|
||||||
|
var bold_font = SystemFont.new()
|
||||||
|
bold_font.font_names = font_resource.font_names
|
||||||
|
bold_font.font_weight = 700
|
||||||
|
label.add_theme_font_override("bold_font", bold_font)
|
||||||
|
|
||||||
|
# Create italic variant
|
||||||
|
var italic_font = SystemFont.new()
|
||||||
|
italic_font.font_names = font_resource.font_names
|
||||||
|
italic_font.font_italic = true
|
||||||
|
italic_font.font_weight = font_weight
|
||||||
|
label.add_theme_font_override("italics_font", italic_font)
|
||||||
|
|
||||||
|
else:
|
||||||
|
label.add_theme_font_override("normal_font", font_resource)
|
||||||
|
label.add_theme_font_override("bold_font", font_resource)
|
||||||
|
label.add_theme_font_override("italics_font", font_resource)
|
||||||
|
|
||||||
normal_font.font_weight = font_weight
|
# Handle bold_italics_font
|
||||||
|
if font_resource is FontFile:
|
||||||
label.add_theme_font_override("normal_font", normal_font)
|
label.add_theme_font_override("bold_italics_font", font_resource)
|
||||||
|
elif font_resource is SystemFont:
|
||||||
var bold_font = SystemFont.new()
|
var bold_italic_font = SystemFont.new()
|
||||||
bold_font.font_names = font_resource.font_names if font_resource is SystemFont else ["Arial"]
|
bold_italic_font.font_names = font_resource.font_names
|
||||||
bold_font.font_weight = 700 # Bold weight
|
bold_italic_font.font_weight = 700
|
||||||
label.add_theme_font_override("bold_font", bold_font)
|
bold_italic_font.font_italic = true
|
||||||
|
label.add_theme_font_override("bold_italics_font", bold_italic_font)
|
||||||
var italic_font = SystemFont.new()
|
|
||||||
italic_font.font_names = font_resource.font_names if font_resource is SystemFont else ["Arial"]
|
|
||||||
italic_font.font_italic = true
|
|
||||||
label.add_theme_font_override("italics_font", italic_font)
|
|
||||||
|
|
||||||
var bold_italic_font = SystemFont.new()
|
|
||||||
bold_italic_font.font_names = font_resource.font_names if font_resource is SystemFont else ["Arial"]
|
|
||||||
bold_italic_font.font_weight = 700 # Bold weight
|
|
||||||
bold_italic_font.font_italic = true
|
|
||||||
label.add_theme_font_override("bold_italics_font", bold_italic_font)
|
|
||||||
|
|
||||||
static func apply_font_to_button(button: Button, styles: Dictionary) -> void:
|
static func apply_font_to_button(button: Button, styles: Dictionary) -> void:
|
||||||
if styles.has("font-family"):
|
if styles.has("font-family"):
|
||||||
|
|||||||
@@ -142,9 +142,9 @@ func fetch_external_resource(url: String, base_url: String = "") -> String:
|
|||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
func fetch_gurt_resource(url: String) -> String:
|
func fetch_gurt_resource(url: String, as_binary: bool = false):
|
||||||
if not GurtProtocol.is_gurt_domain(url):
|
if not GurtProtocol.is_gurt_domain(url):
|
||||||
return ""
|
return PackedByteArray() if as_binary else ""
|
||||||
|
|
||||||
var gurt_url = url
|
var gurt_url = url
|
||||||
if not gurt_url.begins_with("gurt://"):
|
if not gurt_url.begins_with("gurt://"):
|
||||||
@@ -184,11 +184,17 @@ func fetch_gurt_resource(url: String) -> String:
|
|||||||
status_code = response.status_code
|
status_code = response.status_code
|
||||||
error_msg += ": " + str(response.status_code) + " " + response.status_message
|
error_msg += ": " + str(response.status_code) + " " + response.status_message
|
||||||
NetworkManager.complete_request(network_request.id, status_code, error_msg, {}, "")
|
NetworkManager.complete_request(network_request.id, status_code, error_msg, {}, "")
|
||||||
return ""
|
return PackedByteArray() if as_binary else ""
|
||||||
|
|
||||||
var response_headers = response.headers if response.headers else {}
|
var response_headers = response.headers if response.headers else {}
|
||||||
var response_body = response.body.get_string_from_utf8()
|
|
||||||
|
|
||||||
NetworkManager.complete_request(network_request.id, response.status_code, "OK", response_headers, response_body)
|
var response_body = response.body
|
||||||
|
|
||||||
return response_body
|
if as_binary:
|
||||||
|
var size_info = "Binary data: " + str(response_body.size()) + " bytes"
|
||||||
|
NetworkManager.complete_request(network_request.id, response.status_code, "OK", response_headers, size_info, response_body)
|
||||||
|
return response_body
|
||||||
|
else:
|
||||||
|
var response_body_str = response_body.get_string_from_utf8()
|
||||||
|
NetworkManager.complete_request(network_request.id, response.status_code, "OK", response_headers, response_body_str)
|
||||||
|
return response_body_str
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ var canvas_height: int = 150
|
|||||||
var draw_commands: Array = []
|
var draw_commands: Array = []
|
||||||
var context_2d: CanvasContext2D = null
|
var context_2d: CanvasContext2D = null
|
||||||
var context_shader: CanvasContextShader = null
|
var context_shader: CanvasContextShader = null
|
||||||
|
var pending_redraw: bool = false
|
||||||
|
var max_draw_commands: int = 1000
|
||||||
|
|
||||||
class CanvasContext2D:
|
class CanvasContext2D:
|
||||||
var canvas: HTMLCanvas
|
var canvas: HTMLCanvas
|
||||||
@@ -41,8 +43,7 @@ class CanvasContext2D:
|
|||||||
"color": color,
|
"color": color,
|
||||||
"transform": current_transform
|
"transform": current_transform
|
||||||
}
|
}
|
||||||
canvas.draw_commands.append(cmd)
|
canvas._add_draw_command(cmd)
|
||||||
canvas.queue_redraw()
|
|
||||||
|
|
||||||
func strokeRect(x: float, y: float, width: float, height: float, color_hex: String = "", stroke_width: float = 0.0):
|
func strokeRect(x: float, y: float, width: float, height: float, color_hex: String = "", stroke_width: float = 0.0):
|
||||||
var color = _parse_color(stroke_style if color_hex.is_empty() else color_hex)
|
var color = _parse_color(stroke_style if color_hex.is_empty() else color_hex)
|
||||||
@@ -57,10 +58,14 @@ class CanvasContext2D:
|
|||||||
"stroke_width": width_val,
|
"stroke_width": width_val,
|
||||||
"transform": current_transform
|
"transform": current_transform
|
||||||
}
|
}
|
||||||
canvas.draw_commands.append(cmd)
|
canvas._add_draw_command(cmd)
|
||||||
canvas.queue_redraw()
|
|
||||||
|
|
||||||
func clearRect(x: float, y: float, width: float, height: float):
|
func clearRect(x: float, y: float, width: float, height: float):
|
||||||
|
if x == 0 and y == 0 and width >= canvas.canvas_width and height >= canvas.canvas_height:
|
||||||
|
canvas.draw_commands.clear()
|
||||||
|
canvas._do_redraw()
|
||||||
|
return
|
||||||
|
|
||||||
var cmd = {
|
var cmd = {
|
||||||
"type": "clearRect",
|
"type": "clearRect",
|
||||||
"x": x,
|
"x": x,
|
||||||
@@ -68,8 +73,7 @@ class CanvasContext2D:
|
|||||||
"width": width,
|
"width": width,
|
||||||
"height": height
|
"height": height
|
||||||
}
|
}
|
||||||
canvas.draw_commands.append(cmd)
|
canvas._add_draw_command(cmd)
|
||||||
canvas.queue_redraw()
|
|
||||||
|
|
||||||
func drawCircle(x: float, y: float, radius: float, color_hex: String = "#000000", filled: bool = true):
|
func drawCircle(x: float, y: float, radius: float, color_hex: String = "#000000", filled: bool = true):
|
||||||
var cmd = {
|
var cmd = {
|
||||||
@@ -81,8 +85,7 @@ class CanvasContext2D:
|
|||||||
"filled": filled,
|
"filled": filled,
|
||||||
"transform": current_transform
|
"transform": current_transform
|
||||||
}
|
}
|
||||||
canvas.draw_commands.append(cmd)
|
canvas._add_draw_command(cmd)
|
||||||
canvas.queue_redraw()
|
|
||||||
|
|
||||||
func drawText(x: float, y: float, text: String, color_hex: String = "#000000"):
|
func drawText(x: float, y: float, text: String, color_hex: String = "#000000"):
|
||||||
var color = _parse_color(fill_style if color_hex == "#000000" else color_hex)
|
var color = _parse_color(fill_style if color_hex == "#000000" else color_hex)
|
||||||
@@ -95,8 +98,7 @@ class CanvasContext2D:
|
|||||||
"font_size": _parse_font_size(font),
|
"font_size": _parse_font_size(font),
|
||||||
"transform": current_transform
|
"transform": current_transform
|
||||||
}
|
}
|
||||||
canvas.draw_commands.append(cmd)
|
canvas._add_draw_command(cmd)
|
||||||
canvas.queue_redraw()
|
|
||||||
|
|
||||||
# Path-based drawing functions
|
# Path-based drawing functions
|
||||||
func beginPath():
|
func beginPath():
|
||||||
@@ -149,8 +151,7 @@ class CanvasContext2D:
|
|||||||
"line_cap": line_cap,
|
"line_cap": line_cap,
|
||||||
"line_join": line_join
|
"line_join": line_join
|
||||||
}
|
}
|
||||||
canvas.draw_commands.append(cmd)
|
canvas._add_draw_command(cmd)
|
||||||
canvas.queue_redraw()
|
|
||||||
|
|
||||||
func fill():
|
func fill():
|
||||||
if current_path.size() < 3:
|
if current_path.size() < 3:
|
||||||
@@ -161,8 +162,7 @@ class CanvasContext2D:
|
|||||||
"path": current_path.duplicate(),
|
"path": current_path.duplicate(),
|
||||||
"color": _parse_color(fill_style)
|
"color": _parse_color(fill_style)
|
||||||
}
|
}
|
||||||
canvas.draw_commands.append(cmd)
|
canvas._add_draw_command(cmd)
|
||||||
canvas.queue_redraw()
|
|
||||||
|
|
||||||
# Transformation functions
|
# Transformation functions
|
||||||
func save():
|
func save():
|
||||||
@@ -328,6 +328,10 @@ func withContext(context_type: String):
|
|||||||
func _draw():
|
func _draw():
|
||||||
draw_rect(Rect2(Vector2.ZERO, size), Color.TRANSPARENT)
|
draw_rect(Rect2(Vector2.ZERO, size), Color.TRANSPARENT)
|
||||||
|
|
||||||
|
# Skip if too many commands to prevent frame drops
|
||||||
|
if draw_commands.size() > max_draw_commands * 2:
|
||||||
|
return
|
||||||
|
|
||||||
for cmd in draw_commands:
|
for cmd in draw_commands:
|
||||||
match cmd.type:
|
match cmd.type:
|
||||||
"fillRect":
|
"fillRect":
|
||||||
@@ -407,6 +411,31 @@ func _draw():
|
|||||||
if path.size() > 2:
|
if path.size() > 2:
|
||||||
draw_colored_polygon(path, clr)
|
draw_colored_polygon(path, clr)
|
||||||
|
|
||||||
|
func _add_draw_command(cmd: Dictionary):
|
||||||
|
_optimize_command(cmd)
|
||||||
|
|
||||||
|
draw_commands.append(cmd)
|
||||||
|
|
||||||
|
if draw_commands.size() > max_draw_commands:
|
||||||
|
draw_commands = draw_commands.slice(draw_commands.size() - max_draw_commands)
|
||||||
|
|
||||||
|
if not pending_redraw:
|
||||||
|
pending_redraw = true
|
||||||
|
call_deferred("_do_redraw")
|
||||||
|
|
||||||
|
func _optimize_command(cmd: Dictionary):
|
||||||
|
# Remove redundant consecutive clearRect commands
|
||||||
|
if cmd.type == "clearRect" and draw_commands.size() > 0:
|
||||||
|
var last_cmd = draw_commands[-1]
|
||||||
|
if last_cmd.type == "clearRect" and \
|
||||||
|
last_cmd.x == cmd.x and last_cmd.y == cmd.y and \
|
||||||
|
last_cmd.width == cmd.width and last_cmd.height == cmd.height:
|
||||||
|
draw_commands.pop_back()
|
||||||
|
|
||||||
|
func _do_redraw():
|
||||||
|
pending_redraw = false
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
func clear():
|
func clear():
|
||||||
draw_commands.clear()
|
draw_commands.clear()
|
||||||
queue_redraw()
|
_do_redraw()
|
||||||
|
|||||||
@@ -3,8 +3,35 @@ extends RefCounted
|
|||||||
|
|
||||||
# This file mainly creates operations that are handled by canvas.gd
|
# This file mainly creates operations that are handled by canvas.gd
|
||||||
|
|
||||||
|
static var pending_operations: Dictionary = {}
|
||||||
|
static var batch_timer: SceneTreeTimer = null
|
||||||
|
|
||||||
static func emit_canvas_operation(lua_api: LuaAPI, operation: Dictionary) -> void:
|
static func emit_canvas_operation(lua_api: LuaAPI, operation: Dictionary) -> void:
|
||||||
lua_api.threaded_vm.call_deferred("_emit_dom_operation_request", operation)
|
var element_id = operation.get("element_id", "")
|
||||||
|
|
||||||
|
if not pending_operations.has(element_id):
|
||||||
|
pending_operations[element_id] = []
|
||||||
|
|
||||||
|
pending_operations[element_id].append(operation)
|
||||||
|
|
||||||
|
if not batch_timer or batch_timer.time_left <= 0:
|
||||||
|
var scene_tree = lua_api.get_tree() if lua_api else Engine.get_main_loop()
|
||||||
|
if scene_tree:
|
||||||
|
batch_timer = scene_tree.create_timer(0.001) # 1ms batch window
|
||||||
|
batch_timer.timeout.connect(_flush_pending_operations.bind(lua_api))
|
||||||
|
|
||||||
|
static func _flush_pending_operations(lua_api: LuaAPI) -> void:
|
||||||
|
if not lua_api or not lua_api.is_inside_tree():
|
||||||
|
pending_operations.clear()
|
||||||
|
return
|
||||||
|
|
||||||
|
for element_id in pending_operations:
|
||||||
|
var operations = pending_operations[element_id]
|
||||||
|
for operation in operations:
|
||||||
|
lua_api.threaded_vm.call_deferred("_emit_dom_operation_request", operation)
|
||||||
|
|
||||||
|
pending_operations.clear()
|
||||||
|
batch_timer = null
|
||||||
|
|
||||||
static func _element_withContext_wrapper(vm: LuauVM) -> int:
|
static func _element_withContext_wrapper(vm: LuauVM) -> int:
|
||||||
var lua_api = vm.get_meta("lua_api") as LuaAPI
|
var lua_api = vm.get_meta("lua_api") as LuaAPI
|
||||||
|
|||||||
@@ -1054,6 +1054,60 @@ static func _element_index_wrapper(vm: LuauVM) -> int:
|
|||||||
# Fallback to true (visible by default)
|
# Fallback to true (visible by default)
|
||||||
vm.lua_pushboolean(true)
|
vm.lua_pushboolean(true)
|
||||||
return 1
|
return 1
|
||||||
|
"size":
|
||||||
|
if lua_api:
|
||||||
|
vm.lua_getfield(1, "_element_id")
|
||||||
|
var element_id: String = vm.lua_tostring(-1)
|
||||||
|
vm.lua_pop(1)
|
||||||
|
|
||||||
|
var dom_node = lua_api.dom_parser.parse_result.dom_nodes.get(element_id, null)
|
||||||
|
if dom_node and dom_node is Control:
|
||||||
|
var result = [0.0, 0.0, false]
|
||||||
|
lua_api.call_deferred("_get_element_size_sync", result, element_id)
|
||||||
|
while not result[2]: # wait for completion flag
|
||||||
|
OS.delay_msec(1)
|
||||||
|
|
||||||
|
vm.lua_newtable()
|
||||||
|
vm.lua_pushnumber(result[0])
|
||||||
|
vm.lua_setfield(-2, "width")
|
||||||
|
vm.lua_pushnumber(result[1])
|
||||||
|
vm.lua_setfield(-2, "height")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Fallback to zero size
|
||||||
|
vm.lua_newtable()
|
||||||
|
vm.lua_pushnumber(0)
|
||||||
|
vm.lua_setfield(-2, "width")
|
||||||
|
vm.lua_pushnumber(0)
|
||||||
|
vm.lua_setfield(-2, "height")
|
||||||
|
return 1
|
||||||
|
"position":
|
||||||
|
if lua_api:
|
||||||
|
vm.lua_getfield(1, "_element_id")
|
||||||
|
var element_id: String = vm.lua_tostring(-1)
|
||||||
|
vm.lua_pop(1)
|
||||||
|
|
||||||
|
var dom_node = lua_api.dom_parser.parse_result.dom_nodes.get(element_id, null)
|
||||||
|
if dom_node and dom_node is Control:
|
||||||
|
var result = [0.0, 0.0, false]
|
||||||
|
lua_api.call_deferred("_get_element_position_sync", result, element_id)
|
||||||
|
while not result[2]: # wait for completion flag
|
||||||
|
OS.delay_msec(1)
|
||||||
|
|
||||||
|
vm.lua_newtable()
|
||||||
|
vm.lua_pushnumber(result[0])
|
||||||
|
vm.lua_setfield(-2, "x")
|
||||||
|
vm.lua_pushnumber(result[1])
|
||||||
|
vm.lua_setfield(-2, "y")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Fallback to zero position
|
||||||
|
vm.lua_newtable()
|
||||||
|
vm.lua_pushnumber(0)
|
||||||
|
vm.lua_setfield(-2, "x")
|
||||||
|
vm.lua_pushnumber(0)
|
||||||
|
vm.lua_setfield(-2, "y")
|
||||||
|
return 1
|
||||||
_:
|
_:
|
||||||
# Check for DOM traversal properties first
|
# Check for DOM traversal properties first
|
||||||
if lua_api:
|
if lua_api:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ static func connect_element_event(signal_node: Node, event_name: String, subscri
|
|||||||
var wrapper = func():
|
var wrapper = func():
|
||||||
LuaAudioUtils.mark_user_event()
|
LuaAudioUtils.mark_user_event()
|
||||||
LuaDownloadUtils.mark_user_event()
|
LuaDownloadUtils.mark_user_event()
|
||||||
subscription.lua_api._on_event_triggered(subscription)
|
subscription.lua_api._execute_lua_callback(subscription, [{}])
|
||||||
signal_node.pressed.connect(wrapper)
|
signal_node.pressed.connect(wrapper)
|
||||||
subscription.connected_signal = "pressed"
|
subscription.connected_signal = "pressed"
|
||||||
subscription.connected_node = signal_node if signal_node != subscription.lua_api.get_dom_node(signal_node.get_parent(), "signal") else null
|
subscription.connected_node = signal_node if signal_node != subscription.lua_api.get_dom_node(signal_node.get_parent(), "signal") else null
|
||||||
@@ -189,6 +189,11 @@ static func connect_body_event(event_name: String, subscription, lua_api) -> boo
|
|||||||
subscription.connected_signal = "input_mousemove"
|
subscription.connected_signal = "input_mousemove"
|
||||||
subscription.connected_node = lua_api
|
subscription.connected_node = lua_api
|
||||||
return true
|
return true
|
||||||
|
"mousedown", "mouseup":
|
||||||
|
lua_api.set_process_input(true)
|
||||||
|
subscription.connected_signal = "input"
|
||||||
|
subscription.connected_node = lua_api
|
||||||
|
return true
|
||||||
"mouseenter", "mouseexit":
|
"mouseenter", "mouseexit":
|
||||||
var main_container = lua_api.dom_parser.parse_result.dom_nodes.get("body", null)
|
var main_container = lua_api.dom_parser.parse_result.dom_nodes.get("body", null)
|
||||||
if main_container:
|
if main_container:
|
||||||
|
|||||||
@@ -56,11 +56,12 @@ func stop_lua_thread():
|
|||||||
lua_thread.wait_to_finish()
|
lua_thread.wait_to_finish()
|
||||||
lua_thread = null
|
lua_thread = null
|
||||||
|
|
||||||
func execute_script_async(script_code: String):
|
func execute_script_async(script_code: String, chunk_name: String = "dostring"):
|
||||||
queue_mutex.lock()
|
queue_mutex.lock()
|
||||||
command_queue.append({
|
command_queue.append({
|
||||||
"type": "execute_script",
|
"type": "execute_script",
|
||||||
"code": script_code
|
"code": script_code,
|
||||||
|
"chunk_name": chunk_name
|
||||||
})
|
})
|
||||||
queue_mutex.unlock()
|
queue_mutex.unlock()
|
||||||
thread_semaphore.post()
|
thread_semaphore.post()
|
||||||
@@ -143,21 +144,28 @@ func _process_command_queue():
|
|||||||
for command in commands_to_process:
|
for command in commands_to_process:
|
||||||
match command.type:
|
match command.type:
|
||||||
"execute_script":
|
"execute_script":
|
||||||
_execute_script_in_thread(command.code)
|
_execute_script_in_thread(command.code, command.get("chunk_name", "dostring"))
|
||||||
"execute_callback":
|
"execute_callback":
|
||||||
_execute_callback_in_thread(command.callback_ref, command.args)
|
_execute_callback_in_thread(command.callback_ref, command.args)
|
||||||
"execute_timeout":
|
"execute_timeout":
|
||||||
_execute_timeout_in_thread(command.timeout_id)
|
_execute_timeout_in_thread(command.timeout_id)
|
||||||
|
|
||||||
func _execute_script_in_thread(script_code: String):
|
func _execute_script_in_thread(script_code: String, chunk_name: String = "dostring"):
|
||||||
if not lua_vm:
|
if not lua_vm:
|
||||||
call_deferred("_emit_script_error", "Lua VM not initialized")
|
call_deferred("_emit_script_error", "Lua VM not initialized")
|
||||||
return
|
return
|
||||||
|
|
||||||
var result = lua_vm.lua_dostring(script_code)
|
# Use load_string with custom chunk name, then lua_pcall
|
||||||
|
var load_result = lua_vm.load_string(script_code, chunk_name)
|
||||||
|
|
||||||
if result == lua_vm.LUA_OK:
|
if load_result == lua_vm.LUA_OK:
|
||||||
call_deferred("_emit_script_completed", {"success": true})
|
var call_result = lua_vm.lua_pcall(0, 0, 0)
|
||||||
|
if call_result == lua_vm.LUA_OK:
|
||||||
|
call_deferred("_emit_script_completed", {"success": true})
|
||||||
|
else:
|
||||||
|
var error_msg = lua_vm.lua_tostring(-1)
|
||||||
|
lua_vm.lua_pop(1)
|
||||||
|
call_deferred("_emit_script_error", error_msg)
|
||||||
else:
|
else:
|
||||||
var error_msg = lua_vm.lua_tostring(-1)
|
var error_msg = lua_vm.lua_tostring(-1)
|
||||||
lua_vm.lua_pop(1)
|
lua_vm.lua_pop(1)
|
||||||
@@ -257,6 +265,16 @@ func _print_handler(vm: LuauVM) -> int:
|
|||||||
"count": message_parts.size()
|
"count": message_parts.size()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Also call trace.log with the formatted message
|
||||||
|
var message_strings: Array[String] = []
|
||||||
|
for part in message_parts:
|
||||||
|
if part.type == "table":
|
||||||
|
message_strings.append(str(part.data))
|
||||||
|
else:
|
||||||
|
message_strings.append(part.data)
|
||||||
|
var final_message = "\t".join(message_strings)
|
||||||
|
call_deferred("_emit_trace_message", final_message, "log")
|
||||||
|
|
||||||
call_deferred("_emit_print_output", print_data)
|
call_deferred("_emit_print_output", print_data)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
@@ -359,6 +377,12 @@ func _setup_threaded_gurt_api():
|
|||||||
lua_vm.lua_setfield(-2, "on")
|
lua_vm.lua_setfield(-2, "on")
|
||||||
lua_vm.lua_setfield(-2, "body")
|
lua_vm.lua_setfield(-2, "body")
|
||||||
|
|
||||||
|
lua_vm.lua_pushcallable(_gurt_get_width_handler, "gurt.width")
|
||||||
|
lua_vm.lua_setfield(-2, "width")
|
||||||
|
|
||||||
|
lua_vm.lua_pushcallable(_gurt_get_height_handler, "gurt.height")
|
||||||
|
lua_vm.lua_setfield(-2, "height")
|
||||||
|
|
||||||
lua_vm.lua_setglobal("gurt")
|
lua_vm.lua_setglobal("gurt")
|
||||||
|
|
||||||
func _setup_additional_lua_apis():
|
func _setup_additional_lua_apis():
|
||||||
@@ -499,9 +523,49 @@ func _set_interval_handler(vm: LuauVM) -> int:
|
|||||||
func get_current_href() -> String:
|
func get_current_href() -> String:
|
||||||
var main_node = Engine.get_main_loop().current_scene
|
var main_node = Engine.get_main_loop().current_scene
|
||||||
|
|
||||||
|
if main_node == null:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return main_node.current_domain
|
||||||
|
|
||||||
|
func _gurt_get_width_handler(vm: LuauVM) -> int:
|
||||||
|
var result = [0.0]
|
||||||
|
call_deferred("_get_width_sync", result)
|
||||||
|
while result[0] == 0.0:
|
||||||
|
OS.delay_msec(1)
|
||||||
|
vm.lua_pushnumber(result[0])
|
||||||
|
return 1
|
||||||
|
|
||||||
|
func _gurt_get_height_handler(vm: LuauVM) -> int:
|
||||||
|
var result = [0.0]
|
||||||
|
call_deferred("_get_height_sync", result)
|
||||||
|
while result[0] == 0.0:
|
||||||
|
OS.delay_msec(1)
|
||||||
|
vm.lua_pushnumber(result[0])
|
||||||
|
return 1
|
||||||
|
|
||||||
|
func _get_width_sync(result: Array):
|
||||||
|
var main_node = Engine.get_main_loop().current_scene
|
||||||
if main_node:
|
if main_node:
|
||||||
return main_node.current_domain
|
var active_tab = main_node.get_active_tab()
|
||||||
return ""
|
if active_tab and active_tab.website_container:
|
||||||
|
result[0] = active_tab.website_container.size.x
|
||||||
|
else:
|
||||||
|
result[0] = 1024.0
|
||||||
|
else:
|
||||||
|
result[0] = 1024.0
|
||||||
|
|
||||||
|
func _get_height_sync(result: Array):
|
||||||
|
var main_node = Engine.get_main_loop().current_scene
|
||||||
|
if main_node:
|
||||||
|
var active_tab = main_node.get_active_tab()
|
||||||
|
if active_tab and active_tab.website_container:
|
||||||
|
result[0] = active_tab.website_container.size.y
|
||||||
|
else:
|
||||||
|
result[0] = 768.0
|
||||||
|
else:
|
||||||
|
result[0] = 768.0
|
||||||
|
|
||||||
|
|
||||||
func _gurt_select_handler(vm: LuauVM) -> int:
|
func _gurt_select_handler(vm: LuauVM) -> int:
|
||||||
var selector: String = vm.luaL_checkstring(1)
|
var selector: String = vm.luaL_checkstring(1)
|
||||||
|
|||||||
@@ -52,21 +52,21 @@ static func clear_messages() -> void:
|
|||||||
_messages.clear()
|
_messages.clear()
|
||||||
|
|
||||||
static func _lua_trace_log_handler(vm: LuauVM) -> int:
|
static func _lua_trace_log_handler(vm: LuauVM) -> int:
|
||||||
var message = vm.luaL_checkstring(1)
|
var message = LuaPrintUtils.lua_value_to_string(vm, 1)
|
||||||
vm.lua_getglobal("_trace_log")
|
vm.lua_getglobal("_trace_log")
|
||||||
vm.lua_pushstring(message)
|
vm.lua_pushstring(message)
|
||||||
vm.lua_call(1, 0)
|
vm.lua_call(1, 0)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
static func _lua_trace_warn_handler(vm: LuauVM) -> int:
|
static func _lua_trace_warn_handler(vm: LuauVM) -> int:
|
||||||
var message = vm.luaL_checkstring(1)
|
var message = LuaPrintUtils.lua_value_to_string(vm, 1)
|
||||||
vm.lua_getglobal("_trace_warning")
|
vm.lua_getglobal("_trace_warning")
|
||||||
vm.lua_pushstring(message)
|
vm.lua_pushstring(message)
|
||||||
vm.lua_call(1, 0)
|
vm.lua_call(1, 0)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
static func _lua_trace_error_handler(vm: LuauVM) -> int:
|
static func _lua_trace_error_handler(vm: LuauVM) -> int:
|
||||||
var message = vm.luaL_checkstring(1)
|
var message = LuaPrintUtils.lua_value_to_string(vm, 1)
|
||||||
vm.lua_getglobal("_trace_error")
|
vm.lua_getglobal("_trace_error")
|
||||||
vm.lua_pushstring(message)
|
vm.lua_pushstring(message)
|
||||||
vm.lua_call(1, 0)
|
vm.lua_call(1, 0)
|
||||||
|
|||||||
@@ -388,7 +388,7 @@ func render_content(html_bytes: PackedByteArray) -> void:
|
|||||||
await parser.process_external_styles(current_domain)
|
await parser.process_external_styles(current_domain)
|
||||||
|
|
||||||
# Process and load all custom fonts defined in <font> tags
|
# Process and load all custom fonts defined in <font> tags
|
||||||
parser.process_fonts()
|
parser.process_fonts(current_domain)
|
||||||
FontManager.load_all_fonts()
|
FontManager.load_all_fonts()
|
||||||
|
|
||||||
if parse_result.errors.size() > 0:
|
if parse_result.errors.size() > 0:
|
||||||
@@ -810,7 +810,6 @@ func register_font_dependent_element(label: Control, styles: Dictionary, element
|
|||||||
})
|
})
|
||||||
|
|
||||||
func refresh_fonts(font_name: String) -> void:
|
func refresh_fonts(font_name: String) -> void:
|
||||||
# Find all elements that should use this font and refresh them
|
|
||||||
for element_info in font_dependent_elements:
|
for element_info in font_dependent_elements:
|
||||||
var label = element_info["label"]
|
var label = element_info["label"]
|
||||||
var styles = element_info["styles"]
|
var styles = element_info["styles"]
|
||||||
@@ -819,7 +818,7 @@ func refresh_fonts(font_name: String) -> void:
|
|||||||
|
|
||||||
if styles.has("font-family") and styles["font-family"] == font_name:
|
if styles.has("font-family") and styles["font-family"] == font_name:
|
||||||
if is_instance_valid(label):
|
if is_instance_valid(label):
|
||||||
StyleManager.apply_styles_to_label(label, styles, element, parser)
|
StyleManager.apply_styles_to_label(label, styles, element, parser, "", true)
|
||||||
|
|
||||||
func get_current_url() -> String:
|
func get_current_url() -> String:
|
||||||
return current_domain if not current_domain.is_empty() else ""
|
return current_domain if not current_domain.is_empty() else ""
|
||||||
|
|||||||
Binary file not shown.
@@ -39,3 +39,70 @@ unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
|
|||||||
ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
|
ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
|
||||||
kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\")
|
kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\")
|
||||||
rm -rf \"{temp_dir}\""
|
rm -rf \"{temp_dir}\""
|
||||||
|
|
||||||
|
[preset.1]
|
||||||
|
|
||||||
|
name="Windows Desktop"
|
||||||
|
platform="Windows Desktop"
|
||||||
|
runnable=true
|
||||||
|
advanced_options=false
|
||||||
|
dedicated_server=false
|
||||||
|
custom_features=""
|
||||||
|
export_filter="all_resources"
|
||||||
|
include_filter=""
|
||||||
|
exclude_filter=""
|
||||||
|
export_path="build-scripts/Windows/Flumi"
|
||||||
|
patches=PackedStringArray()
|
||||||
|
encryption_include_filters=""
|
||||||
|
encryption_exclude_filters=""
|
||||||
|
seed=0
|
||||||
|
encrypt_pck=false
|
||||||
|
encrypt_directory=false
|
||||||
|
script_export_mode=2
|
||||||
|
|
||||||
|
[preset.1.options]
|
||||||
|
|
||||||
|
custom_template/debug=""
|
||||||
|
custom_template/release=""
|
||||||
|
debug/export_console_wrapper=0
|
||||||
|
binary_format/embed_pck=false
|
||||||
|
texture_format/s3tc_bptc=true
|
||||||
|
texture_format/etc2_astc=false
|
||||||
|
binary_format/architecture="x86_64"
|
||||||
|
codesign/enable=false
|
||||||
|
codesign/timestamp=true
|
||||||
|
codesign/timestamp_server_url=""
|
||||||
|
codesign/digest_algorithm=1
|
||||||
|
codesign/description=""
|
||||||
|
codesign/custom_options=PackedStringArray()
|
||||||
|
application/modify_resources=true
|
||||||
|
application/icon="uid://ctpe0lbehepen"
|
||||||
|
application/console_wrapper_icon="uid://ctpe0lbehepen"
|
||||||
|
application/icon_interpolation=4
|
||||||
|
application/file_version=""
|
||||||
|
application/product_version=""
|
||||||
|
application/company_name="Outpoot"
|
||||||
|
application/product_name="Flumi"
|
||||||
|
application/file_description="A browser for GURT protocol sites"
|
||||||
|
application/copyright="2025 Gurted"
|
||||||
|
application/trademarks=""
|
||||||
|
application/export_angle=0
|
||||||
|
application/export_d3d12=0
|
||||||
|
application/d3d12_agility_sdk_multiarch=true
|
||||||
|
ssh_remote_deploy/enabled=false
|
||||||
|
ssh_remote_deploy/host="user@host_ip"
|
||||||
|
ssh_remote_deploy/port="22"
|
||||||
|
ssh_remote_deploy/extra_args_ssh=""
|
||||||
|
ssh_remote_deploy/extra_args_scp=""
|
||||||
|
ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}'
|
||||||
|
$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}'
|
||||||
|
$trigger = New-ScheduledTaskTrigger -Once -At 00:00
|
||||||
|
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
|
||||||
|
$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings
|
||||||
|
Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true
|
||||||
|
Start-ScheduledTask -TaskName godot_remote_debug
|
||||||
|
while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 }
|
||||||
|
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue"
|
||||||
|
ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue
|
||||||
|
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
|
||||||
|
Remove-Item -Recurse -Force '{temp_dir}'"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ config_version=5
|
|||||||
[application]
|
[application]
|
||||||
|
|
||||||
config/name="Flumi"
|
config/name="Flumi"
|
||||||
config/version="1.0.0"
|
config/version="1.0.1"
|
||||||
run/main_scene="uid://bytm7bt2s4ak8"
|
run/main_scene="uid://bytm7bt2s4ak8"
|
||||||
config/use_custom_user_dir=true
|
config/use_custom_user_dir=true
|
||||||
config/features=PackedStringArray("4.4", "Forward Plus")
|
config/features=PackedStringArray("4.4", "Forward Plus")
|
||||||
@@ -80,3 +80,8 @@ DevTools={
|
|||||||
"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)
|
"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)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
ReloadPage={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"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":82,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
4
protocol/cli/Cargo.lock
generated
4
protocol/cli/Cargo.lock
generated
@@ -416,7 +416,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gurt"
|
name = "gurtlib"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
@@ -440,7 +440,7 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"clap",
|
"clap",
|
||||||
"colored",
|
"colored",
|
||||||
"gurt",
|
"gurtlib",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"regex",
|
"regex",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ name = "gurty"
|
|||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gurt = { path = "../library" }
|
gurtlib = { path = "../library" }
|
||||||
|
|
||||||
tokio = { version = "1.0", features = ["full"] }
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ impl From<std::io::Error> for ServerError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<gurt::GurtError> for ServerError {
|
impl From<gurtlib::GurtError> for ServerError {
|
||||||
fn from(err: gurt::GurtError) -> Self {
|
fn from(err: gurtlib::GurtError) -> Self {
|
||||||
ServerError::ServerStartup(err.to_string())
|
ServerError::ServerStartup(err.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::{
|
|||||||
config::GurtConfig,
|
config::GurtConfig,
|
||||||
security::SecurityMiddleware,
|
security::SecurityMiddleware,
|
||||||
};
|
};
|
||||||
use gurt::prelude::*;
|
use gurtlib::prelude::*;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing;
|
use tracing;
|
||||||
@@ -238,14 +238,14 @@ impl RequestHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let result = match method {
|
let result = match method {
|
||||||
gurt::message::GurtMethod::GET => {
|
gurtlib::message::GurtMethod::GET => {
|
||||||
if ctx.path() == "/" {
|
if ctx.path() == "/" {
|
||||||
self.handle_root_request().await
|
self.handle_root_request().await
|
||||||
} else {
|
} else {
|
||||||
self.handle_file_request(ctx.path()).await
|
self.handle_file_request(ctx.path()).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
gurt::message::GurtMethod::HEAD => {
|
gurtlib::message::GurtMethod::HEAD => {
|
||||||
let mut response = if ctx.path() == "/" {
|
let mut response = if ctx.path() == "/" {
|
||||||
self.handle_root_request().await?
|
self.handle_root_request().await?
|
||||||
} else {
|
} else {
|
||||||
@@ -254,7 +254,7 @@ impl RequestHandler {
|
|||||||
response.body = Vec::new();
|
response.body = Vec::new();
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
gurt::message::GurtMethod::OPTIONS => {
|
gurtlib::message::GurtMethod::OPTIONS => {
|
||||||
let allowed_methods = if let Some(config) = &self.config {
|
let allowed_methods = if let Some(config) = &self.config {
|
||||||
if let Some(security) = &config.security {
|
if let Some(security) = &config.security {
|
||||||
security.allowed_methods.join(", ")
|
security.allowed_methods.join(", ")
|
||||||
@@ -272,7 +272,7 @@ impl RequestHandler {
|
|||||||
Ok(self.apply_global_headers(response))
|
Ok(self.apply_global_headers(response))
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let response = GurtResponse::new(gurt::protocol::GurtStatusCode::MethodNotAllowed)
|
let response = GurtResponse::new(gurtlib::protocol::GurtStatusCode::MethodNotAllowed)
|
||||||
.with_header("Content-Type", "text/html");
|
.with_header("Content-Type", "text/html");
|
||||||
Ok(self.apply_global_headers(response))
|
Ok(self.apply_global_headers(response))
|
||||||
}
|
}
|
||||||
@@ -430,7 +430,7 @@ impl RequestHandler {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use gurt::GurtStatusCode;
|
use gurtlib::GurtStatusCode;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::config::GurtConfig;
|
use crate::config::GurtConfig;
|
||||||
use gurt::{prelude::*, GurtMethod, GurtStatusCode};
|
use gurtlib::{prelude::*, GurtMethod, GurtStatusCode};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::{
|
|||||||
handlers::{FileHandler, DirectoryHandler, DefaultFileHandler, DefaultDirectoryHandler},
|
handlers::{FileHandler, DirectoryHandler, DefaultFileHandler, DefaultDirectoryHandler},
|
||||||
request_handler::{RequestHandler, RequestHandlerBuilder},
|
request_handler::{RequestHandler, RequestHandlerBuilder},
|
||||||
};
|
};
|
||||||
use gurt::prelude::*;
|
use gurtlib::prelude::*;
|
||||||
use std::{path::PathBuf, sync::Arc};
|
use std::{path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
pub struct FileServerBuilder {
|
pub struct FileServerBuilder {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ name = "gurt_godot"
|
|||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gurt = { path = "../library" }
|
gurtlib = { path = "../library" }
|
||||||
|
|
||||||
godot = "0.1"
|
godot = "0.1"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use godot::prelude::*;
|
use godot::prelude::*;
|
||||||
use gurt::prelude::*;
|
use gurtlib::prelude::*;
|
||||||
use gurt::{GurtMethod, GurtClientConfig, GurtRequest};
|
use gurtlib::{GurtMethod, GurtClientConfig, GurtRequest};
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
@@ -266,12 +266,12 @@ impl GurtProtocolClient {
|
|||||||
|
|
||||||
#[func]
|
#[func]
|
||||||
fn get_version(&self) -> GString {
|
fn get_version(&self) -> GString {
|
||||||
gurt::GURT_VERSION.to_string().into()
|
gurtlib::GURT_VERSION.to_string().into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[func]
|
#[func]
|
||||||
fn get_default_port(&self) -> i32 {
|
fn get_default_port(&self) -> i32 {
|
||||||
gurt::DEFAULT_PORT as i32
|
gurtlib::DEFAULT_PORT as i32
|
||||||
}
|
}
|
||||||
|
|
||||||
#[func]
|
#[func]
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ path = "src/main.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.0", features = ["derive"] }
|
clap = { version = "4.0", features = ["derive"] }
|
||||||
tokio = { version = "1.0", features = ["full"] }
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
gurt = { path = "../library" }
|
gurtlib = { path = "../library" }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
openssl = "0.10"
|
openssl = "0.10"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use gurt::prelude::*;
|
use gurtlib::prelude::*;
|
||||||
|
|
||||||
pub struct GurtCAClient {
|
pub struct GurtCAClient {
|
||||||
ca_url: String,
|
ca_url: String,
|
||||||
@@ -67,9 +67,9 @@ impl GurtCAClient {
|
|||||||
println!("✅ Fetched CA certificate via HTTP bootstrap");
|
println!("✅ Fetched CA certificate via HTTP bootstrap");
|
||||||
|
|
||||||
// Create new client with custom CA
|
// Create new client with custom CA
|
||||||
let mut config = gurt::client::GurtClientConfig::default();
|
let mut config = gurtlib::client::GurtClientConfig::default();
|
||||||
config.custom_ca_certificates = vec![ca_cert];
|
config.custom_ca_certificates = vec![ca_cert];
|
||||||
let gurt_client = gurt::GurtClient::with_config(config);
|
let gurt_client = gurtlib::GurtClient::with_config(config);
|
||||||
let client_with_ca = Self {
|
let client_with_ca = Self {
|
||||||
ca_url,
|
ca_url,
|
||||||
gurt_client,
|
gurt_client,
|
||||||
|
|||||||
2
protocol/library/Cargo.lock
generated
2
protocol/library/Cargo.lock
generated
@@ -369,7 +369,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gurt"
|
name = "gurtlib"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "gurt"
|
name = "gurtlib"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["FaceDev"]
|
authors = ["FaceDev"]
|
||||||
@@ -8,7 +8,7 @@ repository = "https://github.com/outpoot/gurted"
|
|||||||
description = "Official GURT:// protocol implementation"
|
description = "Official GURT:// protocol implementation"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "gurt"
|
name = "gurtlib"
|
||||||
crate-type = ["cdylib", "lib"]
|
crate-type = ["cdylib", "lib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use gurt::{GurtServer, GurtResponse, ServerContext, Result};
|
use gurtlib::{GurtServer, GurtResponse, ServerContext, Result};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
|
|||||||
@@ -109,9 +109,9 @@ impl GurtClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_pooled_connection(&self, host: &str, port: u16) -> Result<tokio_rustls::client::TlsStream<TcpStream>> {
|
async fn get_pooled_connection(&self, host: &str, port: u16, original_host: Option<&str>) -> Result<tokio_rustls::client::TlsStream<TcpStream>> {
|
||||||
if !self.config.enable_connection_pooling {
|
if !self.config.enable_connection_pooling {
|
||||||
return self.perform_handshake(host, port).await;
|
return self.perform_handshake(host, port, original_host).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = ConnectionKey {
|
let key = ConnectionKey {
|
||||||
@@ -131,7 +131,7 @@ impl GurtClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
debug!("Creating new connection for {}:{}", host, port);
|
debug!("Creating new connection for {}:{}", host, port);
|
||||||
self.perform_handshake(host, port).await
|
self.perform_handshake(host, port, original_host).await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn return_connection_to_pool(&self, host: &str, port: u16, connection: tokio_rustls::client::TlsStream<TcpStream>) {
|
fn return_connection_to_pool(&self, host: &str, port: u16, connection: tokio_rustls::client::TlsStream<TcpStream>) {
|
||||||
@@ -231,13 +231,16 @@ impl GurtClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn perform_handshake(&self, host: &str, port: u16) -> Result<tokio_rustls::client::TlsStream<TcpStream>> {
|
async fn perform_handshake(&self, host: &str, port: u16, original_host: Option<&str>) -> Result<tokio_rustls::client::TlsStream<TcpStream>> {
|
||||||
debug!("Starting GURT handshake with {}:{}", host, port);
|
debug!("Starting GURT handshake with {}:{}", host, port);
|
||||||
|
|
||||||
let mut plain_conn = self.create_connection(host, port).await?;
|
let mut plain_conn = self.create_connection(host, port).await?;
|
||||||
|
|
||||||
|
// Use original_host for the Host header if available, otherwise fall back to host
|
||||||
|
let host_header = original_host.unwrap_or(host);
|
||||||
|
|
||||||
let handshake_request = GurtRequest::new(GurtMethod::HANDSHAKE, "/".to_string())
|
let handshake_request = GurtRequest::new(GurtMethod::HANDSHAKE, "/".to_string())
|
||||||
.with_header("Host", host)
|
.with_header("Host", host_header)
|
||||||
.with_header("User-Agent", &self.config.user_agent);
|
.with_header("User-Agent", &self.config.user_agent);
|
||||||
|
|
||||||
let handshake_data = handshake_request.to_string();
|
let handshake_data = handshake_request.to_string();
|
||||||
@@ -261,7 +264,10 @@ impl GurtClient {
|
|||||||
Connection::Plain(stream) => stream,
|
Connection::Plain(stream) => stream,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.upgrade_to_tls(tcp_stream, host).await
|
// Use original_host for TLS SNI if available, otherwise fall back to host
|
||||||
|
let tls_host = original_host.unwrap_or(host);
|
||||||
|
|
||||||
|
self.upgrade_to_tls(tcp_stream, tls_host).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn upgrade_to_tls(&self, stream: TcpStream, host: &str) -> Result<tokio_rustls::client::TlsStream<TcpStream>> {
|
async fn upgrade_to_tls(&self, stream: TcpStream, host: &str) -> Result<tokio_rustls::client::TlsStream<TcpStream>> {
|
||||||
@@ -323,10 +329,10 @@ impl GurtClient {
|
|||||||
Ok(tls_stream)
|
Ok(tls_stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_request_internal(&self, host: &str, port: u16, request: GurtRequest) -> Result<GurtResponse> {
|
async fn send_request_internal(&self, host: &str, port: u16, request: GurtRequest, original_host: Option<&str>) -> Result<GurtResponse> {
|
||||||
debug!("Sending {} {} to {}:{}", request.method, request.path, host, port);
|
debug!("Sending {} {} to {}:{}", request.method, request.path, host, port);
|
||||||
|
|
||||||
let mut tls_stream = self.get_pooled_connection(host, port).await?;
|
let mut tls_stream = self.get_pooled_connection(host, port, original_host).await?;
|
||||||
|
|
||||||
let request_data = request.to_string();
|
let request_data = request.to_string();
|
||||||
tls_stream.write_all(request_data.as_bytes()).await
|
tls_stream.write_all(request_data.as_bytes()).await
|
||||||
@@ -501,7 +507,7 @@ impl GurtClient {
|
|||||||
|
|
||||||
request = request.with_header("Host", host);
|
request = request.with_header("Host", host);
|
||||||
|
|
||||||
self.send_request_internal(&resolved_host, port, request).await
|
self.send_request_internal(&resolved_host, port, request, Some(host)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_gurt_url(&self, url: &str) -> Result<(String, u16, String)> {
|
fn parse_gurt_url(&self, url: &str) -> Result<(String, u16, String)> {
|
||||||
@@ -564,7 +570,7 @@ impl GurtClient {
|
|||||||
.with_header("Content-Type", "application/json")
|
.with_header("Content-Type", "application/json")
|
||||||
.with_string_body(dns_request_body);
|
.with_string_body(dns_request_body);
|
||||||
|
|
||||||
let dns_response = self.send_request_internal(&dns_server_ip, self.config.dns_server_port, dns_request).await?;
|
let dns_response = self.send_request_internal(&dns_server_ip, self.config.dns_server_port, dns_request, None).await?;
|
||||||
|
|
||||||
if dns_response.status_code != 200 {
|
if dns_response.status_code != 200 {
|
||||||
return Err(GurtError::invalid_message(format!(
|
return Err(GurtError::invalid_message(format!(
|
||||||
@@ -675,4 +681,55 @@ mod tests {
|
|||||||
assert_eq!(key1, key2);
|
assert_eq!(key1, key2);
|
||||||
assert_ne!(key1, key3);
|
assert_ne!(key1, key3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_host_header_preserved_with_dns_resolution() {
|
||||||
|
use crate::message::{GurtMethod, GurtRequest};
|
||||||
|
|
||||||
|
let mut config = GurtClientConfig::default();
|
||||||
|
config.enable_connection_pooling = false;
|
||||||
|
let client = GurtClient::with_config(config);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut dns_cache = client.dns_cache.lock().unwrap();
|
||||||
|
dns_cache.insert("arson.dev".to_string(), "1.1.1.1".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = GurtRequest::new(GurtMethod::GET, "/test".to_string());
|
||||||
|
|
||||||
|
let original_host = "arson.dev";
|
||||||
|
|
||||||
|
let mut test_request = request.clone();
|
||||||
|
test_request = test_request.with_header("Host", original_host);
|
||||||
|
|
||||||
|
assert_eq!(test_request.headers.get("host").unwrap(), original_host);
|
||||||
|
|
||||||
|
let resolved = client.resolve_domain("arson.dev").await.unwrap();
|
||||||
|
assert_eq!(resolved, "1.1.1.1");
|
||||||
|
|
||||||
|
let request_with_host = GurtRequest::new(GurtMethod::GET, "/test".to_string())
|
||||||
|
.with_header("Host", original_host);
|
||||||
|
|
||||||
|
assert_eq!(request_with_host.headers.get("host").unwrap(), "arson.dev");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_handshake_request_uses_original_host() {
|
||||||
|
use crate::message::{GurtMethod, GurtRequest};
|
||||||
|
|
||||||
|
let original_host = "arson.dev";
|
||||||
|
|
||||||
|
let handshake_request = GurtRequest::new(GurtMethod::HANDSHAKE, "/".to_string())
|
||||||
|
.with_header("Host", original_host)
|
||||||
|
.with_header("User-Agent", "GURT-Client/1.0.0");
|
||||||
|
|
||||||
|
assert_eq!(handshake_request.headers.get("host").unwrap(), "arson.dev");
|
||||||
|
assert_ne!(handshake_request.headers.get("host").unwrap(), "1.1.1.1");
|
||||||
|
|
||||||
|
assert_eq!(handshake_request.method, GurtMethod::HANDSHAKE);
|
||||||
|
assert_eq!(handshake_request.path, "/");
|
||||||
|
|
||||||
|
assert!(handshake_request.headers.contains_key("user-agent"));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -577,8 +577,8 @@ mod tests {
|
|||||||
assert!(!route.matches(&GurtMethod::POST, "/test"));
|
assert!(!route.matches(&GurtMethod::POST, "/test"));
|
||||||
assert!(!route.matches(&GurtMethod::GET, "/other"));
|
assert!(!route.matches(&GurtMethod::GET, "/other"));
|
||||||
|
|
||||||
assert!(!route.matches(&GurtMethod::GET, "/test?foo=bar"));
|
assert!(route.matches(&GurtMethod::GET, "/test?foo=bar"));
|
||||||
assert!(!route.matches(&GurtMethod::GET, "/test?page=1&limit=100"));
|
assert!(route.matches(&GurtMethod::GET, "/test?page=1&limit=100"));
|
||||||
|
|
||||||
let wildcard_route = Route::get("/api/*");
|
let wildcard_route = Route::get("/api/*");
|
||||||
assert!(wildcard_route.matches(&GurtMethod::GET, "/api/users"));
|
assert!(wildcard_route.matches(&GurtMethod::GET, "/api/users"));
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ tokio = { version = "1.38.0", features = ["full"] }
|
|||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
tantivy = "0.22"
|
tantivy = "0.22"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
gurt = { path = "../protocol/library" }
|
gurtlib = { path = "../protocol/library" }
|
||||||
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "chrono", "uuid"] }
|
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "chrono", "uuid"] }
|
||||||
scraper = "0.20"
|
scraper = "0.20"
|
||||||
lol_html = "1.2"
|
lol_html = "1.2"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use anyhow::{Result, Context};
|
use anyhow::{Result, Context};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use gurt::{GurtClient, GurtClientConfig};
|
use gurtlib::{GurtClient, GurtClientConfig};
|
||||||
use scraper::{Html, Selector};
|
use scraper::{Html, Selector};
|
||||||
use std::collections::{HashSet, VecDeque};
|
use std::collections::{HashSet, VecDeque};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use anyhow::{Result, Context};
|
use anyhow::{Result, Context};
|
||||||
use gurt::prelude::*;
|
use gurtlib::prelude::*;
|
||||||
use gurt::GurtError;
|
use gurtlib::GurtError;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing::{info, error};
|
use tracing::{info, error};
|
||||||
|
|||||||
@@ -272,7 +272,7 @@
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<pre class="bg-muted text-foreground overflow-x-auto rounded-lg p-4 text-sm"><code class="language-lua">{`<script>
|
<pre class="bg-muted text-foreground overflow-x-auto rounded-lg p-4 text-sm"><code class="language-lua">{`<script>
|
||||||
local heading = gurt.select('h1')
|
local heading = gurt.select('h1')
|
||||||
heading:text('Dynamic Content!')
|
heading.text = 'Dynamic Content!'
|
||||||
|
|
||||||
local new_div = gurt.create('div', {
|
local new_div = gurt.create('div', {
|
||||||
style = 'bg-blue-500 p-4 rounded'
|
style = 'bg-blue-500 p-4 rounded'
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import Navbar from '$lib/components/Navbar.svelte';
|
import Navbar from '$lib/components/Navbar.svelte';
|
||||||
|
|
||||||
const version = '1.0.0';
|
const version = '1.0.1';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
<CardDescription>Ubuntu 20.04+ / Fedora 35+</CardDescription>
|
<CardDescription>Ubuntu 20.04+ / Fedora 35+</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="space-y-4">
|
<CardContent class="space-y-4">
|
||||||
<Button class="w-full" size="lg" onclick={() => alert('oh my god bruh')}>
|
<Button class="w-full" size="lg" href="https://github.com/outpoot/gurted/releases/download/v{version}/Flumi_Linux.tar.gz" target="_blank" rel="noopener noreferrer">
|
||||||
<Download class="h-4 w-4 " />
|
<Download class="h-4 w-4 " />
|
||||||
Download
|
Download
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user