Compare commits

4 Commits

25 changed files with 7310 additions and 2371 deletions

3
.cargo/config.toml Normal file
View File

@@ -0,0 +1,3 @@
[target.'cfg(target_os = "linux")']
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=/usr/bin/mold"]

View File

@@ -11,79 +11,56 @@ jobs:
include:
- os: ubuntu-22.04
platform: linux
python-version: "3.11"
- os: windows-latest
platform: windows
python-version: "3.11"
steps:
- name: Check-out repository
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
- name: Cache
uses: actions/cache@v4
with:
python-version: "3.11"
architecture: "x64"
cache: "pip"
cache-dependency-path: |
**/requirements*.txt
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install Dependencies
run: pip install -r requirements.txt
- name: Build Executable
uses: Nuitka/Nuitka-Action@main
with:
nuitka-version: main
script-name: run.py
nofollow-import-to: "*tk*,_codecs,encodings,multiprocessing,gi"
disable-plugins: tk-inter,dill-compat,eventlet,gevent,pyqt5,pyqt6,pyside2,pyside6,delvewheel,pywebview,matplotlib,spacy,enum-compat,pbr-compat,gevent,pmw-freezer,transformers,upx,kivy,options-nanny,multiprocessing,gi
include-data-dir: assets=assets
include-data-files: CREDITS=CREDITS
mode: onefile
output-file: csd4ni3lBrowser
- name: Locate and rename executable (Linux)
- name: Install mold, clang, Wayland, ALSA and x11 headers and dependencies (Linux)
if: matrix.os == 'ubuntu-22.04'
run: |
set -euo pipefail
echo "Searching for built Linux binary..."
# List to help debugging when paths change
ls -laR . | head -n 500 || true
BIN=$(find . -maxdepth 4 -type f -name 'csd4ni3lBrowser*' -perm -u+x | head -n1 || true)
if [ -z "${BIN}" ]; then
echo "ERROR: No Linux binary found after build"
exit 1
fi
echo "Found: ${BIN}"
mkdir -p build_output
cp "${BIN}" build_output/csd4ni3lBrowser.bin
chmod +x build_output/csd4ni3lBrowser.bin
echo "Executable ready: build_output/csd4ni3lBrowser.bin"
sudo apt update
sudo apt install -y build-essential clang cmake pkg-config mold \
libwayland-dev libxkbcommon-dev libegl1-mesa-dev \
libwayland-egl-backend-dev \
libx11-dev libxext-dev libxrandr-dev libxinerama-dev libxcursor-dev \
libxi-dev libxfixes-dev libxrender-dev \
libfreetype6-dev libfontconfig1-dev libgl1-mesa-dev \
libasound2-dev libudev-dev
shell: bash
- name: Locate and rename executable (Windows)
- name: Build
run: cargo build --release --verbose
- name: Verify executable (Linux)
if: matrix.os == 'ubuntu-22.04'
run: test target/release/csd4ni3l-browser
shell: bash
- name: Verify executable (Windows)
if: matrix.os == 'windows-latest'
run: |
Write-Host "Searching for built Windows binary..."
Get-ChildItem -Recurse -File -Filter 'csd4ni3lBrowser*.exe' | Select-Object -First 1 | ForEach-Object {
Write-Host ("Found: " + $_.FullName)
New-Item -ItemType Directory -Force -Path build_output | Out-Null
Copy-Item $_.FullName "build_output\csd4ni3lBrowser.exe"
Write-Host "Executable ready: build_output\csd4ni3lBrowser.exe"
}
if (!(Test-Path build_output\csd4ni3lBrowser.exe)) {
Write-Error "ERROR: No Windows binary found after build"
exit 1
}
run: Test-Path target\release\csd4ni3l-browser.exe
shell: pwsh
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.platform }}
path: build_output/csd4ni3lBrowser.*
path: |
target/release/csd4ni3l-browser
target/release/csd4ni3l-browser.exe
release:
runs-on: ubuntu-latest
@@ -114,5 +91,5 @@ jobs:
--notes "Automated build for $TAG"
fi
# Upload the executables directly (no zip files)
gh release upload "$TAG" downloads/linux/csd4ni3lBrowser.bin --clobber
gh release upload "$TAG" downloads/windows/csd4ni3lBrowser.exe --clobber
gh release upload "$TAG" downloads/linux/csd4ni3l-browser --clobber
gh release upload "$TAG" downloads/windows/csd4ni3l-browser.exe --clobber

188
.gitignore vendored
View File

@@ -1,184 +1,4 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
.vscode
test*.py
.zed/
logs/
logs
settings.json
html_cache/
css_cache/
python
/target
css_cache
html_cache

View File

@@ -1 +0,0 @@
3.11

11
CREDITS
View File

@@ -4,12 +4,7 @@ The Roboto Black font used in this project is licensed under the Open Font Licen
Thanks to https://browser.engineering for their tutorial on how to make a web browser from scratch!
Huge Thanks to Python for being the programming language used in this game.
https://www.python.org/
Huge Thanks to Rust for being the programming language used in this app.
https://www.rust-lang.org/
Huge thanks to Arcade and Pyglet for being the graphical engines used in this application.
https://arcade.academy/
https://pyglet.readthedocs.io/en/latest/
Thanks to the following other libraries used in this game:
pypresence - https://github.com/qwertyquerty/pypresence - Used for Discord Rich Presence
Huge thanks to Bevy for being the graphical engine / ECS used in this app.

6017
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

53
Cargo.toml Normal file
View File

@@ -0,0 +1,53 @@
[package]
name = "csd4ni3l-browser"
version = "0.1.0"
edition = "2024"
[dependencies]
base64 = "0.22.1"
bevy_egui = "0.38.1"
native-tls = "0.2.18"
rand = "0.9.2"
rfd = "0.16.0"
serde = "1.0.228"
serde_json = "1.0.146"
[dependencies.bevy]
version = "0.17.3"
default-features = false
features = [
"bevy_log",
"bevy_window",
"bevy_winit",
"bevy_render",
"bevy_core_pipeline",
"bevy_sprite",
"bevy_text",
"bevy_ui",
"bevy_asset",
"bevy_picking",
"multi_threaded",
]
[profile.dev.package."*"]
opt-level = 2
debug = false
[target.'cfg(target_os = "linux")'.dependencies.bevy]
default-features = false
version = "0.17.3"
features = [
"bevy_log",
"bevy_window",
"bevy_winit",
"bevy_render",
"bevy_core_pipeline",
"bevy_sprite",
"bevy_text",
"bevy_ui",
"multi_threaded",
"bevy_asset",
"bevy_picking",
"wayland",
"x11"
]

View File

@@ -1,246 +0,0 @@
import socket, logging, ssl, threading, os, ujson, time
from http_client.html_parser import HTML, CSSParser, Element, tree_to_list, get_inline_styles
def resolve_url(scheme, host, port, path, url):
if "://" in url: return url
if not url.startswith("/"):
dir, _ = path.rsplit("/", 1)
while url.startswith("../"):
_, url = url.split("/", 1)
if "/" in dir:
dir, _ = dir.rsplit("/", 1)
url = f"{dir}/{url}"
if url.startswith("//"):
return f"{scheme}:{url}"
else:
return f"{scheme}://{host}:{port}{url}"
class HTTPClient():
def __init__(self):
self.scheme = "http"
self.host = ""
self.path = ""
self.port = 0
self.request_headers = {}
self.response_explanation = None
self.response_headers = {}
self.response_http_version = None
self.response_status = None
self.nodes = None
self.css_rules = []
self.content_response = ""
self.view_source = False
self.redirect_count = 0
self.needs_render = False
def file_request(self, url):
with open(url.split("file://", 1)[1], "r") as file:
self.content_response = file.read()
def get_request(self, url, request_headers, css=False):
if url.startswith("view-source:"):
url = url.split("view-source:")[1]
self.view_source = True
else:
self.view_source = False
self.scheme, url_parts = url.split("://", 1)
if "/" not in url_parts:
self.host = url_parts
self.path = "/"
else:
self.host, self.path = url_parts.split("/", 1)
self.path = f"/{self.path}"
if ":" in self.host:
self.host, port = self.host.split(":", 1)
self.port = int(port)
else:
self.port = 80 if self.scheme == "http" else 443
self.request_headers = request_headers
self.response_explanation = None
self.response_headers = {}
self.response_http_version = None
self.response_status = None
self.content_response = ""
if "Host" not in self.request_headers:
self.request_headers["Host"] = self.host
cache_filename = f"{self.scheme}_{self.host}_{self.port}_{self.path.replace('/', '_')}.html"
if os.path.exists(f"html_cache/{cache_filename}"):
threading.Thread(target=self.parse, daemon=True).start()
return
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.connect((self.host, self.port))
if self.scheme == "https":
ctx = ssl.create_default_context()
try:
self.socket = ctx.wrap_socket(self.socket, server_hostname=self.host)
except ssl.SSLCertVerificationError:
logging.debug(f"Invalid SSL cert for {self.host}:{self.port}{self.path}")
return
request_header_lines = '\r\n'.join([f"{header_name}: {header_value}" for header_name, header_value in self.request_headers.items()])
request = f"GET {self.path} HTTP/1.0\r\n{request_header_lines}\r\n\r\n"
logging.debug(f"Sending Request:\n{request}")
self.socket.send(request.encode())
threading.Thread(target=self.receive_response, daemon=True, args=(css,)).start()
def receive_response(self, css=False):
buffer = b""
headers_parsed = False
content_length = None
while True:
try:
data = self.socket.recv(2048)
if not data:
logging.debug("Connection closed by peer.")
break
buffer += data
if not headers_parsed:
header_end_index = buffer.find(b"\r\n\r\n")
if header_end_index != -1: # not found
header_data = buffer[:header_end_index].decode('latin-1')
body_data = buffer[header_end_index + 4:] # +4 for the \r\n\r\n
self._parse_headers(header_data)
headers_parsed = True
content_length_header = self.response_headers.get("Content-Length")
if content_length_header:
try:
content_length = int(content_length_header)
except ValueError:
logging.debug(f"Invalid Content-Length header: {content_length_header}")
self.content_response = body_data.decode('utf-8', errors='ignore') # Assuming body is UTF-8
if content_length is not None and len(body_data) >= content_length:
break
elif content_length is None:
pass
else:
continue
else:
self.content_response += data.decode('utf-8', errors='ignore')
if content_length is not None and len(self.content_response.encode('utf-8')) >= content_length:
break
except Exception as e:
logging.error(f"Error receiving messages: {e}")
break
self.socket.close()
self.socket = None
if 300 <= int(self.response_status) < 400:
if self.redirect_count >= 4:
return
location_header = self.response_headers["Location"]
if "http" in location_header or "https" in location_header:
self.get_request(location_header, self.request_headers)
else:
self.get_request(f"{self.scheme}://{self.host}{location_header}", self.request_headers)
else:
self.redirect_count = 0
if not css:
self.parse()
def _parse_headers(self, header_data):
lines = header_data.splitlines()
if not lines:
logging.debug("Received empty header data.")
return
response_status_line = lines[0]
try:
self.response_http_version, self.response_status, *explanation_parts = response_status_line.split(" ", 2)
self.response_explanation = " ".join(explanation_parts)
except ValueError:
logging.error(f"Error parsing status line: {response_status_line}")
return
headers = {}
for i in range(1, len(lines)):
line = lines[i]
if not line:
break
try:
header_name, value = line.split(":", 1)
headers[header_name.strip()] = value.strip()
except ValueError:
logging.error(f"Error parsing header line: {line}")
self.response_headers = headers
def parse(self):
self.css_rules = []
html_cache_filename = f"{self.scheme}_{self.host}_{self.port}_{self.path.replace('/', '_')}.html"
if html_cache_filename in os.listdir("html_cache"):
with open(f"html_cache/{html_cache_filename}", "r") as file:
self.content_response = file.read()
else:
with open(f"html_cache/{html_cache_filename}", "w") as file:
file.write(self.content_response)
original_scheme = self.scheme
original_host = self.host
original_port = self.port
original_path = self.path
original_response = self.content_response
self.nodes = HTML(self.content_response).parse()
css_links = [
node.attributes["href"]
for node in tree_to_list(self.nodes, [])
if isinstance(node, Element)
and node.tag == "link"
and node.attributes.get("rel") == "stylesheet"
and "href" in node.attributes
]
for css_link in css_links:
self.content_response = ""
css_cache_filename = f"{self.scheme}_{self.host}_{self.port}_{self.path}_{css_link}.json".replace('/', '_').replace('@', '_').replace('/', '_').replace(';', '_').replace('&', '_').replace('?', '_').replace(':', '') # we need to include the other variables so for example /styles.css wouldnt be cached for all websites
if css_cache_filename in os.listdir("css_cache"):
with open(f"css_cache/{css_cache_filename}", "r") as file:
rules = CSSParser.from_json(ujson.load(file))
else:
self.get_request(resolve_url(self.scheme, self.host, self.port, self.path, css_link), self.request_headers, css=True)
while not self.content_response:
time.sleep(0.025)
rules = CSSParser(self.content_response).parse()
with open(f"css_cache/{css_cache_filename}", "w") as file:
ujson.dump(CSSParser.to_json(rules), file)
self.css_rules.extend(rules)
self.css_rules.extend(get_inline_styles(self.nodes))
self.scheme = original_scheme
self.host = original_host
self.port = original_port
self.path = original_path
self.content_response = original_response
self.needs_render = True

View File

@@ -1,355 +0,0 @@
from utils.constants import SELF_CLOSING_TAGS, HEAD_TAGS, INHERITED_PROPERTIES
import html.entities
class Element:
def __init__(self, tag, attributes, parent):
self.tag = tag
self.attributes = attributes
self.children = []
self.parent = parent
def __repr__(self):
attrs = [" " + k + "=\"" + v + "\"" for k, v in self.attributes.items()]
attr_str = ""
for attr in attrs:
attr_str += attr
return "<" + self.tag + attr_str + ">"
class Text:
def __init__(self, text, parent):
self.text = text
self.children = []
self.parent = parent
def __repr__(self):
return repr(self.text)
class HTML():
def __init__(self, raw_html):
self.raw_html = raw_html
self.unfinished = []
self.parse()
def parse(self):
text = ""
in_tag = False
for c in self.raw_html:
if c == "<":
in_tag = True
if (not self.unfinished or not self.unfinished[-1].tag == "style") and text:
self.add_text(text) # start of new tag means before everything was content/text
text = ""
elif c == ">":
in_tag = False
self.add_tag(text) # end of a tag means everything in-between were tags
text = ""
else:
text += c
if not in_tag and text:
self.add_text(text)
return self.finish()
def add_text(self, text):
if text.isspace(): return
self.implicit_tags(None)
parent = self.unfinished[-1]
node = Text(text, parent)
parent.children.append(node)
def get_attributes(self, text):
parts = text.split()
tag = parts[0].casefold()
attributes = {}
for attrpair in parts[1:]:
if "=" in attrpair:
key, value = attrpair.split("=", 1)
if len(value) > 2 and value[0] in ["'", "\""]:
value = value[1:-1]
attributes[key.casefold()] = value
else:
attributes[attrpair.casefold()] = ""
return tag, attributes
def add_tag(self, tag):
tag, attributes = self.get_attributes(tag)
if tag.startswith("!"): return
self.implicit_tags(tag)
if tag.startswith("/"):
if len(self.unfinished) == 1: return
node = self.unfinished.pop()
parent = self.unfinished[-1]
parent.children.append(node)
elif tag in SELF_CLOSING_TAGS:
parent = self.unfinished[-1]
node = Element(tag, attributes, parent)
parent.children.append(node)
else:
parent = self.unfinished[-1] if self.unfinished else None
node = Element(tag, attributes, parent)
self.unfinished.append(node)
def implicit_tags(self, tag):
while True:
open_tags = [node.tag for node in self.unfinished]
if open_tags == [] and tag != "html":
self.add_tag("html")
elif open_tags == ["html"] and tag not in ["head", "body", "/html"]:
if tag in HEAD_TAGS:
self.add_tag("head")
else:
self.add_tag("body")
elif open_tags == ["html", "head"] and tag not in ["/head"] + HEAD_TAGS:
self.add_tag("/head")
else:
break
def finish(self):
if not self.unfinished:
self.implicit_tags(None)
while len(self.unfinished) > 1:
node = self.unfinished.pop()
parent = self.unfinished[-1]
parent.children.append(node)
return self.unfinished.pop()
@staticmethod
def print_tree(node, indent=0):
print(" " * indent, node)
for child in node.children:
HTML.print_tree(child, indent + 2)
@staticmethod
def to_json(tree: Element | Text):
if isinstance(tree, Text):
return ["text", tree.text, [HTML.to_json(child) for child in tree.children]]
elif isinstance(tree, Element):
return ["element", tree.tag, tree.attributes, [HTML.to_json(child) for child in tree.children]]
@staticmethod
def from_json(json_list, parent=None):
if json_list[0] == "text":
text = Text(json_list[1], parent)
text.children = [HTML.from_json(child, text) for child in json_list[2]]
return text
elif json_list[0] == "element":
element = Element(json_list[1], json_list[2], parent)
element.children = [HTML.from_json(child, element) for child in json_list[3]]
return element
class TagSelector:
def __init__(self, tag):
self.tag = tag
self.priority = 1
def matches(self, node):
return isinstance(node, Element) and self.tag == node.tag
class DescendantSelector:
def __init__(self, ancestor, descendant):
self.ancestor = ancestor
self.descendant = descendant
self.priority = ancestor.priority + descendant.priority
def matches(self, node):
if not self.descendant.matches(node): return False
while node.parent:
if self.ancestor.matches(node.parent): return True
node = node.parent
return False
def cascade_priority(rule):
selector, body = rule
return selector.priority
def get_inline_styles(node):
all_rules = []
for node in node.children:
if isinstance(node, Element) and node.tag == "style":
if not node.children:
continue
if isinstance(node.children[0], Text):
all_rules.extend(CSSParser(node.children[0].text).parse()) # node's first children will just be a text element that contains the css
all_rules.extend(get_inline_styles(node))
return all_rules
class CSSParser:
def __init__(self, s):
self.s = s
self.i = 0
def whitespace(self):
while self.i < len(self.s) and self.s[self.i].isspace():
self.i += 1
def literal(self, literal):
if not (self.i < len(self.s) and self.s[self.i] == literal):
raise Exception("Parsing error")
self.i += 1
def word(self):
start = self.i
while self.i < len(self.s):
if self.s[self.i].isalnum() or self.s[self.i] in "#-.%":
self.i += 1
else:
break
if not (self.i > start):
raise Exception("Parsing error")
return self.s[start:self.i]
def pair(self):
prop = self.word()
self.whitespace()
self.literal(":")
self.whitespace()
val = self.word()
return prop.casefold(), val
def ignore_until(self, chars):
while self.i < len(self.s):
if self.s[self.i] in chars:
return self.s[self.i]
else:
self.i += 1
return None
def body(self):
pairs = {}
while self.i < len(self.s) and self.s[self.i] != "}":
try:
prop, val = self.pair()
pairs[prop] = val
self.whitespace()
self.literal(";")
self.whitespace()
except Exception:
why = self.ignore_until([";", "}"])
if why == ";":
self.literal(";")
self.whitespace()
else:
break
return pairs
def selector(self):
out = TagSelector(self.word().casefold())
self.whitespace()
while self.i < len(self.s) and self.s[self.i] != "{":
tag = self.word()
descendant = TagSelector(tag.casefold())
out = DescendantSelector(out, descendant)
self.whitespace()
return out
def parse(self):
rules = []
while self.i < len(self.s):
try:
self.whitespace()
selector = self.selector()
self.literal("{")
self.whitespace()
body = self.body()
self.literal("}")
rules.append((selector, body))
except Exception:
why = self.ignore_until(["}"])
if why == "}":
self.literal("}")
self.whitespace()
else:
break
return rules
@classmethod
def convert_selector_to_json(self, selector):
if isinstance(selector, TagSelector):
return ["tag", selector.tag, selector.priority]
elif isinstance(selector, DescendantSelector):
return ["descendant", self.convert_selector_to_json(selector.ancestor), self.convert_selector_to_json(selector.descendant)]
@classmethod
def get_selector_from_json(self, selector_list):
if selector_list[0] == "tag":
selector = TagSelector(selector_list[1])
selector.priority = selector_list[2]
return selector
elif selector_list[0] == "descendant":
return DescendantSelector(self.get_selector_from_json(selector_list[1]), self.get_selector_from_json(selector_list[2]))
@classmethod
def to_json(self, rules_list: list[tuple[TagSelector | DescendantSelector, dict[str, str]]]):
return [[self.convert_selector_to_json(rule[0]), rule[1]] for rule in rules_list]
@classmethod
def from_json(self, rules_list):
return [(self.get_selector_from_json(rule[0]), rule[1]) for rule in rules_list]
def style(node, rules):
node.style = {}
for property, default_value in INHERITED_PROPERTIES.items():
if node.parent:
node.style[property] = node.parent.style[property]
else:
node.style[property] = default_value
for selector, body in rules:
if not selector.matches(node): continue
for property, value in body.items():
node.style[property] = value
if isinstance(node, Element) and "style" in node.attributes:
pairs = CSSParser(node.attributes["style"]).body()
for property, value in pairs.items():
node.style[property] = value
if node.style["font-size"].endswith("%"):
if node.parent:
parent_font_size = node.parent.style["font-size"]
else:
parent_font_size = INHERITED_PROPERTIES["font-size"]
node_pct = float(node.style["font-size"][:-1]) / 100
parent_px = float(parent_font_size[:-2])
node.style["font-size"] = str(node_pct * parent_px) + "px"
for child in node.children:
style(child, rules)
def tree_to_list(tree, list):
list.append(tree)
for child in tree.children:
tree_to_list(child, list)
return list
def replace_symbols(text):
for key, value in html.entities.html5.items():
text = text.replace(f"&{key};", value)
return text

View File

@@ -1,357 +0,0 @@
import arcade, pyglet, platform
from utils.constants import BLOCK_ELEMENTS, token_pattern, emoji_pattern, INHERITED_PROPERTIES
from utils.utils import get_color_from_name, hex_to_rgb
from http_client.connection import HTTPClient
from http_client.html_parser import CSSParser, Text, Element, style, cascade_priority
from pyglet.font.base import Font as BaseFont
from functools import lru_cache
HSTEP = 13
VSTEP = 18
SPACE_MULTIPLIER = 0.25 if not platform.system() == "Windows" else 0.33
font_cache = {}
def ensure_font(font_family, size, weight, style, emoji):
if not (font_family, size, weight, style, emoji) in font_cache:
font_cache[(font_family, size, weight, style, emoji)] = pyglet.font.load(font_family if pyglet.font.have_font(font_family) else "Arial", size, weight, style == "italic") if not emoji else pyglet.font.load("OpenMoji Color", size, weight, style == "italic")
return font_cache[(font_family, size, weight, style, emoji)]
@lru_cache
def get_space_width(font: BaseFont):
return font.get_text_size("a")[0] * SPACE_MULTIPLIER # i have to do this because space width is 0 on Windows (of course, why wouldnt Windows ruin everything?)
class DrawText:
def __init__(self, x1, y1, text, font, color):
self.top = y1
self.left = x1
self.text = text
self.font = font
self.color = color
class DrawRect:
def __init__(self, x1, y1, width, height, color):
self.top = y1
self.left = x1
self.width = width
self.height = height
self.color = color
class LineLayout:
def __init__(self, node, parent, previous):
self.node = node
self.parent = parent
self.previous = previous
self.children = []
def paint(self):
return []
def layout(self):
self.width = self.parent.width
self.x = self.parent.x
if self.previous:
self.y = self.previous.y + self.previous.height
else:
self.y = self.parent.y
for word in self.children:
word.layout()
if not self.children:
self.height = 0
return
fonts_on_line = [word.font for word in self.children]
max_ascent = max(font.ascent for font in fonts_on_line)
baseline = self.y + 2 * max_ascent
for word in self.children:
word.y = baseline - word.font.ascent
max_descent = min(font.descent for font in fonts_on_line)
self.height = 2 * (max_ascent + max_descent)
class TextLayout():
def __init__(self, node, word, emoji, parent, previous):
self.node = node
self.word = word
self.children = []
self.parent = parent
self.previous = previous
self.emoji = emoji
def paint(self):
color = get_color_from_name(self.node.style["color"])
return [DrawText(self.x, self.y, self.word, self.font, color)]
def layout(self):
weight = self.node.style["font-weight"]
style = self.node.style["font-style"]
font_family = self.node.style["font-family"]
style = "roman" if style == "normal" else style
if not self.node.style["font-size"].endswith("em") and not self.node.style["font-size"].endswith("rem"):
size = int(float(self.node.style["font-size"][:-2]))
else:
size = int(INHERITED_PROPERTIES["font-size"][:-2])
self.font = ensure_font(font_family, size, weight, style, self.emoji)
self.width = self.font.get_text_size(self.word + (" " if not self.emoji else " "))[0]
if self.previous:
space = get_space_width(self.previous.font)
self.x = self.previous.x + space + self.previous.width
else:
self.x = self.parent.x
self.height = self.font.ascent + self.font.descent
class BlockLayout:
def __init__(self, node, parent, previous):
self.node = node
self.parent = parent
self.previous = previous
self.children = []
self.cursor_x = 0
self.x, self.y, self.width, self.height = None, None, None, None
def paint(self):
cmds = []
if self.layout_mode() == "inline":
bgcolor = self.node.style.get("background-color", "transparent")
if bgcolor != "transparent":
rect = DrawRect(self.x, self.y, self.width, self.height, hex_to_rgb(bgcolor) if bgcolor.startswith("#") else get_color_from_name(bgcolor))
cmds.append(rect)
return cmds
def layout_mode(self):
if isinstance(self.node, Text):
return "inline"
elif any([isinstance(child, Element) and child.tag in BLOCK_ELEMENTS for child in self.node.children]):
return "block"
elif self.node.children:
return "inline"
else:
return "block"
def layout(self):
self.x = self.parent.x
self.width = self.parent.width
if self.previous:
self.y = self.previous.y + self.previous.height
else:
self.y = self.parent.y
mode = self.layout_mode()
if mode == "block":
previous = None
for child in self.node.children:
next = BlockLayout(child, self, previous)
self.children.append(next)
previous = next
else:
self.new_line()
self.recurse(self.node)
for child in self.children:
child.layout()
self.height = sum([child.height for child in self.children])
def recurse(self, node):
if isinstance(node, Text):
word_list = [match.group(0) for match in token_pattern.finditer(node.text)]
for word in word_list:
if emoji_pattern.fullmatch(word):
self.word(self.node, word, emoji=True)
else:
self.word(self.node, word)
else:
if node.tag == "br":
self.new_line()
for child in node.children:
self.recurse(child)
def word(self, node, word: str, emoji=False):
weight = node.style["font-weight"]
style = node.style["font-style"]
font_family = node.style["font-family"]
style = "roman" if style == "normal" else style
if not node.style["font-size"].endswith("em") and not node.style["font-size"].endswith("rem"):
size = int(float(node.style["font-size"][:-2]))
else:
size = int(INHERITED_PROPERTIES["font-size"][:-2])
font = ensure_font(font_family, size, weight, style, emoji)
w = font.get_text_size(word + (" " if not emoji else " "))[0]
if self.cursor_x + w > self.width:
self.new_line()
line = self.children[-1]
previous_word = line.children[-1] if line.children else None
text = TextLayout(node, word, emoji, line, previous_word)
line.children.append(text)
self.cursor_x += w + get_space_width(font)
def new_line(self):
self.cursor_x = 0
last_line = self.children[-1] if self.children else None
new_line = LineLayout(self.node, self, last_line)
self.children.append(new_line)
class DocumentLayout:
def __init__(self, node):
self.node = node
self.parent = None
self.children = []
def layout(self):
child = BlockLayout(self.node, self, None)
self.children.append(child)
self.width = arcade.get_window().width - 2 * HSTEP
self.x = HSTEP
self.y = VSTEP
child.layout()
self.height = child.height
def paint(self):
return []
def paint_tree(layout_object, display_list):
display_list.extend(layout_object.paint())
for child in layout_object.children:
paint_tree(child, display_list)
class Renderer():
def __init__(self, http_client: HTTPClient, window):
self.content = ''
self.request_scheme = 'http'
self.window: arcade.Window = window
self.current_window_size = self.window.size
self.http_client = http_client
self.scroll_y = 0
self.scroll_y_speed = 50
self.allow_scroll = False
self.smallest_y = 0
self.document = None
self.widgets: list[pyglet.text.Label] = []
self.text_to_create = []
self.batch = pyglet.graphics.Batch()
def hide_out_of_bounds_labels(self):
for widget in self.widgets:
invisible = (widget.y + (widget.content_height if not isinstance(widget, pyglet.shapes.Rectangle) else widget.height)) > self.window.height * 0.925
# Doing visible flag set manually since it takes a lot of time
if widget.visible:
if invisible:
widget.visible = False
else:
if not invisible:
widget.visible = True
def on_resize(self, width, height):
self.current_window_size = self.window.size
self.http_client.needs_render = True
def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
if not self.allow_scroll:
return
old_y = self.scroll_y
self.scroll_y = max(0, min(abs(self.scroll_y - (scroll_y * self.scroll_y_speed)), abs(self.smallest_y) - (self.window.height * 0.925) + 5)) # flip scroll direction
for widget in self.widgets:
widget.y += (self.scroll_y - old_y)
self.hide_out_of_bounds_labels()
def add_text(self, x, y, text, font, color, multiline=False):
self.widgets.append(
pyglet.text.Label(
text=text,
font_name=font.name,
italic=font.italic,
weight=font.weight,
font_size=font.size,
width=self.window.width * 0.5 if multiline else None,
multiline=multiline,
color=color,
x=x,
y=(self.window.height * 0.925) - y,
batch=self.batch
)
)
if (self.window.height * 0.925) - y < self.smallest_y:
self.smallest_y = y
def add_background(self, left, top, width, height, color):
self.widgets.append(
pyglet.shapes.Rectangle(
left,
(self.window.height * 0.925) - top - height,
width,
height,
color,
batch=self.batch
)
)
def update(self):
if not self.http_client.needs_render:
return
self.http_client.needs_render = False
self.allow_scroll = True
for child in self.widgets:
child.delete()
del child
self.widgets.clear()
self.smallest_y = 0
if self.http_client.view_source or self.http_client.scheme == "file":
self.add_text(x=HSTEP, y=self.window.height * 0.05, text=self.http_client.content_response, font=pyglet.font.load("Roboto", 16), color=arcade.color.BLACK, multiline=True)
elif self.http_client.scheme == "http" or self.http_client.scheme == "https":
if self.http_client.nodes:
style(self.http_client.nodes, sorted(self.http_client.css_rules + CSSParser(open("assets/css/browser.css").read()).parse(), key=cascade_priority))
self.document = DocumentLayout(self.http_client.nodes)
self.document.layout()
self.cmds = []
paint_tree(self.document, self.cmds)
for cmd in self.cmds:
if isinstance(cmd, DrawText):
self.add_text(cmd.left, cmd.top, cmd.text, cmd.font, cmd.color)
elif isinstance(cmd, DrawRect):
self.add_background(cmd.left, cmd.top, cmd.width, cmd.height, cmd.color)
self.hide_out_of_bounds_labels()

View File

@@ -1,181 +0,0 @@
import arcade, arcade.gui, asyncio, pypresence, time, copy, json, asyncio
from utils.constants import discord_presence_id, DEFAULT_HEADERS
from utils.utils import FakePyPresence
from http_client.connection import HTTPClient, resolve_url
from http_client.html_parser import tree_to_list, Text
from http_client.renderer import Renderer
class Tab():
def __init__(self, url, window, tab_button, pypresence_client):
self.pypresence_client = pypresence_client
self.tab_button = tab_button
self.window = window
self.http_client = HTTPClient()
self.renderer = Renderer(self.http_client, window)
self.request(url)
def request(self, url):
if url.startswith("http://") or url.startswith("https://") or url.startswith("view-source:"):
self.http_client.get_request(url, DEFAULT_HEADERS)
elif url.startswith("file://"):
self.http_client.file_request(url)
elif url.startswith("data:text/html,"):
self.http_client.content_response = url.split("data:text/html,")[1]
self.http_client.scheme = "http"
elif url == "about:blank":
self.http_client.content_response = ""
self.http_client.scheme = "http"
elif url == "about:config" or url == "about:settings":
self.settings()
else:
self.http_client.get_request(f"https://{url}", DEFAULT_HEADERS)
self.tab_button.text = url
def settings(self):
from menus.settings import Settings
self.window.show_view(Settings(self.pypresence_client))
class Main(arcade.gui.UIView):
def __init__(self, pypresence_client=None):
super().__init__()
self.pypresence_client = pypresence_client
with open("settings.json", "r") as file:
self.settings_dict = json.load(file)
if self.settings_dict.get('discord_rpc', True):
if self.pypresence_client == None: # Game has started
try:
asyncio.get_event_loop()
except:
asyncio.set_event_loop(asyncio.new_event_loop())
try:
self.pypresence_client = pypresence.Presence(discord_presence_id)
self.pypresence_client.connect()
self.pypresence_client.start_time = time.time()
except:
self.pypresence_client = FakePyPresence()
self.pypresence_client.start_time = time.time()
elif isinstance(self.pypresence_client, FakePyPresence): # the user has enabled RPC in the settings in this session.
# get start time from old object
start_time = copy.deepcopy(self.pypresence_client.start_time)
try:
self.pypresence_client = pypresence.Presence(discord_presence_id)
self.pypresence_client.connect()
self.pypresence_client.start_time = start_time
except:
self.pypresence_client = FakePyPresence()
self.pypresence_client.start_time = start_time
self.pypresence_client.update(state='Browsing', details='In the browser', start=self.pypresence_client.start_time)
else: # game has started, but the user has disabled RPC in the settings.
self.pypresence_client = FakePyPresence()
self.pypresence_client.start_time = time.time()
self.pypresence_client.update(state='Browsing', details='In the browser', start=self.pypresence_client.start_time)
self.tabs: list[Tab] = []
self.tab_buttons = []
self.active_tab = None
def on_show_view(self):
super().on_show_view()
self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1)))
self.navigation_box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=10), anchor_x="center", anchor_y="top")
self.tab_box = self.navigation_box.add(arcade.gui.UIBoxLayout(space_between=5, vertical=False))
self.new_tab_button = self.tab_box.add(arcade.gui.UIFlatButton(text="+", width=self.window.width / 25, height=30, style=arcade.gui.UIFlatButton.DEFAULT_STYLE))
self.new_tab_button.on_click = lambda event: self.new_tab()
self.tab_buttons.append(self.tab_box.add(arcade.gui.UIFlatButton(text="about:blank", width=self.window.width / 7, height=30, style=arcade.gui.UIFlatButton.STYLE_BLUE)))
self.search_bar = self.navigation_box.add(arcade.gui.UIInputText(width=self.window.width * 0.5, height=30, font_name="Roboto", font_size=14, text_color=arcade.color.BLACK, caret_color=arcade.color.BLACK, border_color=arcade.color.BLACK))
default_tab = Tab("about:blank", self.window, self.tab_buttons[0], self.pypresence_client)
self.tabs.append(default_tab)
self.tab_buttons[-1].on_click = lambda event, tab=self.tabs[0]: self.switch_to_tab(tab)
self.switch_to_tab(default_tab)
def search(self):
url = self.search_bar.text
self.active_tab.request(url)
def switch_to_tab(self, tab):
if self.active_tab:
self.active_tab.tab_button.style = arcade.gui.UIFlatButton.DEFAULT_STYLE
self.active_tab = tab
self.active_tab.tab_button.style = arcade.gui.UIFlatButton.STYLE_BLUE
if self.active_tab.renderer.current_window_size != self.window.size:
self.active_tab.renderer.on_resize(self.window.width, self.window.height)
self.window.on_mouse_scroll = self.active_tab.renderer.on_mouse_scroll
http_client = self.active_tab.http_client
if http_client.scheme and http_client.host and http_client.path:
port_str = f':{http_client.port}' if not http_client.port in [80, 443, 0] else ''
self.search_bar.text = f"{http_client.scheme}://{http_client.host}{port_str}{http_client.path}"
def new_tab(self, url="about:blank"):
self.tab_buttons.append(self.tab_box.add(arcade.gui.UIFlatButton(text=url, width=self.window.width / 7, height=30, style=arcade.gui.UIFlatButton.STYLE_BLUE)))
self.tabs.append(Tab(url, self.window, self.tab_buttons[-1], self.pypresence_client))
self.tab_buttons[-1].on_click = lambda event, tab=self.tabs[-1]: self.switch_to_tab(tab)
self.switch_to_tab(self.tabs[-1])
def on_resize(self, width, height):
for tab_button in self.tab_buttons:
tab_button.rect = tab_button.rect.resize(width / 7, 30)
self.active_tab.renderer.on_resize(width, height)
def on_key_press(self, symbol, modifiers):
self.search_bar.text = self.search_bar.text.encode("ascii", "ignore").decode().strip("\n")
if symbol == arcade.key.ENTER:
self.search()
def on_update(self, delta_time):
self.active_tab.renderer.update()
def on_mouse_press(self, x: int, y: int, button: int, modifiers: int):
if not self.active_tab.renderer.document:
return
y -= self.active_tab.renderer.scroll_y
objs = [
obj for obj in tree_to_list(self.active_tab.renderer.document, [])
if obj.x <= x < obj.x + obj.width
and ((self.window.height * 0.925) - obj.y - obj.height) <= y < ((self.window.height * 0.925) - obj.y)
]
if not objs:
return
elt = objs[-1].node
while elt:
if isinstance(elt, Text):
pass
elif elt.tag == "a" and "href" in elt.attributes:
url = resolve_url(self.active_tab.http_client.scheme, self.active_tab.http_client.host, self.active_tab.http_client.port, self.active_tab.http_client.path, elt.attributes["href"])
self.new_tab(url)
return
elt = elt.parent
def on_draw(self):
super().on_draw()
self.active_tab.renderer.batch.draw()

View File

@@ -1,282 +0,0 @@
import copy, pypresence, json
import arcade, arcade.gui
from utils.constants import button_style, dropdown_style, slider_style, settings, discord_presence_id, settings_start_category
from utils.utils import FakePyPresence
from utils.preload import button_texture, button_hovered_texture
from arcade.gui import UIBoxLayout, UIAnchorLayout
class Settings(arcade.gui.UIView):
def __init__(self, pypresence_client):
super().__init__()
with open("settings.json", "r") as file:
self.settings_dict = json.load(file)
self.pypresence_client = pypresence_client
self.pypresence_client.update(state='In Settings', details='Modifying Settings', start=self.pypresence_client.start_time)
self.slider_labels = {}
self.sliders = {}
self.on_radiobuttons = {}
self.off_radiobuttons = {}
self.current_category = settings_start_category
self.modified_settings = {}
def create_layouts(self):
self.anchor = self.add_widget(UIAnchorLayout(size_hint=(1, 1)))
self.box = UIBoxLayout(space_between=50, align="center", vertical=False)
self.anchor.add(self.box, anchor_x="center", anchor_y="top", align_x=10, align_y=-75)
self.top_box = UIBoxLayout(space_between=self.window.width / 160, vertical=False)
self.anchor.add(self.top_box, anchor_x="left", anchor_y="top", align_x=10, align_y=-10)
self.key_layout = self.box.add(UIBoxLayout(space_between=20, align='left'))
self.value_layout = self.box.add(UIBoxLayout(space_between=13, align='left'))
def on_show_view(self):
super().on_show_view()
self.create_layouts()
self.ui.push_handlers(self)
self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50)
self.back_button.on_click = lambda event: self.main_exit()
self.top_box.add(self.back_button)
self.display_categories()
self.display_category(settings_start_category)
def display_categories(self):
for category in settings:
category_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text=category, style=button_style, width=self.window.width / 10, height=50)
if not category == "Credits":
category_button.on_click = lambda event, category=category: self.display_category(category)
else:
category_button.on_click = lambda event: self.credits()
self.top_box.add(category_button)
def display_category(self, category):
if hasattr(self, 'apply_button'):
self.anchor.remove(self.apply_button)
del self.apply_button
if hasattr(self, 'credits_label'):
self.anchor.remove(self.credits_label)
del self.credits_label
self.current_category = category
self.key_layout.clear()
self.value_layout.clear()
for setting in settings[category]:
label = arcade.gui.UILabel(text=setting, font_name="Roboto", font_size=28, text_color=arcade.color.BLACK )
self.key_layout.add(label)
setting_dict = settings[category][setting]
if setting_dict['type'] == "option":
dropdown = arcade.gui.UIDropdown(options=setting_dict['options'], width=200, height=50, default=self.settings_dict.get(setting_dict["config_key"], setting_dict["options"][0]), active_style=dropdown_style, dropdown_style=dropdown_style, primary_style=dropdown_style)
dropdown.on_change = lambda _, setting=setting, dropdown=dropdown: self.update(setting, dropdown.value, "option")
self.value_layout.add(dropdown)
elif setting_dict['type'] == "bool":
button_layout = self.value_layout.add(arcade.gui.UIBoxLayout(space_between=50, vertical=False))
on_radiobutton = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text="ON", style=button_style, width=150, height=50)
self.on_radiobuttons[setting] = on_radiobutton
on_radiobutton.on_click = lambda _, setting=setting: self.update(setting, True, "bool")
button_layout.add(on_radiobutton)
off_radiobutton = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text="OFF", style=button_style, width=150, height=50)
self.off_radiobuttons[setting] = off_radiobutton
off_radiobutton.on_click = lambda _, setting=setting: self.update(setting, False, "bool")
button_layout.add(off_radiobutton)
if self.settings_dict.get(setting_dict["config_key"], setting_dict["default"]):
self.set_highlighted_style(on_radiobutton)
self.set_normal_style(off_radiobutton)
else:
self.set_highlighted_style(off_radiobutton)
self.set_normal_style(on_radiobutton)
elif setting_dict['type'] == "slider":
if setting == "FPS Limit":
if self.settings_dict.get(setting_dict["config_key"]) == 0:
label_text = "FPS Limit: Disabled"
else:
label_text = f"FPS Limit: {self.settings_dict.get(setting_dict['config_key'], setting_dict['default'])}"
else:
label_text = f"{setting}: {int(self.settings_dict.get(setting_dict['config_key'], setting_dict['default']))}"
label.text = label_text
self.slider_labels[setting] = label
slider = arcade.gui.UISlider(width=400, height=50, value=self.settings_dict.get(setting_dict["config_key"], setting_dict["default"]), min_value=setting_dict['min'], max_value=setting_dict['max'], style=slider_style)
slider.on_change = lambda _, setting=setting, slider=slider: self.update(setting, slider.value, "slider")
self.sliders[setting] = slider
self.value_layout.add(slider)
self.apply_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Apply', style=button_style, width=200, height=100)
self.apply_button.on_click = lambda event: self.apply_settings()
self.anchor.add(self.apply_button, anchor_x="right", anchor_y="bottom", align_x=-10, align_y=10)
def apply_settings(self):
for config_key, value in self.modified_settings.items():
self.settings_dict[config_key] = value
if self.settings_dict['window_mode'] == "Fullscreen":
self.window.set_fullscreen(True)
else:
self.window.set_fullscreen(False)
width, height = map(int, self.settings_dict['resolution'].split('x'))
self.window.set_size(width, height)
if self.settings_dict['vsync']:
self.window.set_vsync(True)
display_mode = self.window.display.get_default_screen().get_mode()
refresh_rate = display_mode.rate
self.window.set_update_rate(1 / refresh_rate)
self.window.set_draw_rate(1 / refresh_rate)
elif not self.settings_dict['fps_limit'] == 0:
self.window.set_vsync(False)
self.window.set_update_rate(1 / self.settings_dict['fps_limit'])
self.window.set_draw_rate(1 / self.settings_dict['fps_limit'])
else:
self.window.set_vsync(False)
self.window.set_update_rate(1 / 99999999)
self.window.set_draw_rate(1 / 99999999)
if self.settings_dict['discord_rpc']:
if isinstance(self.pypresence_client, FakePyPresence): # the user has enabled RPC in the settings in this session.
start_time = copy.deepcopy(self.pypresence_client.start_time)
self.pypresence_client.close()
del self.pypresence_client
try:
self.pypresence_client = pypresence.Presence(discord_presence_id)
self.pypresence_client.connect()
self.pypresence_client.update(state='In Settings', details='Modifying Settings', start=start_time)
self.pypresence_client.start_time = start_time
except:
self.pypresence_client = FakePyPresence()
self.pypresence_client.start_time = start_time
else:
if not isinstance(self.pypresence_client, FakePyPresence):
start_time = copy.deepcopy(self.pypresence_client.start_time)
self.pypresence_client.update()
self.pypresence_client.close()
del self.pypresence_client
self.pypresence_client = FakePyPresence()
self.pypresence_client.start_time = start_time
self.ui_cleanup()
self.ui = arcade.gui.UIManager()
self.ui.enable()
self.create_layouts()
self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50)
self.back_button.on_click = lambda event: self.main_exit()
self.top_box.add(self.back_button)
self.display_categories()
self.display_category(self.current_category)
with open("settings.json", "w") as file:
file.write(json.dumps(self.settings_dict, indent=4))
def update(self, setting=None, button_state=None, setting_type="bool"):
setting_dict = settings[self.current_category][setting]
config_key = settings[self.current_category][setting]["config_key"]
if setting_type == "option":
self.modified_settings[config_key] = button_state
elif setting_type == "bool":
self.modified_settings[config_key] = button_state
if button_state:
self.set_highlighted_style(self.on_radiobuttons[setting])
self.set_normal_style(self.off_radiobuttons[setting])
else:
self.set_highlighted_style(self.off_radiobuttons[setting])
self.set_normal_style(self.on_radiobuttons[setting])
elif setting_type == "slider":
new_value = int(button_state)
self.modified_settings[config_key] = new_value
self.sliders[setting].value = new_value
if setting == "FPS Limit":
if new_value == 0:
label_text = "FPS Limit: Disabled"
else:
label_text = f"FPS Limit: {str(new_value).rjust(8)}"
else:
label_text = f"{setting}: {str(new_value).rjust(8)}"
self.slider_labels[setting].text = label_text
def credits(self):
if hasattr(self, 'apply_button'):
self.anchor.remove(self.apply_button)
del self.apply_button
if hasattr(self, 'credits_label'):
self.anchor.remove(self.credits_label)
del self.credits_label
self.key_layout.clear()
self.value_layout.clear()
with open('CREDITS', 'r') as file:
text = file.read()
if self.window.width == 3840:
font_size = 30
elif self.window.width == 2560:
font_size = 20
elif self.window.width == 1920:
font_size = 17
elif self.window.width >= 1440:
font_size = 14
else:
font_size = 12
self.credits_label = arcade.gui.UILabel(text=text, text_color=arcade.color.BLACK, font_name="Roboto", font_size=font_size, align="center", multiline=True)
self.key_layout.add(self.credits_label)
def set_highlighted_style(self, element):
element.texture = button_hovered_texture
element.texture_hovered = button_texture
def set_normal_style(self, element):
element.texture_hovered = button_hovered_texture
element.texture = button_texture
def main_exit(self):
from menus.main import Main
self.window.show_view(Main(self.pypresence_client))
def ui_cleanup(self):
self.ui.clear()
del self.ui

View File

@@ -1,12 +0,0 @@
[project]
name = "csd4ni3l-browser"
version = "0.1.0"
description = "csd4ni3l-browser"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"arcade>=3.3.3",
"certifi>=2025.7.14",
"pypresence>=4.3.0",
"ujson>=5.10.0",
]

View File

@@ -1,26 +0,0 @@
# This file was autogenerated by uv via the following command:
# uv pip compile pyproject.toml -o requirements.txt
arcade==3.3.3
# via csd4ni3l-browser (pyproject.toml)
attrs==25.3.0
# via pytiled-parser
certifi==2025.7.14
# via csd4ni3l-browser (pyproject.toml)
cffi==1.17.1
# via pymunk
pillow==11.3.0
# via arcade
pycparser==2.22
# via cffi
pyglet==2.1.6
# via arcade
pymunk==6.9.0
# via arcade
pypresence==4.3.0
# via csd4ni3l-browser (pyproject.toml)
pytiled-parser==2.2.9
# via arcade
typing-extensions==4.14.1
# via pytiled-parser
ujson==5.10.0
# via csd4ni3l-browser (pyproject.toml)

131
run.py
View File

@@ -1,131 +0,0 @@
import os, certifi
os.environ['SSL_CERT_FILE'] = certifi.where() # Fix SSL not working
import pyglet
pyglet.options['shadow_window'] = False # Fix double window issue on Wayland
pyglet.options.debug_gl = False
import logging, datetime, os, json, sys, arcade, platform
# Set up paths BEFORE importing modules that load assets
script_dir = os.path.dirname(os.path.abspath(__file__))
pyglet.resource.path.append(script_dir)
pyglet.font.add_directory(os.path.join(script_dir, 'assets', 'fonts'))
from utils.utils import get_closest_resolution, print_debug_info, on_exception
from utils.constants import log_dir, menu_background_color
from menus.main import Main
from arcade.experimental.controller_window import ControllerWindow
sys.excepthook = on_exception
if not log_dir in os.listdir():
os.makedirs(log_dir)
if not os.path.exists("html_cache"):
os.makedirs("html_cache")
if not os.path.exists("css_cache"):
os.makedirs("css_cache")
while len(os.listdir(log_dir)) >= 5:
files = [(file, os.path.getctime(os.path.join(log_dir, file))) for file in os.listdir(log_dir)]
oldest_file = sorted(files, key=lambda x: x[1])[0][0]
os.remove(os.path.join(log_dir, oldest_file))
timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
log_filename = f"debug_{timestamp}.log"
logging.basicConfig(filename=f'{os.path.join(log_dir, log_filename)}', format='%(asctime)s %(name)s %(levelname)s: %(message)s', level=logging.DEBUG)
for logger_name_to_disable in ['arcade', "numba"]:
logging.getLogger(logger_name_to_disable).propagate = False
logging.getLogger(logger_name_to_disable).disabled = True
if os.path.exists('settings.json'):
with open('settings.json', 'r') as settings_file:
settings = json.load(settings_file)
resolution = list(map(int, settings['resolution'].split('x')))
if not settings.get("anti_aliasing", "4x MSAA") == "None":
antialiasing = int(settings.get("anti_aliasing", "4x MSAA").split('x')[0])
else:
antialiasing = 0
# Wayland workaround (can be overridden with environment variable)
if (platform.system() == "Linux" and
os.environ.get("WAYLAND_DISPLAY") and
not os.environ.get("ARCADE_FORCE_MSAA")):
logging.info("Wayland detected - disabling MSAA (set ARCADE_FORCE_MSAA=1 to override)")
antialiasing = 0
fullscreen = settings['window_mode'] == 'Fullscreen'
style = arcade.Window.WINDOW_STYLE_BORDERLESS if settings['window_mode'] == 'borderless' else arcade.Window.WINDOW_STYLE_DEFAULT
vsync = settings['vsync']
fps_limit = settings['fps_limit']
else:
resolution = get_closest_resolution()
antialiasing = 4
# Wayland workaround (can be overridden with environment variable)
if (platform.system() == "Linux" and
os.environ.get("WAYLAND_DISPLAY") and
not os.environ.get("ARCADE_FORCE_MSAA")):
logging.info("Wayland detected - disabling MSAA (set ARCADE_FORCE_MSAA=1 to override)")
antialiasing = 0
fullscreen = False
style = arcade.Window.WINDOW_STYLE_DEFAULT
vsync = True
fps_limit = 0
settings = {
"resolution": f"{resolution[0]}x{resolution[1]}",
"antialiasing": "4x MSAA",
"window_mode": "Windowed",
"vsync": True,
"fps_limit": 60,
"discord_rpc": True
}
with open("settings.json", "w") as file:
file.write(json.dumps(settings))
try:
window = ControllerWindow(width=resolution[0], height=resolution[1], title='csd4ni3lBrowser', samples=antialiasing, antialiasing=antialiasing > 0, fullscreen=fullscreen, vsync=vsync, resizable=False, style=style, visible=False)
except (FileNotFoundError, PermissionError) as e:
logging.warning(f"Controller support unavailable: {e}. Falling back to regular window.")
window = arcade.Window(width=resolution[0], height=resolution[1], title='csd4ni3lBrowser', samples=antialiasing, antialiasing=antialiasing > 0, fullscreen=fullscreen, vsync=vsync, resizable=False, style=style, visible=False)
if vsync:
window.set_vsync(True)
display_mode = window.display.get_default_screen().get_mode()
if display_mode:
refresh_rate = display_mode.rate
else:
refresh_rate = 60
window.set_update_rate(1 / refresh_rate)
window.set_draw_rate(1 / refresh_rate)
elif not fps_limit == 0:
window.set_update_rate(1 / fps_limit)
window.set_draw_rate(1 / fps_limit)
else:
window.set_update_rate(1 / 99999999)
window.set_draw_rate(1 / 99999999)
arcade.set_background_color(menu_background_color)
print_debug_info()
main = Main()
window.show_view(main)
# Make window visible after all setup is complete (helps prevent double window on Wayland)
window.set_visible(True)
logging.debug('Game started.')
arcade.run()
logging.info('Exited with error code 0.')

70
src/constants.rs Normal file
View File

@@ -0,0 +1,70 @@
use std::collections::HashMap;
use std::sync::LazyLock;
// Regex is by AI
// pub const emoji_pattern = re.compile(
// r'['
// r'\U0001F300-\U0001F5FF'
// r'\U0001F600-\U0001F64F'
// r'\U0001F680-\U0001F6FF'
// r'\U0001F700-\U0001F77F'
// r'\U0001F780-\U0001F7FF'
// r'\U0001F800-\U0001F8FF'
// r'\U0001F900-\U0001F9FF'
// r'\U0001FA00-\U0001FA6F'
// r'\U0001FA70-\U0001FAFF'
// r'\u2600-\u26FF'
// r'\u2700-\u27BF'
// r']',
// flags=re.UNICODE
// )
// token_pattern = re.compile(
// f'({emoji_pattern.pattern})' // emoji
// r'|(\w+)' // word
// r'|([^\w\s])', // punctuation
// flags=re.UNICODE
// )
pub const BLOCK_ELEMENTS: [&str; 37] = [
"html", "body", "article", "section", "nav", "aside",
"h1", "h2", "h3", "h4", "h5", "h6", "hgroup", "header",
"footer", "address", "p", "hr", "pre", "blockquote",
"ol", "ul", "menu", "li", "dl", "dt", "dd", "figure",
"figcaption", "main", "div", "table", "form", "fieldset",
"legend", "details", "summary"
];
pub const SELF_CLOSING_TAGS: [&str; 14] = [
"area", "base", "br", "col", "embed", "hr", "img", "input",
"link", "meta", "param", "source", "track", "wbr",
];
pub const HEAD_TAGS: [&str; 9] = [
"base", "basefont", "bgsound", "noscript",
"link", "meta", "title", "style", "script",
];
pub const HEAD_TAGS_EXTRA: [&str; 10] = [
"base", "basefont", "bgsound", "noscript",
"link", "meta", "title", "style", "script", "/head"
];
pub static INHERITED_PROPERTIES: LazyLock<HashMap<String, String>> = LazyLock::new(|| HashMap::from([
(String::from("font-family"), String::from("Arial")),
(String::from("font-size"), String::from("16px")),
(String::from("font-style"), String::from("normal")),
(String::from("font-weight"), String::from("normal")),
(String::from("color"), String::from("black")),
(String::from("display"), String::from("inline")),
(String::from("width"), String::from("auto")),
(String::from("height"), String::from("auto"))
]));
pub static DEFAULT_HEADERS: LazyLock<HashMap<String, String>> = LazyLock::new(|| HashMap::from([
(String::from("User-Agent"), String::from("Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0")),
(String::from("Accept"), String::from("text/html")),
(String::from("Sec-Fetch-Dest"), String::from("document")),
(String::from("Sec-Fetch-Mode"), String::from("navigate")),
(String::from("Sec-Fetch-Site"), String::from("none"))
]));

View File

@@ -0,0 +1,382 @@
use std::{collections::HashMap, net::{Shutdown, TcpStream}, io::{Read, Write}, fs};
use native_tls::{TlsConnector, TlsStream};
use base64::{engine::general_purpose::URL_SAFE, Engine as _};
use crate::http_client::html_parser::{CSSCache, CSSParser, CSSSelector, HTML, Node, get_inline_styles, tree_to_vec};
use serde_json;
pub enum Connection {
Plain(TcpStream),
Tls(TlsStream<TcpStream>),
}
impl Connection {
fn shutdown(&mut self, how: Shutdown) -> std::io::Result<()> {
match self {
Connection::Plain(s) => s.shutdown(how),
Connection::Tls(s) => {let _ = s.shutdown(); Ok(())},
}
}
}
impl Read for Connection {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
match self {
Connection::Plain(s) => s.read(buf),
Connection::Tls(s) => s.read(buf),
}
}
}
impl Write for Connection {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
match self {
Connection::Plain(s) => s.write(buf),
Connection::Tls(s) => s.write(buf),
}
}
fn flush(&mut self) -> std::io::Result<()> {
match self {
Connection::Plain(s) => s.flush(),
Connection::Tls(s) => s.flush(),
}
}
}
pub fn resolve_url(scheme: &str, host: &str, port: u16, path: &str, url: &str) -> String {
let mut new_url = url;
if new_url.contains("://") {
return new_url.to_string();
}
let resolved_path = if !new_url.starts_with("/") {
let mut dir = path.rsplitn(2, '/').nth(1).unwrap_or("");
while new_url.starts_with("../") {
new_url = new_url.strip_prefix("../").unwrap();
if dir.contains('/') {
dir = dir.rsplitn(2, '/').nth(1).unwrap_or("");
}
}
format!("{}/{}", dir, new_url)
} else {
new_url.to_string()
};
if resolved_path.starts_with("//") {
format!("{}:{}", scheme, resolved_path)
}
else {
format!("{}://{}:{}{}", scheme, host, port, resolved_path)
}
}
pub struct HTTPClient {
pub scheme: String,
pub host: String,
pub path: String,
pub port: u16,
pub request_headers: HashMap<String, String>,
pub response_explanation: Option<String>,
pub response_headers: HashMap<String, String>,
pub response_http_version: Option<String>,
pub response_status: Option<u32>,
pub node: Option<Node>,
pub css_rules: Vec<(CSSSelector, HashMap<String, String>)>,
pub content_response: String,
pub view_source: bool,
pub redirect_count: u32,
pub needs_render: bool,
pub tcp_stream: Option<Connection>
}
impl HTTPClient {
pub fn new() -> HTTPClient {
HTTPClient {
scheme: String::new(),
host: String::new(),
path: String::new(),
port: 0,
request_headers: HashMap::new(),
response_explanation: None,
response_headers: HashMap::new(),
response_http_version: None,
response_status: None,
node: None,
css_rules: Vec::new(),
content_response: String::new(),
view_source: false,
redirect_count: 0,
needs_render: false,
tcp_stream: None
}
}
pub fn file_request(&mut self, url: &String) {
self.content_response = fs::read_to_string(url.split_once("file://").unwrap().1).unwrap();
}
pub fn get_request(&mut self, url: &String, headers: HashMap<String, String>, css: bool) {
let mut parsed_url = url.clone();
if parsed_url.starts_with("view-source:") {
parsed_url = parsed_url.split_once("view-source:").unwrap().1.to_string();
self.view_source = true;
}
else {
self.view_source = false;
}
let (scheme_str, parsed_url_parts) = parsed_url.split_once("://").unwrap();
self.scheme = scheme_str.to_string();
if !(parsed_url_parts.contains("/")) {
self.host = parsed_url_parts.to_string();
self.path = "/".to_string();
}
else {
let (host_str, path_str) = parsed_url_parts.split_once("/").unwrap();
self.host = host_str.to_string();
self.path = format!("/{}", path_str.to_string());
}
if self.host.contains(":") {
let temp_host = self.host.clone();
let (host_str, port_str) = temp_host.split_once(":").unwrap();
self.host = host_str.to_string();
self.port = port_str.parse().unwrap();
}
else {
if self.scheme == "http" {
self.port = 80;
}
else {
self.port = 443;
}
}
self.request_headers = headers;
self.response_explanation = None;
self.response_headers = HashMap::new();
self.response_http_version = None;
self.response_status = None;
self.content_response = "".to_string();
self.tcp_stream = None;
if self.request_headers.contains_key("Host") {
self.request_headers.remove("Host");
}
self.request_headers.insert("Host".to_string(), self.host.clone());
let html_cache_key = URL_SAFE.encode(format!("{}_{}_{}_{}", self.scheme, self.host, self.port, self.path).as_bytes());
let html_cache_path = format!("html_cache/{}.html", html_cache_key);
if std::fs::exists(html_cache_path.clone()).unwrap() {
self.content_response = fs::read_to_string(html_cache_path).unwrap();
self.parse();
return;
}
let tcp = TcpStream::connect(format!("{}:{}", self.host, self.port.to_string())).unwrap();
if self.scheme == "https" {
let connector = TlsConnector::new().unwrap();
self.tcp_stream = Some(Connection::Tls(connector.connect(self.host.as_str(), tcp).unwrap()));
}
else {
self.tcp_stream = Some(Connection::Plain(tcp));
}
let request_header_lines: String = self.request_headers
.iter()
.map(|(header_name, header_value)|{
format!("{}: {}", header_name, header_value)
})
.collect::<Vec<_>>()
.join("\r\n");
let request = format!("GET {} HTTP/1.0\r\n{}\r\n\r\n", self.path, request_header_lines);
self.tcp_stream.as_mut().unwrap().write_all(request.as_bytes()).unwrap();
self.receive_response(css); // TODO: use threading
}
fn receive_response(&mut self, css: bool) {
let mut temp_buffer = [0; 16384];
let mut headers_parsed: bool = false;
let mut content_length: Option<usize> = None;
loop {
let bytes_read = self.tcp_stream.as_mut().unwrap().read(&mut temp_buffer).unwrap_or(0);
if bytes_read == 0 {
println!("Connection closed by peer.");
break;
}
if !headers_parsed {
let header_end_index = temp_buffer[..bytes_read].windows(4).position(|window| {window == b"\r\n\r\n"});
if let Some(header_end_index) = header_end_index {
let header_data = std::str::from_utf8(&temp_buffer[..header_end_index]).unwrap_or("");
let body_data = &temp_buffer[header_end_index + 4..bytes_read]; // +4 for the \r\n\r\n
self._parse_headers(header_data.to_string());
headers_parsed = true;
let content_length_header = self.response_headers.get("content-length");
if let Some(content_length_header) = content_length_header {
content_length = Some(content_length_header.parse().unwrap());
}
self.content_response = std::str::from_utf8(&body_data).unwrap_or("").to_string(); // Assuming body is UTF-8
if !content_length.is_none() && body_data.len() >= content_length.unwrap() {
break;
}
else if content_length.is_none() {}
}
else {
continue;
}
}
else {
self.content_response.push_str(std::str::from_utf8(&temp_buffer[..bytes_read]).unwrap_or(""));
if !content_length.is_none() && self.content_response.len() >= content_length.unwrap() {
break;
}
}
};
if let Some(ref mut stream) = self.tcp_stream {
stream.shutdown(Shutdown::Both).ok();
}
self.tcp_stream = None;
if 300 <= self.response_status.unwrap() && self.response_status.unwrap() < 400 {
if self.redirect_count >= 4 {
return;
}
self.redirect_count += 1;
let headers = self.request_headers.clone();
let location = self.response_headers.get("location")
.cloned()
.unwrap_or("/".to_string());
if location.starts_with("http") || location.starts_with("https") {
self.get_request(&location, headers, false);
}
else {
self.get_request(&format!("{}://{}{}", self.scheme, self.host, location), headers, false);
}
}
else {
self.redirect_count = 0;
}
if !css {
if !(300..400).contains(&self.response_status.unwrap_or(0)) {
self.parse();
}
}
}
fn _parse_headers(&mut self, header_data: String) {
let lines: Vec<&str> = header_data.lines().collect();
if lines.is_empty() {
println!("Received empty header data.");
return
}
let response_status_line = lines[0];
let mut parts = response_status_line.splitn(3, ' ');
self.response_http_version = Some(parts.next().unwrap().to_string());
self.response_status = Some(parts.next().unwrap().parse().unwrap());
let explanation_parts: Vec<&str> = parts.collect();
self.response_explanation = Some(explanation_parts.join(" "));
let mut headers = HashMap::new();
for i in 1..lines.len() {
let line = &lines[i];
if line.is_empty() {
break;
}
let (header_name, value) = line.split_once(":").unwrap();
headers.insert(header_name.trim().to_lowercase().to_string(), value.trim().to_string());
}
self.response_headers = headers;
}
pub fn parse(&mut self) {
self.css_rules.clear();
let html_cache_key = URL_SAFE.encode(format!("{}_{}_{}_{}", self.scheme, self.host, self.port, self.path).as_bytes());
let html_cache_path = format!("html_cache/{}.html", html_cache_key);
if std::fs::exists(html_cache_path.clone()).unwrap() {
self.content_response = std::fs::read_to_string(html_cache_path).unwrap();
}
else {
let _ = std::fs::write(html_cache_path, self.content_response.clone());
}
let original_scheme = self.scheme.clone();
let original_host = self.host.clone();
let original_port = self.port;
let original_path = self.path.clone();
let original_response = self.content_response.clone();
self.node = Some(Node::Element(HTML::new(self.content_response.clone()).parse()));
let mut flattened_tree = vec![];
tree_to_vec(self.node.as_ref().unwrap(), &mut flattened_tree);
let css_links: Vec<String> = flattened_tree.iter()
.filter(|node| {
matches!(node, Node::Element(_)) && node.tag().unwrap() == "link".to_string() && node.attributes().unwrap().get("rel").unwrap() == &"stylesheet".to_string() && node.attributes().unwrap().get("href").is_some()
})
.map(|node: &&Node| {
node.attributes().unwrap()["href"].clone()
}).collect();
for css_link in css_links {
self.content_response.clear();
// we need to include the other variables so for example /styles.css wouldnt be cached for all websites
let css_cache_key = URL_SAFE.encode(format!("{}_{}_{}_{}", self.scheme, self.host, self.port, css_link).as_bytes());
let css_cache_path = format!("css_cache/{}.json", css_cache_key);
let rules: Vec<(CSSSelector, HashMap<String, String>)> = if std::path::Path::new(&css_cache_path).exists() {
let css_cache_content = std::fs::read_to_string(&css_cache_path).unwrap();
let json: CSSCache = serde_json::from_str(&css_cache_content).unwrap();
json.css_cache
}
else {
let resolved = resolve_url(self.scheme.as_str(), self.host.as_str(), self.port, self.path.as_str(), css_link.as_str());
let headers = self.request_headers.clone();
self.get_request(&resolved, headers, true);
let parsed_css = CSSParser::new(self.content_response.clone()).parse();
let json = CSSCache { css_cache: parsed_css };
let _ = std::fs::write(&css_cache_path, serde_json::to_string(&json).unwrap());
json.css_cache
};
self.css_rules.extend(rules);
}
self.css_rules.extend(get_inline_styles(self.node.as_ref().unwrap()));
self.scheme = original_scheme;
self.host = original_host;
self.port = original_port;
self.path = original_path;
self.content_response = original_response;
self.needs_render = true;
}
}

View File

@@ -0,0 +1,507 @@
use crate::constants::*;
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
#[derive(Clone)]
pub struct Element {
pub tag: String,
pub children: Vec<Node>,
pub attributes: HashMap<String, String>,
pub parent: Option<Box<Node>>
}
#[derive(Clone)]
pub struct Text {
pub text: String,
pub children: Vec<Node>,
pub parent: Option<Box<Node>>
}
#[derive(Clone)]
pub enum Node {
Element(Element),
Text(Text),
}
impl Node {
pub fn attributes(&self) -> Option<&HashMap<String, String>> {
match self {
Node::Element(e) => Some(&e.attributes),
Node::Text(_) => None
}
}
pub fn tag(&self) -> Option<&str> {
match self {
Node::Element(e) => Some(&e.tag),
Node::Text(_) => None
}
}
pub fn children(&self) -> &Vec<Node> {
match self {
Node::Element(e) => &e.children,
Node::Text(t) => &t.children
}
}
pub fn parent(&self) -> Option<&Node> {
match self {
Node::Text(t) => t.parent.as_deref(),
Node::Element(e) => e.parent.as_deref(),
}
}
}
pub struct HTML {
pub raw_html: String,
pub unfinished: Vec<Element>,
}
impl HTML {
pub fn new(raw_html: String) -> Self {
HTML {
raw_html,
unfinished: Vec::new(),
}
}
pub fn parse(&mut self) -> Element {
let mut text = String::new();
let mut in_tag = false;
let html_content: Vec<char> = self.raw_html.chars().collect();
for c in html_content {
if c == '<' {
in_tag = true;
if (self.unfinished.is_empty() || self.unfinished.last().unwrap().tag != "style") && !text.is_empty() {
self.add_text(&text);
}
text.clear();
} else if c == '>' {
in_tag = false;
self.add_tag(&text);
text.clear();
} else {
text.push(c);
}
}
if !in_tag && !text.is_empty() {
self.add_text(&text);
}
self.finish()
}
fn add_text(&mut self, text: &str) {
let trimmed = text.trim();
if trimmed.is_empty() {
return;
}
self.implicit_tags(None);
if let Some(parent) = self.unfinished.last_mut() {
let node = Node::Text(Text {
text: text.to_string(),
parent: Some(Box::new(Node::Element(parent.clone()))),
children: Vec::new()
});
parent.children.push(node);
}
}
fn get_attributes(&self, text: &str) -> (String, HashMap<String, String>) {
let parts: Vec<&str> = text.split_whitespace().collect();
if parts.is_empty() {
return (String::new(), HashMap::new());
}
let tag = parts[0].to_lowercase();
let mut attributes: HashMap<String, String> = HashMap::new();
for attrpair in &parts[1..] {
if attrpair.contains('=') {
let mut split = attrpair.splitn(2, '=');
let key = split.next().unwrap_or("");
let mut value = split.next().unwrap_or("");
if value.len() > 2 && (value.starts_with('\'') || value.starts_with('"')) {
value = &value[1..value.len() - 1];
}
attributes.insert(key.to_lowercase(), value.to_string());
} else {
attributes.insert(attrpair.to_lowercase(), String::new());
}
}
(tag, attributes)
}
fn add_tag(&mut self, tag: &str) {
let (tag_name, attributes) = self.get_attributes(tag);
if tag_name.starts_with('!') {
return;
}
self.implicit_tags(Some(&tag_name));
if tag_name.starts_with('/') {
if self.unfinished.len() == 1 {
return;
}
let node = self.unfinished.pop().unwrap();
if let Some(parent) = self.unfinished.last_mut() {
parent.children.push(Node::Element(node));
}
} else if SELF_CLOSING_TAGS.contains(&tag_name.as_str()) {
let parent_node = self.unfinished.last().map(|p| Box::new(Node::Element(p.clone())));
if let Some(parent) = self.unfinished.last_mut() {
let node = Element {
tag: tag_name,
attributes,
children: Vec::new(),
parent: parent_node,
};
parent.children.push(Node::Element(node));
}
} else {
let node = Element {
tag: tag_name,
attributes,
children: Vec::new(),
parent: self.unfinished.last().map(|p| Box::new(Node::Element(p.clone()))),
};
self.unfinished.push(node);
}
}
fn implicit_tags(&mut self, tag: Option<&str>) {
loop {
let open_tags: Vec<String> = self.unfinished.iter().map(|node| node.tag.clone()).collect();
if open_tags.is_empty() && tag != Some("html") {
self.add_tag("html");
} else if open_tags == vec!["html"] && !matches!(tag, Some("head") | Some("body") | Some("/html")) {
if let Some(tag_str) = tag {
if HEAD_TAGS.contains(&tag_str) {
self.add_tag("head");
} else {
self.add_tag("body");
}
} else {
self.add_tag("body");
}
} else if open_tags == vec!["html", "head"] && !matches!(tag, Some(t) if HEAD_TAGS_EXTRA.contains(&t)) {
self.add_tag("/head");
} else {
break;
}
}
}
fn finish(&mut self) -> Element {
if self.unfinished.is_empty() {
self.implicit_tags(None);
}
while self.unfinished.len() > 1 {
let node = self.unfinished.pop().unwrap();
if let Some(parent) = self.unfinished.last_mut() {
parent.children.push(Node::Element(node));
}
}
self.unfinished.pop().unwrap()
}
}
#[derive(Serialize, Deserialize)]
pub struct TagSelector {
pub tag: String,
pub priority: i32
}
impl TagSelector {
pub fn matches(&self, node: &Node) -> bool{
if let Node::Element(elem) = node {
self.tag == elem.tag
} else {
false
}
}
pub fn new(tag: String) -> TagSelector {
TagSelector {
tag,
priority: 1
}
}
}
#[derive(Serialize, Deserialize)]
pub struct DescendantSelector {
pub ancestor: Box<CSSSelector>,
pub descendant: Box<CSSSelector>,
pub priority: i32
}
impl DescendantSelector {
pub fn new(ancestor: CSSSelector, descendant: CSSSelector) -> DescendantSelector {
let new_priority = ancestor.priority() + descendant.priority();
DescendantSelector {
ancestor: Box::new(ancestor),
descendant: Box::new(descendant),
priority: new_priority
}
}
pub fn matches(&self, node: &Node) -> bool {
if !self.descendant.matches(node) {
return false;
}
let mut current = node;
loop {
match current.parent() {
Some(parent) => {
if self.ancestor.matches(parent) {
return true;
}
current = parent;
},
None => break
}
}
return false;
}
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum CSSSelector {
TagSelector(TagSelector),
DescendantSelector(DescendantSelector),
}
impl CSSSelector {
pub fn priority(&self) -> i32 {
match self {
CSSSelector::TagSelector(s) => s.priority,
CSSSelector::DescendantSelector(s) => s.priority,
}
}
pub fn matches(&self, node: &Node) -> bool {
match self {
CSSSelector::TagSelector(s) => s.matches(node),
CSSSelector::DescendantSelector(s) => s.matches(node),
}
}
}
pub fn cascade_priority(rule: (CSSSelector, HashMap<String, String>)) -> i32 {
rule.0.priority()
}
pub fn get_inline_styles(node: &Node) -> Vec<(CSSSelector, HashMap<String, String>)> {
let mut all_rules = vec![];
if let Node::Element(elem) = node {
for child in &elem.children {
if let Node::Element(child_elem) = child {
if child_elem.tag == "style" {
if let Some(Node::Text(text_node)) = child_elem.children.first() {
all_rules.extend(CSSParser::new(text_node.text.clone()).parse());
}
}
}
all_rules.extend(get_inline_styles(child));
}
}
all_rules
}
pub struct CSSParser {
chars: Vec<char>,
len: usize,
i: usize
}
impl CSSParser {
pub fn new (s: String) -> CSSParser {
CSSParser {
chars: s.chars().collect(),
len: s.chars().count(),
i: 0
}
}
fn whitespace(&mut self) {
while self.i < self.len && self.chars[self.i].is_whitespace() {
self.i += 1;
}
}
fn literal(&mut self, literal: char) -> Result<(), String> {
if !(self.i < self.len && self.chars[self.i] == literal) {
return Err(format!("Expected '{}'", literal));
}
self.i += 1;
Ok(())
}
fn word(&mut self) -> Result<String, String> {
let start = self.i;
while self.i < self.len {
if self.chars[self.i].is_alphanumeric() || "#-.%".contains(self.chars[self.i]) {
self.i += 1;
}
else {
break
}
}
if !(self.i > start) {
return Err("Parsing error: unexpected word".to_string())
};
Ok(self.chars[start..self.i].iter().collect())
}
fn pair(&mut self) -> Result<(String, String), String> {
let prop = self.word()?;
self.whitespace();
self.literal(':')?;
self.whitespace();
let val = self.word()?;
Ok((prop.to_lowercase(), val))
}
fn ignore_until(&mut self, chars: Vec<char>) -> Option<char> {
while self.i < self.len {
let c = self.chars[self.i];
if chars.contains(&c) {
return Some(c);
}
else {
self.i += 1;
}
}
return None;
}
fn body(&mut self) -> HashMap<String, String> {
let mut pairs = HashMap::new();
while self.i < self.len && self.chars[self.i] != '}' {
match self.pair() {
Ok((prop, val)) => {
pairs.insert(prop, val);
self.whitespace();
let _ = self.literal(';');
self.whitespace();
}
Err(_) => {
let ignore_char: Vec<char> = vec![';', '}'];
if let Some(';') = self.ignore_until(ignore_char) {
let _ = self.literal(';');
self.whitespace();
} else {
break;
}
}
}
}
pairs
}
fn selector(&mut self) -> Result<CSSSelector, String> {
let mut out = CSSSelector::TagSelector(TagSelector::new(self.word()?.to_lowercase()));
self.whitespace();
while self.i < self.len && self.chars[self.i] != '{' {
let tag = self.word()?;
let descendant = CSSSelector::TagSelector(TagSelector::new(tag.to_lowercase()));
out = CSSSelector::DescendantSelector(DescendantSelector::new(out, descendant));
self.whitespace();
}
Ok(out)
}
pub fn parse(&mut self) -> Vec<(CSSSelector, HashMap<String, String>)> {
let mut rules = vec![];
while self.i < self.len {
self.whitespace();
let selector = match self.selector() {
Ok(s) => s,
Err(_) => {
if let Some('}') = self.ignore_until(vec!['}']) {
let _ = self.literal('}');
self.whitespace();
} else {
break;
}
continue;
}
};
if self.literal('{').is_err() {
if let Some('}') = self.ignore_until(vec!['}']) {
let _ = self.literal('}');
self.whitespace();
} else {
break;
}
continue;
};
self.whitespace();
let body = self.body();
let _ = self.literal('}');
rules.push((selector, body));
}
return rules;
}
}
pub fn tree_to_vec<'a>(tree: &'a Node, vec: &mut Vec<&'a Node>) {
vec.push(tree);
for child in tree.children() {
tree_to_vec(child, vec);
}
}
#[derive(Serialize, Deserialize)]
pub struct CSSCache {
pub css_cache: Vec<(CSSSelector, HashMap<String, String>)>
}

3
src/http_client/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod connection;
pub mod html_parser;
pub mod renderer;

View File

@@ -0,0 +1,47 @@
use crate::http_client::connection::HTTPClient;
use bevy_egui::egui::Ui;
enum Widget {
}
struct DocumentLayout {
}
pub struct Renderer {
content: String,
request_scheme: String,
scroll_y: f64,
scroll_y_speed: f64,
smallest_y: f64,
document: Option<DocumentLayout>,
widgets: Vec<Widget>
}
impl Renderer {
pub fn new() -> Renderer {
Renderer {
content: String::new(),
request_scheme: String::new(),
scroll_y: 0.0,
scroll_y_speed: 50.0,
smallest_y: 0.0,
document: None,
widgets: Vec::new()
}
}
fn update_content(&mut self, http_client: &mut HTTPClient) {
self.widgets.clear()
}
pub fn render(&mut self, http_client: &mut HTTPClient, ui: &mut Ui) {
if http_client.needs_render {
self.update_content(http_client);
http_client.needs_render = false;
}
ui.label(http_client.content_response.clone());
}
}

189
src/main.rs Normal file
View File

@@ -0,0 +1,189 @@
use bevy::{log::Level, prelude::*};
use bevy_egui::{EguiContextSettings, EguiContexts, EguiPrimaryContextPass, EguiStartupSet, egui::{self, ecolor::Color32}};
// use serde::{Deserialize, Serialize};
pub mod constants;
pub mod http_client;
use crate::constants::DEFAULT_HEADERS;
use crate::http_client::{html_parser::{tree_to_vec, Node}, connection::HTTPClient, renderer::Renderer};
struct Tab {
url: String,
title: String,
http_client: HTTPClient,
renderer: Renderer
}
impl Tab {
fn new(url: &str) -> Tab {
let http_client = HTTPClient::new();
Tab {
url: url.to_string(),
title: url.to_string(),
http_client,
renderer: Renderer::new()
}
}
fn request(&mut self, url: String) {
self.url = url;
if self.url.starts_with("http://") || self.url.starts_with("https://") || self.url.starts_with("view-source:") {
self.http_client.get_request(&self.url, DEFAULT_HEADERS.clone(), false);
} else if self.url.starts_with("file://") {
self.http_client.file_request(&self.url);
} else if self.url.starts_with("data:text/html,") {
self.http_client.content_response = self.url.split("data:text/html,").nth(1).unwrap_or("").to_string();
self.http_client.scheme = "http".to_string();
} else if self.url == "about:blank" {
self.http_client.content_response = String::new();
self.http_client.scheme = "http".to_string();
} else {
self.http_client.get_request(&format!("https://{}", self.url), DEFAULT_HEADERS.clone(), false);
}
self.update_title();
}
fn update_title(&mut self){
let mut flattened_tree = vec![];
tree_to_vec(&self.http_client.node.as_ref().unwrap(), &mut flattened_tree);
self.title = flattened_tree.iter()
.find(|node| {
if let Node::Text(text_node) = node {
if let Some(parent) = &text_node.parent {
return parent.tag().map(|t| t == "title").unwrap_or(false);
}
}
false
})
.and_then(|node| {
if let Node::Text(text_node) = node {
Some(text_node.text.clone())
} else {
None
}
})
.unwrap_or_else(|| self.url.clone())
}
}
#[derive(Resource)]
struct AppState {
current_url: String,
active_tab: usize,
tabs: Vec<Tab>
}
fn main() {
let new_tab = Tab::new("about:blank");
let mut tabs = Vec::new();
tabs.push(new_tab);
App::new()
.insert_resource(ClearColor(Color::BLACK))
.add_plugins(
DefaultPlugins
.set(bevy::log::LogPlugin {
filter: "warn,ui=info".to_string(),
level: Level::INFO,
..Default::default()
})
.set(WindowPlugin {
primary_window: Some(Window {
// You may want this set to `true` if you need virtual keyboard work in mobile browsers.
prevent_default_event_handling: false,
..default()
}),
..default()
}),
)
.add_plugins(bevy_egui::EguiPlugin::default())
.insert_resource(AppState {
current_url: String::new(),
active_tab: 0,
tabs: tabs
})
.add_systems(
PreStartup,
setup_camera_system.before(EguiStartupSet::InitContexts),
)
.add_systems(
EguiPrimaryContextPass,
(draw, update_ui_scale_factor_system),
)
.run();
}
// fn update(mut app_state: ResMut<AppState>) {
// }
fn setup_camera_system(mut commands: Commands) {
commands.spawn(Camera2d);
}
fn update_ui_scale_factor_system(egui_context: Single<(&mut EguiContextSettings, &Camera)>) {
let (mut egui_settings, camera) = egui_context.into_inner();
egui_settings.scale_factor = 1.5 / camera.target_scaling_factor().unwrap_or(1.5);
}
fn draw(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Result {
let ctx = contexts.ctx_mut()?;
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
ui.horizontal(|ui| {
let mut i = 0;
let mut set_active_tab_to: Option<usize> = None;
for tab in &app_state.tabs {
let mut button = egui::Button::new(tab.title.clone());
if i == app_state.active_tab {
button = button.fill(Color32::BLACK);
}
if ui.add(button).clicked() {
set_active_tab_to = Some(i);
}
i += 1;
}
if let Some(set_active_tab_to) = set_active_tab_to {
// set previous's url
let active_tab = app_state.active_tab.clone();
app_state.tabs[active_tab].url = app_state.current_url.clone();
app_state.active_tab = set_active_tab_to;
// set new's url back
app_state.current_url = app_state.tabs[set_active_tab_to].url.clone();
}
if ui.button("+").clicked() {
let new_tab = Tab::new("about:blank");
app_state.tabs.push(new_tab);
}
});
let available_width = ui.available_width();
let available_height = ui.available_height();
let response = ui.add_sized([available_width, available_height / 20.0], egui::TextEdit::singleline(&mut app_state.current_url));
if ui.input(|i| i.key_pressed(egui::Key::Enter)) {
let current_url = app_state.current_url.clone();
let active_tab = app_state.active_tab;
app_state.tabs[active_tab].request(current_url);
}
});
egui::CentralPanel::default().show(ctx, |ui| {
let active_tab_index = app_state.active_tab.clone();
let tab = &mut app_state.tabs[active_tab_index];
tab.renderer.render(&mut tab.http_client, ui);
});
Ok(())
}

View File

@@ -1,105 +0,0 @@
import arcade.color, re
from arcade.types import Color
from arcade.gui.widgets.buttons import UITextureButtonStyle, UIFlatButtonStyle
from arcade.gui.widgets.slider import UISliderStyle
# Regex is by AI
emoji_pattern = re.compile(
r'['
r'\U0001F300-\U0001F5FF'
r'\U0001F600-\U0001F64F'
r'\U0001F680-\U0001F6FF'
r'\U0001F700-\U0001F77F'
r'\U0001F780-\U0001F7FF'
r'\U0001F800-\U0001F8FF'
r'\U0001F900-\U0001F9FF'
r'\U0001FA00-\U0001FA6F'
r'\U0001FA70-\U0001FAFF'
r'\u2600-\u26FF'
r'\u2700-\u27BF'
r']',
flags=re.UNICODE
)
token_pattern = re.compile(
f'({emoji_pattern.pattern})' # emoji
r'|(\w+)' # word
r'|([^\w\s])', # punctuation
flags=re.UNICODE
)
menu_background_color = arcade.color.WHITE
log_dir = 'logs'
discord_presence_id = 1393164073566208051
BLOCK_ELEMENTS = [
"html", "body", "article", "section", "nav", "aside",
"h1", "h2", "h3", "h4", "h5", "h6", "hgroup", "header",
"footer", "address", "p", "hr", "pre", "blockquote",
"ol", "ul", "menu", "li", "dl", "dt", "dd", "figure",
"figcaption", "main", "div", "table", "form", "fieldset",
"legend", "details", "summary"
]
SELF_CLOSING_TAGS = [
"area", "base", "br", "col", "embed", "hr", "img", "input",
"link", "meta", "param", "source", "track", "wbr",
]
HEAD_TAGS = [
"base", "basefont", "bgsound", "noscript",
"link", "meta", "title", "style", "script",
]
INHERITED_PROPERTIES = {
"font-family": "Arial",
"font-size": "16px",
"font-style": "normal",
"font-weight": "normal",
"color": "black",
"display": "inline",
"width": "auto",
"height": "auto"
}
DEFAULT_HEADERS = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0",
"Accept": "text/html",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none"
}
button_style = {'normal': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK), 'hover': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK),
'press': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK), 'disabled': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK)}
big_button_style = {'normal': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26), 'hover': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26),
'press': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26), 'disabled': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26)}
dropdown_style = {'normal': UIFlatButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, bg=Color(128, 128, 128)), 'hover': UIFlatButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, bg=Color(49, 154, 54)),
'press': UIFlatButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, bg=Color(128, 128, 128)), 'disabled': UIFlatButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, bg=Color(128, 128, 128))}
slider_default_style = UISliderStyle(bg=Color(128, 128, 128), unfilled_track=Color(128, 128, 128), filled_track=Color(49, 154, 54))
slider_hover_style = UISliderStyle(bg=Color(49, 154, 54), unfilled_track=Color(128, 128, 128), filled_track=Color(49, 154, 54))
slider_style = {'normal': slider_default_style, 'hover': slider_hover_style, 'press': slider_hover_style, 'disabled': slider_default_style}
settings = {
"Graphics": {
"Window Mode": {"type": "option", "options": ["Windowed", "Fullscreen", "Borderless"], "config_key": "window_mode", "default": "Windowed"},
"Resolution": {"type": "option", "options": ["1366x768", "1440x900", "1600x900", "1920x1080", "2560x1440", "3840x2160"], "config_key": "resolution"},
"Anti-Aliasing": {"type": "option", "options": ["None", "2x MSAA", "4x MSAA", "8x MSAA", "16x MSAA"], "config_key": "anti_aliasing", "default": "4x MSAA"},
"VSync": {"type": "bool", "config_key": "vsync", "default": True},
"FPS Limit": {"type": "slider", "min": 0, "max": 480, "config_key": "fps_limit", "default": 60},
},
"Sound": {
"Music": {"type": "bool", "config_key": "music", "default": True},
"SFX": {"type": "bool", "config_key": "sfx", "default": True},
"Music Volume": {"type": "slider", "min": 0, "max": 100, "config_key": "music_volume", "default": 50},
"SFX Volume": {"type": "slider", "min": 0, "max": 100, "config_key": "sfx_volume", "default": 50},
},
"Miscellaneous": {
"Discord RPC": {"type": "bool", "config_key": "discord_rpc", "default": True},
},
"Credits": {}
}
settings_start_category = "Graphics"

View File

@@ -1,11 +0,0 @@
import arcade.gui, arcade, os
from http_client.html_parser import CSSParser
# Get the directory where this module is located
_module_dir = os.path.dirname(os.path.abspath(__file__))
_assets_dir = os.path.join(os.path.dirname(_module_dir), 'assets')
button_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture(os.path.join(_assets_dir, 'graphics', 'button.png')))
button_hovered_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture(os.path.join(_assets_dir, 'graphics', 'button_hovered.png')))
DEFAULT_STYLE_SHEET = CSSParser(open(os.path.join(_assets_dir, "css", "browser.css")).read()).parse()

View File

@@ -1,86 +0,0 @@
from functools import lru_cache
import logging, arcade, traceback, pyglet.display
def dump_platform():
import platform
logging.debug(f'Platform: {platform.platform()}')
logging.debug(f'Release: {platform.release()}')
logging.debug(f'Machine: {platform.machine()}')
logging.debug(f'Architecture: {platform.architecture()}')
def dump_gl(context=None):
if context is not None:
info = context.get_info()
else:
from pyglet.gl import gl_info as info
logging.debug(f'gl_info.get_version(): {info.get_version()}')
logging.debug(f'gl_info.get_vendor(): {info.get_vendor()}')
logging.debug(f'gl_info.get_renderer(): {info.get_renderer()}')
def print_debug_info():
logging.debug('########################## DEBUG INFO ##########################')
logging.debug('')
dump_platform()
dump_gl()
logging.debug('')
logging.debug(f'Number of screens: {len(pyglet.display.get_display().get_screens())}')
logging.debug('')
for n, screen in enumerate(pyglet.display.get_display().get_screens()):
logging.debug(f"Screen #{n+1}:")
logging.debug(f'DPI: {screen.get_dpi()}')
logging.debug(f'Scale: {screen.get_scale()}')
logging.debug(f'Size: {screen.width}, {screen.height}')
logging.debug(f'Position: {screen.x}, {screen.y}')
logging.debug('')
logging.debug('########################## DEBUG INFO ##########################')
logging.debug('')
def on_exception(*exc_info):
logging.error(f"Unhandled exception:\n{''.join(traceback.format_exception(exc_info[1], limit=None))}")
def get_closest_resolution():
allowed_resolutions = [(1366, 768), (1440, 900), (1600,900), (1920,1080), (2560,1440), (3840,2160)]
screen_width, screen_height = arcade.get_screens()[0].width, arcade.get_screens()[0].height
if (screen_width, screen_height) in allowed_resolutions:
if not allowed_resolutions.index((screen_width, screen_height)) == 0:
closest_resolution = allowed_resolutions[allowed_resolutions.index((screen_width, screen_height))-1]
else:
closest_resolution = (screen_width, screen_height)
else:
target_width, target_height = screen_width // 2, screen_height // 2
closest_resolution = min(
allowed_resolutions,
key=lambda res: abs(res[0] - target_width) + abs(res[1] - target_height)
)
return closest_resolution
class FakePyPresence():
def __init__(self):
...
def update(self, *args, **kwargs):
...
def close(self, *args, **kwargs):
...
@lru_cache
def hex_to_rgb(hex_color):
hex_color = hex_color.lstrip('#')
if len(hex_color) != 6:
return (127, 127, 127)
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
@lru_cache
def get_color_from_name(rgb_name):
rgb_name = rgb_name.upper()
if rgb_name.startswith("LIGHT"):
color_name = rgb_name.split("LIGHT")[1]
color_value = arcade.csscolor.__dict__.get(f"LIGHT_{color_name}")
if not color_value:
color_value = arcade.color.__dict__.get(f"LIGHT_{color_name}")
if color_value:
return color_value
color_value = arcade.csscolor.__dict__.get(rgb_name)
return color_value if color_value else arcade.color.__dict__.get(rgb_name, arcade.color.GRAY)

331
uv.lock generated
View File

@@ -1,331 +0,0 @@
version = 1
revision = 2
requires-python = ">=3.11"
[[package]]
name = "arcade"
version = "3.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pillow" },
{ name = "pyglet" },
{ name = "pymunk" },
{ name = "pytiled-parser" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5b/8f/7e2d6433bfbaf6c501d51614155a1b8e617805edab6c95b74182c7f2c68b/arcade-3.3.3.tar.gz", hash = "sha256:86a73dfafa0ce4fd4fb9551f850cc332b392e22e88142ff17914ea3fef59540b", size = 41952289, upload_time = "2025-10-09T15:46:14.947Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/c9/5888ddef3668caee10b6111dfb98a96addacef40b9b098213497a8ed614e/arcade-3.3.3-py3-none-any.whl", hash = "sha256:25f4e0e5b8bcbe7a7d087c0a23b19e9f423b0c70ec00945c8712c9a3e541ebd2", size = 42669158, upload_time = "2025-10-09T15:46:11.382Z" },
]
[[package]]
name = "attrs"
version = "25.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload_time = "2025-10-06T13:54:44.725Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload_time = "2025-10-06T13:54:43.17Z" },
]
[[package]]
name = "certifi"
version = "2025.7.14"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload_time = "2025-07-14T03:29:28.449Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload_time = "2025-07-14T03:29:26.863Z" },
]
[[package]]
name = "cffi"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload_time = "2025-09-08T23:24:04.541Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload_time = "2025-09-08T23:22:26.456Z" },
{ url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload_time = "2025-09-08T23:22:28.197Z" },
{ url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload_time = "2025-09-08T23:22:29.475Z" },
{ url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload_time = "2025-09-08T23:22:31.063Z" },
{ url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload_time = "2025-09-08T23:22:32.507Z" },
{ url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload_time = "2025-09-08T23:22:34.132Z" },
{ url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload_time = "2025-09-08T23:22:35.443Z" },
{ url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload_time = "2025-09-08T23:22:36.805Z" },
{ url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload_time = "2025-09-08T23:22:38.436Z" },
{ url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload_time = "2025-09-08T23:22:39.776Z" },
{ url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload_time = "2025-09-08T23:22:40.95Z" },
{ url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload_time = "2025-09-08T23:22:42.463Z" },
{ url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload_time = "2025-09-08T23:22:43.623Z" },
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload_time = "2025-09-08T23:22:44.795Z" },
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload_time = "2025-09-08T23:22:45.938Z" },
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload_time = "2025-09-08T23:22:47.349Z" },
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload_time = "2025-09-08T23:22:48.677Z" },
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload_time = "2025-09-08T23:22:50.06Z" },
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload_time = "2025-09-08T23:22:51.364Z" },
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload_time = "2025-09-08T23:22:52.902Z" },
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload_time = "2025-09-08T23:22:54.518Z" },
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload_time = "2025-09-08T23:22:55.867Z" },
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload_time = "2025-09-08T23:22:57.188Z" },
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload_time = "2025-09-08T23:22:58.351Z" },
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload_time = "2025-09-08T23:22:59.668Z" },
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload_time = "2025-09-08T23:23:00.879Z" },
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload_time = "2025-09-08T23:23:02.231Z" },
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload_time = "2025-09-08T23:23:03.472Z" },
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload_time = "2025-09-08T23:23:04.792Z" },
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload_time = "2025-09-08T23:23:06.127Z" },
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload_time = "2025-09-08T23:23:07.753Z" },
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload_time = "2025-09-08T23:23:09.648Z" },
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload_time = "2025-09-08T23:23:10.928Z" },
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload_time = "2025-09-08T23:23:12.42Z" },
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload_time = "2025-09-08T23:23:14.32Z" },
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload_time = "2025-09-08T23:23:15.535Z" },
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload_time = "2025-09-08T23:23:16.761Z" },
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload_time = "2025-09-08T23:23:18.087Z" },
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload_time = "2025-09-08T23:23:19.622Z" },
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload_time = "2025-09-08T23:23:20.853Z" },
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload_time = "2025-09-08T23:23:22.08Z" },
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload_time = "2025-09-08T23:23:23.314Z" },
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload_time = "2025-09-08T23:23:24.541Z" },
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload_time = "2025-09-08T23:23:26.143Z" },
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload_time = "2025-09-08T23:23:27.873Z" },
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload_time = "2025-09-08T23:23:44.61Z" },
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload_time = "2025-09-08T23:23:45.848Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload_time = "2025-09-08T23:23:47.105Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload_time = "2025-09-08T23:23:29.347Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload_time = "2025-09-08T23:23:30.63Z" },
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload_time = "2025-09-08T23:23:31.91Z" },
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload_time = "2025-09-08T23:23:33.214Z" },
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload_time = "2025-09-08T23:23:34.495Z" },
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload_time = "2025-09-08T23:23:36.096Z" },
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload_time = "2025-09-08T23:23:37.328Z" },
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload_time = "2025-09-08T23:23:38.945Z" },
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload_time = "2025-09-08T23:23:40.423Z" },
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload_time = "2025-09-08T23:23:41.742Z" },
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload_time = "2025-09-08T23:23:43.004Z" },
]
[[package]]
name = "csd4ni3l-browser"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "arcade" },
{ name = "certifi" },
{ name = "pypresence" },
{ name = "ujson" },
]
[package.metadata]
requires-dist = [
{ name = "arcade", specifier = ">=3.3.3" },
{ name = "certifi", specifier = ">=2025.7.14" },
{ name = "pypresence", specifier = ">=4.3.0" },
{ name = "ujson", specifier = ">=5.10.0" },
]
[[package]]
name = "pillow"
version = "11.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload_time = "2025-07-01T09:16:30.666Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload_time = "2025-07-01T09:13:59.203Z" },
{ url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload_time = "2025-07-01T09:14:01.101Z" },
{ url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload_time = "2025-07-03T13:09:55.638Z" },
{ url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload_time = "2025-07-03T13:10:00.37Z" },
{ url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload_time = "2025-07-01T09:14:04.491Z" },
{ url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload_time = "2025-07-01T09:14:06.235Z" },
{ url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload_time = "2025-07-01T09:14:07.978Z" },
{ url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload_time = "2025-07-01T09:14:10.233Z" },
{ url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload_time = "2025-07-01T09:14:11.921Z" },
{ url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload_time = "2025-07-01T09:14:13.623Z" },
{ url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload_time = "2025-07-01T09:14:15.268Z" },
{ url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload_time = "2025-07-01T09:14:17.648Z" },
{ url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload_time = "2025-07-01T09:14:19.828Z" },
{ url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload_time = "2025-07-03T13:10:04.448Z" },
{ url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload_time = "2025-07-03T13:10:10.391Z" },
{ url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload_time = "2025-07-01T09:14:21.63Z" },
{ url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload_time = "2025-07-01T09:14:23.321Z" },
{ url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload_time = "2025-07-01T09:14:25.237Z" },
{ url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload_time = "2025-07-01T09:14:27.053Z" },
{ url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload_time = "2025-07-01T09:14:30.104Z" },
{ url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload_time = "2025-07-01T09:14:31.899Z" },
{ url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload_time = "2025-07-01T09:14:33.709Z" },
{ url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload_time = "2025-07-01T09:14:35.276Z" },
{ url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload_time = "2025-07-01T09:14:37.203Z" },
{ url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload_time = "2025-07-01T09:14:39.344Z" },
{ url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload_time = "2025-07-01T09:14:41.843Z" },
{ url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload_time = "2025-07-01T09:14:44.008Z" },
{ url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload_time = "2025-07-03T13:10:15.628Z" },
{ url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload_time = "2025-07-03T13:10:21.857Z" },
{ url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload_time = "2025-07-01T09:14:45.698Z" },
{ url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload_time = "2025-07-01T09:14:47.415Z" },
{ url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload_time = "2025-07-01T09:14:49.636Z" },
{ url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload_time = "2025-07-01T09:14:51.962Z" },
{ url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload_time = "2025-07-01T09:14:54.142Z" },
{ url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload_time = "2025-07-01T09:14:56.436Z" },
{ url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload_time = "2025-07-01T09:14:58.072Z" },
{ url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload_time = "2025-07-01T09:14:59.79Z" },
{ url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload_time = "2025-07-01T09:15:01.648Z" },
{ url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload_time = "2025-07-03T13:10:27.018Z" },
{ url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload_time = "2025-07-03T13:10:33.01Z" },
{ url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload_time = "2025-07-01T09:15:03.365Z" },
{ url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload_time = "2025-07-01T09:15:05.655Z" },
{ url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload_time = "2025-07-01T09:15:07.358Z" },
{ url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload_time = "2025-07-01T09:15:09.317Z" },
{ url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload_time = "2025-07-01T09:15:11.311Z" },
{ url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload_time = "2025-07-01T09:15:13.164Z" },
{ url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload_time = "2025-07-01T09:15:15.695Z" },
{ url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload_time = "2025-07-01T09:15:17.429Z" },
{ url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload_time = "2025-07-01T09:15:19.423Z" },
{ url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload_time = "2025-07-03T13:10:38.404Z" },
{ url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload_time = "2025-07-03T13:10:44.987Z" },
{ url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload_time = "2025-07-01T09:15:21.237Z" },
{ url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload_time = "2025-07-01T09:15:23.186Z" },
{ url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload_time = "2025-07-01T09:15:25.1Z" },
{ url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload_time = "2025-07-01T09:15:27.378Z" },
{ url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload_time = "2025-07-01T09:15:29.294Z" },
{ url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload_time = "2025-07-01T09:15:31.128Z" },
{ url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload_time = "2025-07-01T09:15:33.328Z" },
{ url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload_time = "2025-07-01T09:15:35.194Z" },
{ url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload_time = "2025-07-01T09:15:37.114Z" },
{ url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload_time = "2025-07-03T13:10:50.248Z" },
{ url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload_time = "2025-07-03T13:10:56.432Z" },
{ url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload_time = "2025-07-01T09:15:39.436Z" },
{ url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload_time = "2025-07-01T09:15:41.269Z" },
{ url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload_time = "2025-07-01T09:15:43.13Z" },
{ url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload_time = "2025-07-01T09:15:44.937Z" },
{ url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload_time = "2025-07-01T09:15:46.673Z" },
{ url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload_time = "2025-07-01T09:15:48.512Z" },
{ url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload_time = "2025-07-01T09:15:50.399Z" },
{ url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload_time = "2025-07-01T09:16:19.801Z" },
{ url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload_time = "2025-07-01T09:16:21.818Z" },
{ url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload_time = "2025-07-03T13:11:20.738Z" },
{ url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload_time = "2025-07-03T13:11:26.283Z" },
{ url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload_time = "2025-07-01T09:16:23.762Z" },
{ url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload_time = "2025-07-01T09:16:25.593Z" },
{ url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload_time = "2025-07-01T09:16:27.732Z" },
]
[[package]]
name = "pycparser"
version = "2.23"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload_time = "2025-09-09T13:23:47.91Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload_time = "2025-09-09T13:23:46.651Z" },
]
[[package]]
name = "pyglet"
version = "2.1.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e3/6b/84c397a74cd33eb377168c682e9e3d6b90c1c10c661e11ea5b397ac8497c/pyglet-2.1.11.tar.gz", hash = "sha256:8285d0af7d0ab443232a81df4d941e0d5c48c18a23ec770b3e5c59a222f5d56e", size = 6594448, upload_time = "2025-11-07T04:29:52.092Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/77/a2/2b09fbff0eedbe44fbf164b321439a38f7c5568d8b754aa197ee45886431/pyglet-2.1.11-py3-none-any.whl", hash = "sha256:fa0f4fdf366cfc5040aeb462416910b0db2fa374b7d620b7a432178ca3fa8af1", size = 1032213, upload_time = "2025-11-07T04:29:46.06Z" },
]
[[package]]
name = "pymunk"
version = "6.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ac/08/1513c868bc2a6bfa22d47acded27f5525c1db10bf1db4fdfa39160991616/pymunk-6.9.0.tar.gz", hash = "sha256:765f7c561a859a1b565bc517a47cc3992d6258e860f9174c533033c218af63c3", size = 3104088, upload_time = "2024-10-13T09:02:40.008Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6f/ba/34524aac6c57990aa9561c4a949543794e5f7128a0b01537ed061bdaed08/pymunk-6.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:536cf3ef9a3add0ea04d83a4c01fe090ff137fb591c3b6fff6e69102384ec5d5", size = 364338, upload_time = "2024-10-13T08:58:08.889Z" },
{ url = "https://files.pythonhosted.org/packages/19/9a/0d4931e3114495c31b600a17f27d5541f2ee35883e7c693199e1ccdf1ab0/pymunk-6.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e474bb748ded01d96d6eac8e282446baef324b67e0280213b495b1f936c06e7", size = 346937, upload_time = "2024-10-13T08:58:10.604Z" },
{ url = "https://files.pythonhosted.org/packages/61/d0/acd6a6cd8266ac0333792ac3ae36558a58859ca806e0add8f5ea01627b24/pymunk-6.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f54bd14512ca5fed0e77f964b1de4e7da1a31386dbf125e33482874d69bb6537", size = 1065273, upload_time = "2024-10-13T08:58:13.012Z" },
{ url = "https://files.pythonhosted.org/packages/5d/d3/2e5763d2eea69e8953782da83fe81a0235650339c22a4f8c65ecdd07cec0/pymunk-6.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc8d6fe79f77f3ed6e2f33682d355eedb6864684120b845a3501fdf2d3efdcb6", size = 988611, upload_time = "2024-10-13T08:58:15.262Z" },
{ url = "https://files.pythonhosted.org/packages/ac/db/ff2cfa5b87d3e60992b2264a03ffedc738de64d0107b4ce96c623f9098e7/pymunk-6.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:70ee413899672c2d7d2ffbecabee133dba49a109867b520d77c829c0d9b3fe92", size = 974971, upload_time = "2024-10-13T08:58:17.706Z" },
{ url = "https://files.pythonhosted.org/packages/ff/44/8fd8677048aa864d91915702522c70c5aaadedfd7cd95000b75d7aabeffd/pymunk-6.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d163dcba2e5814bc5f1274e0ee6ec2a7e06bed8bf0050f30f22b604634bf7dbc", size = 1037097, upload_time = "2024-10-13T08:58:20.264Z" },
{ url = "https://files.pythonhosted.org/packages/19/fc/e6b8bf53255f2012dbdf4a2b063b6c02f8c13ce13b21fdfd84dda64fea80/pymunk-6.9.0-cp311-cp311-win32.whl", hash = "sha256:5d3ae7df3d39afe5b11633496cd464b198d5c62bec69f767f3b61f9fe7f09b98", size = 315321, upload_time = "2024-10-13T08:58:22.475Z" },
{ url = "https://files.pythonhosted.org/packages/bc/3c/925a0193bbcca7203f46fc531f4f0703885c102c1e2c118c8db35816aee3/pymunk-6.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:2db4797ecec3668d51bc112a37192ee1836e236bbacdf5ed12f5a994cf1bae33", size = 366711, upload_time = "2024-10-13T08:58:24.796Z" },
{ url = "https://files.pythonhosted.org/packages/93/96/d8505f4e9661c0e5343db5492895b90b2ada6ec4547fdc7a2df50eb0cdf2/pymunk-6.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02bb0fbbbce2b12c18a033e2cec747e6c4b0db93d2cb9a20f45e569b571ba184", size = 364703, upload_time = "2024-10-13T08:58:27.144Z" },
{ url = "https://files.pythonhosted.org/packages/54/3e/610a2f2b0c6c14038168f6f862148cb245aef867b01906ce18704acafe1c/pymunk-6.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6aae4f93ac686d5e2ec60b01faa1b3722a8ab630464d0c127e16462e7bef6292", size = 347056, upload_time = "2024-10-13T08:58:29.39Z" },
{ url = "https://files.pythonhosted.org/packages/4a/dd/4e12fb3671a6c4f2c0604420f0f15b5402b05c4964bba001088a3d92e3b9/pymunk-6.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7734d13e490e84665b1f03e616270b248d5279ed34e03859267f67868f1b94c", size = 1071014, upload_time = "2024-10-13T08:58:32.274Z" },
{ url = "https://files.pythonhosted.org/packages/91/f8/0618a9204aff896da8b2a9df44179390b178bf00b189851affd4809b1f03/pymunk-6.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b05dbfa58d366dea860f7259ca48483922a83620ab6a19effaa74e85a4251966", size = 990358, upload_time = "2024-10-13T08:58:35.295Z" },
{ url = "https://files.pythonhosted.org/packages/af/67/ea2ff4a26b66acad394e4f28e4e316fbe306d34909eca401baae211ca182/pymunk-6.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cb9520c52c043de4b2b1f83979f0d097929f6ff13c8a4059d9d211b98ae25887", size = 976300, upload_time = "2024-10-13T08:58:37.339Z" },
{ url = "https://files.pythonhosted.org/packages/91/d9/a69b268712dceacf227cfff74401e2292b53050383661d456605a1928a84/pymunk-6.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:da0e153d321073cd07a48380cfc1b7bd8d40bf4ee1b14a7ede33d90a69ee0452", size = 1042511, upload_time = "2024-10-13T08:58:40.044Z" },
{ url = "https://files.pythonhosted.org/packages/f0/40/21c2a08b027d99f351b75daa36f8a2e2385daba45098078d225811275ff8/pymunk-6.9.0-cp312-cp312-win32.whl", hash = "sha256:8325c9092345764876b1c3855126cb14450dc83dc5b141ff54983a7c77fbae52", size = 315339, upload_time = "2024-10-13T09:01:37.995Z" },
{ url = "https://files.pythonhosted.org/packages/78/b4/0a18c632f96924f969924cc5903689afcaf474d4c472305805dab391b247/pymunk-6.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:13246a79b599c44d174f5619596c62b656d8539797f28bdb2797c4b700c90a33", size = 366671, upload_time = "2024-10-13T09:01:39.965Z" },
{ url = "https://files.pythonhosted.org/packages/b6/5a/c76904d21f3fdb0b713b3a8056622733a0b773f7e55ef974fa4546068cbd/pymunk-6.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5c59e5cf904e148dd0d35cffb7bafe146835042de9280672cafecc3a41caf7a3", size = 364703, upload_time = "2024-10-13T09:01:42.628Z" },
{ url = "https://files.pythonhosted.org/packages/63/b2/378d54b79812da5312b10de272c27aa0ac621498e059aa50eb4eec33ab52/pymunk-6.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4cbc2d37f69d85fedc1097af64edc8f4c43973a13429d51004883cbb9342875e", size = 347058, upload_time = "2024-10-13T09:01:44.529Z" },
{ url = "https://files.pythonhosted.org/packages/ba/a8/c7ea141a1d0e3f5b08ad653f0b5a4ebc0e5854f92bc7049a2a921fbe0d65/pymunk-6.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd64ef76e9e47fda929a2961fe98759ac46b5a7b6126d1ba3e6f04493da6519b", size = 1070851, upload_time = "2024-10-13T09:01:46.638Z" },
{ url = "https://files.pythonhosted.org/packages/10/a2/f40bcc9be90c2af1fe8cf4ba4281385b48d9f5667f03f6834c49aba600fd/pymunk-6.9.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c568c7402acd5d9a55e3965565ae0a596e4603ba8a7b7b7f0952efadd0e69524", size = 990371, upload_time = "2024-10-13T09:01:49.622Z" },
{ url = "https://files.pythonhosted.org/packages/01/ae/ff7fdf1c8d32ba89d1ccada39b5f7ed66e35420b8d31bdc9af6d5d20ea2f/pymunk-6.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f6cbe3d06e468be11a615d4facecc4a870bf58c1a27c365e655b5a85685ec942", size = 976294, upload_time = "2024-10-13T09:01:52.196Z" },
{ url = "https://files.pythonhosted.org/packages/c6/90/64ef000011f0c930b42354f0d91a07b4bc7f70819ec5b6034b84198bf53f/pymunk-6.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:65a9a93a51dbaf1c77efa4d2425549888a1eda9f5c9cd9a5a89b7ca66310968a", size = 1042493, upload_time = "2024-10-13T09:01:55.665Z" },
{ url = "https://files.pythonhosted.org/packages/e5/fb/6516bd5fe565ea51a88308869632dfc896ca6b05b2579b016ffa8047a8ec/pymunk-6.9.0-cp313-cp313-win32.whl", hash = "sha256:a78b37bb360e715657c76caedaf40cdaaf6dab354d497eda481a976cc5cab3d7", size = 315341, upload_time = "2024-10-13T09:01:58.049Z" },
{ url = "https://files.pythonhosted.org/packages/e0/7c/1542df7ffbff70a4523ccb02c9241c9fe4dc24c77b747e2c16fb94891156/pymunk-6.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:d6419e1531df80ff0bb6f1f8215e044f57415514386b7b212dc148919ca629ed", size = 366673, upload_time = "2024-10-13T09:01:59.733Z" },
]
[[package]]
name = "pypresence"
version = "4.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f4/2e/d110f862720b5e3ba1b0b719657385fc4151929befa2c6981f48360aa480/pypresence-4.3.0.tar.gz", hash = "sha256:a6191a3af33a9667f2a4ef0185577c86b962ee70aa82643c472768a6fed1fbf3", size = 10696, upload_time = "2023-07-08T00:33:53.49Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/40/1d30b30e18f81eb71365681223971a9822a89b3d6ee5269dd2aa955bc228/pypresence-4.3.0-py2.py3-none-any.whl", hash = "sha256:af878c6d49315084f1b108aec86b31915080614d9421d6dd3a44737aba9ff13f", size = 11778, upload_time = "2023-07-08T00:33:52.018Z" },
]
[[package]]
name = "pytiled-parser"
version = "2.2.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/79/62/0d8a2220ee0747522f3b73e4f38bea7c78aefdf707afb86decf26f799fc5/pytiled_parser-2.2.9.tar.gz", hash = "sha256:225269fdd37afcbcd3b76ea3e2cab6b1e742387027106055990db43fd7451ebd", size = 45958, upload_time = "2025-01-23T18:43:30.538Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/f7/6b6c51b50ed8681a31146e5e7ac325b78fe776ff48b1ec8f56d7e4995d72/pytiled_parser-2.2.9-py2.py3-none-any.whl", hash = "sha256:37f73d31950bf4d02ee3bda59f3d6123c55194dc8d8e876821dd2080af5f1f91", size = 44452, upload_time = "2025-01-23T18:43:28.207Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload_time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload_time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "ujson"
version = "5.10.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f0/00/3110fd566786bfa542adb7932d62035e0c0ef662a8ff6544b6643b3d6fd7/ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1", size = 7154885, upload_time = "2024-05-14T02:02:34.233Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/23/ec/3c551ecfe048bcb3948725251fb0214b5844a12aa60bee08d78315bb1c39/ujson-5.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a5b366812c90e69d0f379a53648be10a5db38f9d4ad212b60af00bd4048d0f00", size = 55353, upload_time = "2024-05-14T02:00:48.04Z" },
{ url = "https://files.pythonhosted.org/packages/8d/9f/4731ef0671a0653e9f5ba18db7c4596d8ecbf80c7922dd5fe4150f1aea76/ujson-5.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:502bf475781e8167f0f9d0e41cd32879d120a524b22358e7f205294224c71126", size = 51813, upload_time = "2024-05-14T02:00:49.28Z" },
{ url = "https://files.pythonhosted.org/packages/1f/2b/44d6b9c1688330bf011f9abfdb08911a9dc74f76926dde74e718d87600da/ujson-5.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b91b5d0d9d283e085e821651184a647699430705b15bf274c7896f23fe9c9d8", size = 51988, upload_time = "2024-05-14T02:00:50.484Z" },
{ url = "https://files.pythonhosted.org/packages/29/45/f5f5667427c1ec3383478092a414063ddd0dfbebbcc533538fe37068a0a3/ujson-5.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:129e39af3a6d85b9c26d5577169c21d53821d8cf68e079060602e861c6e5da1b", size = 53561, upload_time = "2024-05-14T02:00:52.146Z" },
{ url = "https://files.pythonhosted.org/packages/26/21/a0c265cda4dd225ec1be595f844661732c13560ad06378760036fc622587/ujson-5.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f77b74475c462cb8b88680471193064d3e715c7c6074b1c8c412cb526466efe9", size = 58497, upload_time = "2024-05-14T02:00:53.366Z" },
{ url = "https://files.pythonhosted.org/packages/28/36/8fde862094fd2342ccc427a6a8584fed294055fdee341661c78660f7aef3/ujson-5.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ec0ca8c415e81aa4123501fee7f761abf4b7f386aad348501a26940beb1860f", size = 997877, upload_time = "2024-05-14T02:00:55.095Z" },
{ url = "https://files.pythonhosted.org/packages/90/37/9208e40d53baa6da9b6a1c719e0670c3f474c8fc7cc2f1e939ec21c1bc93/ujson-5.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab13a2a9e0b2865a6c6db9271f4b46af1c7476bfd51af1f64585e919b7c07fd4", size = 1140632, upload_time = "2024-05-14T02:00:57.099Z" },
{ url = "https://files.pythonhosted.org/packages/89/d5/2626c87c59802863d44d19e35ad16b7e658e4ac190b0dead17ff25460b4c/ujson-5.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57aaf98b92d72fc70886b5a0e1a1ca52c2320377360341715dd3933a18e827b1", size = 1043513, upload_time = "2024-05-14T02:00:58.488Z" },
{ url = "https://files.pythonhosted.org/packages/2f/ee/03662ce9b3f16855770f0d70f10f0978ba6210805aa310c4eebe66d36476/ujson-5.10.0-cp311-cp311-win32.whl", hash = "sha256:2987713a490ceb27edff77fb184ed09acdc565db700ee852823c3dc3cffe455f", size = 38616, upload_time = "2024-05-14T02:01:00.463Z" },
{ url = "https://files.pythonhosted.org/packages/3e/20/952dbed5895835ea0b82e81a7be4ebb83f93b079d4d1ead93fcddb3075af/ujson-5.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:f00ea7e00447918ee0eff2422c4add4c5752b1b60e88fcb3c067d4a21049a720", size = 42071, upload_time = "2024-05-14T02:01:02.211Z" },
{ url = "https://files.pythonhosted.org/packages/e8/a6/fd3f8bbd80842267e2d06c3583279555e8354c5986c952385199d57a5b6c/ujson-5.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5", size = 55642, upload_time = "2024-05-14T02:01:04.055Z" },
{ url = "https://files.pythonhosted.org/packages/a8/47/dd03fd2b5ae727e16d5d18919b383959c6d269c7b948a380fdd879518640/ujson-5.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e", size = 51807, upload_time = "2024-05-14T02:01:05.25Z" },
{ url = "https://files.pythonhosted.org/packages/25/23/079a4cc6fd7e2655a473ed9e776ddbb7144e27f04e8fc484a0fb45fe6f71/ujson-5.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043", size = 51972, upload_time = "2024-05-14T02:01:06.458Z" },
{ url = "https://files.pythonhosted.org/packages/04/81/668707e5f2177791869b624be4c06fb2473bf97ee33296b18d1cf3092af7/ujson-5.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1", size = 53686, upload_time = "2024-05-14T02:01:07.618Z" },
{ url = "https://files.pythonhosted.org/packages/bd/50/056d518a386d80aaf4505ccf3cee1c40d312a46901ed494d5711dd939bc3/ujson-5.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3", size = 58591, upload_time = "2024-05-14T02:01:08.901Z" },
{ url = "https://files.pythonhosted.org/packages/fc/d6/aeaf3e2d6fb1f4cfb6bf25f454d60490ed8146ddc0600fae44bfe7eb5a72/ujson-5.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21", size = 997853, upload_time = "2024-05-14T02:01:10.772Z" },
{ url = "https://files.pythonhosted.org/packages/f8/d5/1f2a5d2699f447f7d990334ca96e90065ea7f99b142ce96e85f26d7e78e2/ujson-5.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2", size = 1140689, upload_time = "2024-05-14T02:01:12.214Z" },
{ url = "https://files.pythonhosted.org/packages/f2/2c/6990f4ccb41ed93744aaaa3786394bca0875503f97690622f3cafc0adfde/ujson-5.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e", size = 1043576, upload_time = "2024-05-14T02:01:14.39Z" },
{ url = "https://files.pythonhosted.org/packages/14/f5/a2368463dbb09fbdbf6a696062d0c0f62e4ae6fa65f38f829611da2e8fdd/ujson-5.10.0-cp312-cp312-win32.whl", hash = "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e", size = 38764, upload_time = "2024-05-14T02:01:15.83Z" },
{ url = "https://files.pythonhosted.org/packages/59/2d/691f741ffd72b6c84438a93749ac57bf1a3f217ac4b0ea4fd0e96119e118/ujson-5.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc", size = 42211, upload_time = "2024-05-14T02:01:17.567Z" },
{ url = "https://files.pythonhosted.org/packages/0d/69/b3e3f924bb0e8820bb46671979770c5be6a7d51c77a66324cdb09f1acddb/ujson-5.10.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287", size = 55646, upload_time = "2024-05-14T02:01:19.26Z" },
{ url = "https://files.pythonhosted.org/packages/32/8a/9b748eb543c6cabc54ebeaa1f28035b1bd09c0800235b08e85990734c41e/ujson-5.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e", size = 51806, upload_time = "2024-05-14T02:01:20.593Z" },
{ url = "https://files.pythonhosted.org/packages/39/50/4b53ea234413b710a18b305f465b328e306ba9592e13a791a6a6b378869b/ujson-5.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557", size = 51975, upload_time = "2024-05-14T02:01:21.904Z" },
{ url = "https://files.pythonhosted.org/packages/b4/9d/8061934f960cdb6dd55f0b3ceeff207fcc48c64f58b43403777ad5623d9e/ujson-5.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988", size = 53693, upload_time = "2024-05-14T02:01:23.742Z" },
{ url = "https://files.pythonhosted.org/packages/f5/be/7bfa84b28519ddbb67efc8410765ca7da55e6b93aba84d97764cd5794dbc/ujson-5.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816", size = 58594, upload_time = "2024-05-14T02:01:25.554Z" },
{ url = "https://files.pythonhosted.org/packages/48/eb/85d465abafb2c69d9699cfa5520e6e96561db787d36c677370e066c7e2e7/ujson-5.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20", size = 997853, upload_time = "2024-05-14T02:01:27.151Z" },
{ url = "https://files.pythonhosted.org/packages/9f/76/2a63409fc05d34dd7d929357b7a45e3a2c96f22b4225cd74becd2ba6c4cb/ujson-5.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0", size = 1140694, upload_time = "2024-05-14T02:01:29.113Z" },
{ url = "https://files.pythonhosted.org/packages/45/ed/582c4daba0f3e1688d923b5cb914ada1f9defa702df38a1916c899f7c4d1/ujson-5.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f", size = 1043580, upload_time = "2024-05-14T02:01:31.447Z" },
{ url = "https://files.pythonhosted.org/packages/d7/0c/9837fece153051e19c7bade9f88f9b409e026b9525927824cdf16293b43b/ujson-5.10.0-cp313-cp313-win32.whl", hash = "sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165", size = 38766, upload_time = "2024-05-14T02:01:32.856Z" },
{ url = "https://files.pythonhosted.org/packages/d7/72/6cb6728e2738c05bbe9bd522d6fc79f86b9a28402f38663e85a28fddd4a0/ujson-5.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539", size = 42212, upload_time = "2024-05-14T02:01:33.97Z" },
]