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: include:
- os: ubuntu-22.04 - os: ubuntu-22.04
platform: linux platform: linux
python-version: "3.11"
- os: windows-latest - os: windows-latest
platform: windows platform: windows
python-version: "3.11"
steps: steps:
- name: Check-out repository - name: Check-out repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Python - name: Cache
uses: actions/setup-python@v5 uses: actions/cache@v4
with: with:
python-version: "3.11" path: |
architecture: "x64" ~/.cargo/registry
cache: "pip" ~/.cargo/git
cache-dependency-path: | target
**/requirements*.txt key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install Dependencies - name: Install mold, clang, Wayland, ALSA and x11 headers and dependencies (Linux)
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)
if: matrix.os == 'ubuntu-22.04' if: matrix.os == 'ubuntu-22.04'
run: | run: |
set -euo pipefail sudo apt update
echo "Searching for built Linux binary..." sudo apt install -y build-essential clang cmake pkg-config mold \
# List to help debugging when paths change libwayland-dev libxkbcommon-dev libegl1-mesa-dev \
ls -laR . | head -n 500 || true libwayland-egl-backend-dev \
BIN=$(find . -maxdepth 4 -type f -name 'csd4ni3lBrowser*' -perm -u+x | head -n1 || true) libx11-dev libxext-dev libxrandr-dev libxinerama-dev libxcursor-dev \
if [ -z "${BIN}" ]; then libxi-dev libxfixes-dev libxrender-dev \
echo "ERROR: No Linux binary found after build" libfreetype6-dev libfontconfig1-dev libgl1-mesa-dev \
exit 1 libasound2-dev libudev-dev
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"
shell: bash 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' if: matrix.os == 'windows-latest'
run: | run: Test-Path target\release\csd4ni3l-browser.exe
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
}
shell: pwsh shell: pwsh
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: ${{ matrix.platform }} name: ${{ matrix.platform }}
path: build_output/csd4ni3lBrowser.* path: |
target/release/csd4ni3l-browser
target/release/csd4ni3l-browser.exe
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -114,5 +91,5 @@ jobs:
--notes "Automated build for $TAG" --notes "Automated build for $TAG"
fi fi
# Upload the executables directly (no zip files) # Upload the executables directly (no zip files)
gh release upload "$TAG" downloads/linux/csd4ni3lBrowser.bin --clobber gh release upload "$TAG" downloads/linux/csd4ni3l-browser --clobber
gh release upload "$TAG" downloads/windows/csd4ni3lBrowser.exe --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 python
__pycache__/ /target
*.py[cod] css_cache
*$py.class html_cache
# 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/

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! 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. Huge Thanks to Rust for being the programming language used in this app.
https://www.python.org/ https://www.rust-lang.org/
Huge thanks to Arcade and Pyglet for being the graphical engines used in this application. Huge thanks to Bevy for being the graphical engine / ECS used in this app.
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

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" },
]