Rename Port Manager to Frametanium

Avoids confusion with the other Steam Frame project called FramePort. New service
frametanium.service, folders ~/.config/frametanium and ~/.local/share/frametanium
(service code in its service/ subfolder), headset app Frametanium. install.sh moves an
existing Port Manager install's settings and installed versions over and stops the old
service. Library art regenerated by the new tools/make_art.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHDaz86aayqMULHsjByEmT
This commit is contained in:
CogentialandClaude Opus 5.5 committed 2026-10-07 14:28:20 -04:00
1 parent 94865cb34f
commit c6b3edf252
14 files changed
+136 -70

No files matched your search

+11 -11
View File
@@ -1,7 +1,7 @@
# Port Manager for the Steam Frame
# Frametanium for the Steam Frame
Installs and updates Steam Frame apps and ports straight from their GitHub releases, like Obtainium: add any repo,
and Port Manager keeps it up to date. Works from the headset or a browser.
and Frametanium keeps it up to date. Works from the headset or a browser.
- **Add any GitHub repo** by typing `owner/name` (or pasting a link) on the headset's on-screen keyboard, from the
browser page, or by picking a suggestion made for the Frame. The built-in ports (Ship of Harkinian, 2 Ship 2
@@ -13,7 +13,7 @@ and Port Manager keeps it up to date. Works from the headset or a browser.
- *Library app:* unpacked into `~/devkit-game/<name>`, given a `run.sh` (its own, or one that starts the ARM64
program it finds), added to the Steam library with art (the repo's GitHub card and the owner's avatar).
- *Its installer:* when the download has `install.sh` (or `contrib/install.sh`, `setup.sh`), it's unpacked into
`~/.local/share/port-manager/apps/<id>` and that script runs, for tools such as SteamVR overlays. Removing the app
`~/.local/share/frametanium/apps/<id>` and that script runs, for tools such as SteamVR overlays. Removing the app
runs its uninstaller when it has one.
- **Install, update, update all, or pick any version.** Pre-releases are skipped unless you turn them on for an app
(they're on for apps that only have pre-releases).
@@ -22,7 +22,7 @@ and Port Manager keeps it up to date. Works from the headset or a browser.
- Won't touch an app that's running. Only add apps you trust: their programs and installers run as your user.
## Use it
- **In the headset:** open **Port Manager** in the library. D-pad to choose, **A** open an app (install/update,
- **In the headset:** open **Frametanium** in the library. D-pad to choose, **A** open an app (install/update,
versions, previous version, pre-releases, remove), **Y** add an app, **Select** update all, **Start** check for
updates, **B** back/close.
- **From a phone or PC:** `http://<frame-ip>:8643` (add apps, edit name/install style/program, remove).
@@ -32,14 +32,14 @@ Updates are checked every 6 hours and on demand.
## Files
- `catalog.json`: the built-in ports.
- `suggestions.json`: apps offered on the Add screen.
- `~/.config/port-manager/catalog.json` on the Frame: the apps you added and changes to built-in ones
- `~/.config/frametanium/catalog.json` on the Frame: the apps you added and changes to built-in ones
(`{"id": "soh", "hidden": true}` hides one; adding its repo again brings it back).
- `~/.config/port-manager/installed.json`: installed versions.
- `~/.config/frametanium/installed.json`: installed versions.
## Private repos
GitHub only shows releases of private repos to signed-in users. Either make the port repos public (they contain no
ROMs), or save a token in the web page's Settings: a fine-grained token limited to those repos with read-only
**Contents** access is enough. It's stored in `~/.config/port-manager/config.json` (mode 600) and never sent back
**Contents** access is enough. It's stored in `~/.config/frametanium/config.json` (mode 600) and never sent back
to the page.
## Install
@@ -51,12 +51,12 @@ app/build-arm64.sh # cross-compiles the headset app into dist/app
```
Service only, on the Frame: copy this folder over and run `sh install.sh`.
Uninstall: `systemctl --user disable --now frame-port-manager.service` and remove Port Manager from the library.
Uninstall: `systemctl --user disable --now frametanium.service` and remove Frametanium from the library.
## Notes
- The service listens on port 8643 on your network. Set `password` in `~/.config/port-manager/config.json` to require
- The service listens on port 8643 on your network. Set `password` in `~/.config/frametanium/config.json` to require
one in the browser (the headset app connects locally and doesn't need it).
- Standard-library Python only. The headset app is C + SDL2 with the public-domain
[font8x8](https://github.com/dhepper/font8x8).
- Testing on another architecture: `PORT_MANAGER_EXEC_WRAPPER=qemu-aarch64-static` (with `QEMU_LD_PREFIX`) runs
ARM64 AppImages for unpacking; `PORT_MANAGER_GAMES` changes the install folder.
- Testing on another architecture: `FRAMETANIUM_EXEC_WRAPPER=qemu-aarch64-static` (with `QEMU_LD_PREFIX`) runs
ARM64 AppImages for unpacking; `FRAMETANIUM_GAMES` changes the install folder.
+12 -12
View File
@@ -1,4 +1,4 @@
/* Port Manager for the headset: a controller-driven front end for the Port Manager service (manager.py), which
/* Frametanium for the headset: a controller-driven front end for the Frametanium service (manager.py), which
* does the downloading and installing. It talks to the service on 127.0.0.1 with plain HTTP and reads simple
* tab-separated replies.
*
@@ -130,7 +130,7 @@ static void fill(int x, int y, int w, int h, Uint32 rgb) {
static void read_port(void) {
char path[512], buf[2048];
snprintf(path, sizeof path, "%s/.config/port-manager/config.json", getenv("HOME") ? getenv("HOME") : "");
snprintf(path, sizeof path, "%s/.config/frametanium/config.json", getenv("HOME") ? getenv("HOME") : "");
FILE* f = fopen(path, "r");
if (!f) return;
size_t n = fread(buf, 1, sizeof buf - 1, f);
@@ -256,7 +256,7 @@ static int load_releases(const char* id, Release* rels, char* err, size_t errsz)
char* body = http("GET", path, 25);
err[0] = '\0';
if (!body) {
snprintf(err, errsz, "Port Manager service isn't running.");
snprintf(err, errsz, "Frametanium service isn't running.");
return 0;
}
int n = 0;
@@ -306,7 +306,7 @@ static int inspect(const char* repo, Found* fd, char* err, size_t errsz) {
memset(fd, 0, sizeof *fd);
err[0] = '\0';
if (!body) {
snprintf(err, errsz, "The Port Manager service didn't answer.");
snprintf(err, errsz, "The Frametanium service didn't answer.");
return 0;
}
char* save;
@@ -348,7 +348,7 @@ static void post(const char* path, char* err, size_t errsz) {
if (err) {
err[0] = '\0';
if (!body)
snprintf(err, errsz, "The Port Manager service didn't answer.");
snprintf(err, errsz, "The Frametanium service didn't answer.");
else if (body[0] == '!' && body[1] == '\t')
snprintf(err, errsz, "%.*s", (int)strcspn(body + 2, "\n"), body + 2);
else if (strstr(body, "\"error\": \""))
@@ -372,7 +372,7 @@ static void try_start_service(void) {
dup2(devnull, 1);
dup2(devnull, 2);
}
execlp("systemctl", "systemctl", "--user", "start", "frame-port-manager.service", (char*)NULL);
execlp("systemctl", "systemctl", "--user", "start", "frametanium.service", (char*)NULL);
_exit(127);
}
if (pid > 0) waitpid(pid, NULL, 0);
@@ -435,13 +435,13 @@ static int scroll_first(int sel, int max_rows) {
static void draw_list(App* apps, int n, int sel, const char* banner, Uint32 banner_rgb) {
clear();
int m = W / 20, y = title("Port Manager");
int m = W / 20, y = title("Frametanium");
if (banner && *banner) {
text_fit(banner, m, y, S, W - 2 * m, banner_rgb);
y += 14 * S;
}
if (n < 0) {
text("Starting the Port Manager service...", m, y + 8 * S, S, MUTED);
text("Starting the Frametanium service...", m, y + 8 * S, S, MUTED);
legend("B Close");
SDL_RenderPresent(ren);
return;
@@ -594,7 +594,7 @@ static void draw_found(const Found* fd, const char* err) {
text(line, m, y, S, MUTED);
y += 14 * S;
}
text_wrap("Port Manager unpacks it and either adds it to your Steam library or, if it comes with an install.sh, "
text_wrap("Frametanium unpacks it and either adds it to your Steam library or, if it comes with an install.sh, "
"runs that. Only add apps you trust.", m, y, S, W - 2 * m, 3, MUTED);
y += 40 * S;
if (err && *err) text_wrap(err, m, y, S, W - 2 * m, 3, BAD);
@@ -661,7 +661,7 @@ int main(void) {
return 1;
}
Uint32 flags = getenv("PM_WINDOWED") ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP;
SDL_Window* win = SDL_CreateWindow("Port Manager", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1280, 720, flags);
SDL_Window* win = SDL_CreateWindow("Frametanium", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1280, 720, flags);
if (!win) return 1;
ren = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (!ren) ren = SDL_CreateRenderer(win, -1, SDL_RENDERER_SOFTWARE);
@@ -745,7 +745,7 @@ int main(void) {
if (sel > napps) sel = napps > 0 ? napps : 0;
snprintf(banner, sizeof banner, "%s",
napps < 0 && tried_start && SDL_GetTicks() > 8000
? "The Port Manager service isn't running. Restart the Frame and try again."
? "The Frametanium service isn't running. Restart the Frame and try again."
: notice);
}
App* cur = sel < napps ? &apps[sel] : NULL;
@@ -950,7 +950,7 @@ int main(void) {
draw_wait(fd.name, "Adding it...");
char* body = http("POST", path, 30);
if (!body) {
snprintf(err, sizeof err, "The Port Manager service didn't answer.");
snprintf(err, sizeof err, "The Frametanium service didn't answer.");
} else if (body[0] == '!') {
snprintf(err, sizeof err, "%.*s", (int)strcspn(body + 2, "\n"), body + 2);
} else {
+4 -4
View File
@@ -10,15 +10,15 @@ OUT=../dist/app
mkdir -p "$OUT/lib"
"$LLVM/clang" --target=aarch64-linux-gnu --sysroot="$SYSROOT" -fuse-ld=lld -O2 -Wall -march=armv8.2-a \
-I"$SDL/include/SDL2" -D_REENTRANT app.c -L"$SDL/lib" -lSDL2-2.0 -Wl,-rpath,'$ORIGIN/lib' \
-o "$OUT/port-manager"
"$LLVM/llvm-strip" "$OUT/port-manager"
-o "$OUT/frametanium"
"$LLVM/llvm-strip" "$OUT/frametanium"
cp -L "$SDL/lib/libSDL2-2.0.so.0" "$OUT/lib/"
cat > "$OUT/run.sh" <<'RUN'
#!/bin/sh
# Steam launches this. FrameDrop-style installs lose the executable bit, so restore it first.
cd "$(dirname "$0")" || exit 1
chmod +x ./port-manager 2>/dev/null
exec ./port-manager "$@"
chmod +x ./frametanium 2>/dev/null
exec ./frametanium "$@"
RUN
chmod +x "$OUT/run.sh"
echo "built $OUT"
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 50 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 36 KiB

+1 -1
View File
@@ -32,7 +32,7 @@
"rom_hint": "Send your Majora's Mask ROM with Frame Inbox, or put a .z64 in ~/.local/share/2ship.",
"run_sh": [
"#!/bin/sh",
"# Steam Frame launcher for 2 Ship 2 Harkinian (written by Port Manager). Data, saves and the built mm.o2r",
"# Steam Frame launcher for 2 Ship 2 Harkinian (written by Frametanium). Data, saves and the built mm.o2r",
"# live in ~/.local/share/2ship, so reinstalling this folder keeps them.",
"D=\"$(cd \"$(dirname \"$0\")\" && pwd)\"",
"export SHIP_HOME=\"${SHIP_HOME:-$HOME/.local/share/2ship}\"",
+10 -8
View File
@@ -1,6 +1,6 @@
#!/bin/sh
# Installs or updates Port Manager on a Steam Frame from this computer over SSH: the service, and the
# "Port Manager" app in the Steam library with its art.
# Installs or updates Frametanium on a Steam Frame from this computer over SSH: the service, and the
# "Frametanium" app in the Steam library with its art.
#
# Usage: ./deploy.sh [host] host defaults to frame.local
# Env: SSH_KEY (default ~/.ssh/frame_devkit), SSH_USER (default steamos)
@@ -11,18 +11,20 @@ HOST="${1:-frame.local}"
KEY="${SSH_KEY:-$HOME/.ssh/frame_devkit}"
USER_AT="${SSH_USER:-steamos}@$HOST"
SSH="ssh -i $KEY -o ConnectTimeout=10"
GAMEID=Port_Manager
GAMEID=Frametanium
[ -x dist/app/port-manager ] || { echo "Build the app first: app/build-arm64.sh"; exit 1; }
[ -x dist/app/frametanium ] || { echo "Build the app first: app/build-arm64.sh"; exit 1; }
$SSH "$USER_AT" true || { echo "Can't reach $HOST over SSH. Is the Frame on and on this network?"; exit 1; }
echo "== Service"
$SSH "$USER_AT" 'rm -rf ~/.cache/frame-port-manager-src && mkdir -p ~/.cache/frame-port-manager-src'
rsync -a -e "$SSH" manager.py page.html catalog.json suggestions.json frame-port-manager.service install.sh art "$USER_AT:.cache/frame-port-manager-src/"
$SSH "$USER_AT" 'sh ~/.cache/frame-port-manager-src/install.sh'
$SSH "$USER_AT" 'rm -rf ~/.cache/frametanium-src && mkdir -p ~/.cache/frametanium-src'
rsync -a -e "$SSH" manager.py page.html catalog.json suggestions.json frametanium.service install.sh art "$USER_AT:.cache/frametanium-src/"
$SSH "$USER_AT" 'sh ~/.cache/frametanium-src/install.sh'
echo "== Headset app"
rsync -a --delete -e "$SSH" dist/app/ "$USER_AT:devkit-game/$GAMEID/"
# The app used to be called Port Manager. Its old folder goes; remove the old "Port_Manager" tile from the library by hand.
$SSH "$USER_AT" 'rm -rf ~/devkit-game/Port_Manager'
$SSH "$USER_AT" "python3 ~/devkit-utils/steam-client-create-shortcut --parms '{\"gameid\": \"$GAMEID\", \"directory\": \"/home/steamos/devkit-game/$GAMEID\", \"argv\": [\"./run.sh\"], \"env\": {}, \"settings\": {\"compat_tool\": \"SteamLinuxRuntime_4-arm64\", \"steam_play\": \"0\"}}'" >/dev/null
echo "== Library art"
@@ -42,4 +44,4 @@ if [ -n "$APPID" ]; then
else
echo "Couldn't find the shortcut's app id yet; run this again later to add the art."
fi
echo "Done. Open \"Port Manager\" from the library, or http://$HOST:8643 in a browser."
echo "Done. Open \"Frametanium\" from the library, or http://$HOST:8643 in a browser."
-11
View File
@@ -1,11 +0,0 @@
[Unit]
Description=Port Manager (installs and updates Steam Frame apps and ports from GitHub releases)
After=network-online.target
[Service]
ExecStart=/usr/bin/python3 %h/.local/share/frame-port-manager/manager.py
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
+11
View File
@@ -0,0 +1,11 @@
[Unit]
Description=Frametanium (installs and updates Steam Frame apps and ports from GitHub releases)
After=network-online.target
[Service]
ExecStart=/usr/bin/python3 %h/.local/share/frametanium/service/manager.py
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
+23 -8
View File
@@ -1,16 +1,31 @@
#!/bin/sh
# Installs the Port Manager service on a Steam Frame for the current user. Run it on the Frame from this folder.
# Installs the Frametanium service on a Steam Frame for the current user. Run it on the Frame from this folder.
set -e
here="$(cd "$(dirname "$0")" && pwd)"
dest="$HOME/.local/share/frame-port-manager"
mkdir -p "$dest" "$HOME/.config/systemd/user"
dest="$HOME/.local/share/frametanium/service"
mkdir -p "$HOME/.config/systemd/user"
# Frametanium used to be called Port Manager: stop the old service and carry its settings, installed
# versions and added apps over to the new folders.
if [ -f "$HOME/.config/systemd/user/frame-port-manager.service" ]; then
systemctl --user disable --now frame-port-manager.service >/dev/null 2>&1 || true
rm -f "$HOME/.config/systemd/user/frame-port-manager.service"
fi
rm -rf "$HOME/.local/share/frame-port-manager" "$HOME/.cache/port-manager"
for base in "$HOME/.config" "$HOME/.local/share"; do
if [ -d "$base/port-manager" ] && [ ! -e "$base/frametanium" ]; then
mv "$base/port-manager" "$base/frametanium"
fi
done
mkdir -p "$dest"
cp "$here/manager.py" "$here/page.html" "$here/catalog.json" "$here/suggestions.json" "$dest/"
rm -rf "$dest/art" && cp -r "$here/art" "$dest/art"
cp "$here/frame-port-manager.service" "$HOME/.config/systemd/user/"
cp "$here/frametanium.service" "$HOME/.config/systemd/user/"
systemctl --user daemon-reload
systemctl --user enable frame-port-manager.service >/dev/null 2>&1
systemctl --user restart frame-port-manager.service
systemctl --user enable frametanium.service >/dev/null 2>&1
systemctl --user restart frametanium.service
sleep 1
ip=$(python3 -c 'import socket;s=socket.socket(2,2);s.connect(("192.0.2.1",9));print(s.getsockname()[0])' 2>/dev/null || hostname)
echo "Port Manager is running: http://$ip:8643"
echo "Uninstall: systemctl --user disable --now frame-port-manager.service"
echo "Frametanium is running: http://$ip:8643"
echo "Uninstall: systemctl --user disable --now frametanium.service"
+12 -12
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Port Manager: installs and updates Steam Frame apps and ports from their GitHub releases, like Obtainium.
"""Frametanium: installs and updates Steam Frame apps and ports from their GitHub releases, like Obtainium.
A small web service (standard library only) that runs on the Frame. It tracks a list of GitHub repos: the built-in
ports in catalog.json plus any repo you add. For each it reads the releases, picks the download meant for the Frame
@@ -33,14 +33,14 @@ from pathlib import Path
HOME = Path.home()
APP_DIR = Path(__file__).resolve().parent
CONFIG_DIR = Path(os.environ.get("XDG_CONFIG_HOME", HOME / ".config")) / "port-manager"
CONFIG_DIR = Path(os.environ.get("XDG_CONFIG_HOME", HOME / ".config")) / "frametanium"
CONFIG_FILE = CONFIG_DIR / "config.json"
INSTALLED_FILE = CONFIG_DIR / "installed.json"
USER_CATALOG = CONFIG_DIR / "catalog.json"
CACHE_DIR = Path(os.environ.get("XDG_CACHE_HOME", HOME / ".cache")) / "port-manager"
DATA_DIR = Path(os.environ.get("XDG_DATA_HOME", HOME / ".local/share")) / "port-manager"
CACHE_DIR = Path(os.environ.get("XDG_CACHE_HOME", HOME / ".cache")) / "frametanium"
DATA_DIR = Path(os.environ.get("XDG_DATA_HOME", HOME / ".local/share")) / "frametanium"
APPS_DIR = DATA_DIR / "apps" # installs of "installer" apps (their installers may point back into them)
GAMES_DIR = Path(os.environ.get("PORT_MANAGER_GAMES", HOME / "devkit-game"))
GAMES_DIR = Path(os.environ.get("FRAMETANIUM_GAMES", HOME / "devkit-game"))
CREATE_SHORTCUT = HOME / "devkit-utils" / "steam-client-create-shortcut"
STEAM_USERDATA = HOME / ".local/share/Steam/userdata"
@@ -123,7 +123,7 @@ def unhide_builtin(repo):
def gh_request(url, accept="application/vnd.github+json"):
headers = {"Accept": accept, "User-Agent": "frame-port-manager", "X-GitHub-Api-Version": "2022-11-28"}
headers = {"Accept": accept, "User-Agent": "frametanium", "X-GitHub-Api-Version": "2022-11-28"}
token = config().get("github_token")
if token:
headers["Authorization"] = f"Bearer {token}"
@@ -402,7 +402,7 @@ def register_with_steam(port, folder):
def fetch(url, dest, limit=8 * 1024 * 1024):
req = urllib.request.Request(url, headers={"User-Agent": "frame-port-manager"})
req = urllib.request.Request(url, headers={"User-Agent": "frametanium"})
with urllib.request.urlopen(req, timeout=20) as r:
data = r.read(limit)
if not data.startswith(b"\x89PNG") and not data.startswith(b"\xff\xd8"):
@@ -498,8 +498,8 @@ def extract_zip(archive, dest):
def extract_appimage(archive, dest):
os.chmod(archive, 0o755)
# PORT_MANAGER_EXEC_WRAPPER (e.g. qemu-aarch64-static) lets tests on another architecture run the AppImage.
wrapper = os.environ.get("PORT_MANAGER_EXEC_WRAPPER", "").split()
# FRAMETANIUM_EXEC_WRAPPER (e.g. qemu-aarch64-static) lets tests on another architecture run the AppImage.
wrapper = os.environ.get("FRAMETANIUM_EXEC_WRAPPER", "").split()
try:
r = subprocess.run(wrapper + [str(archive), "--appimage-extract"], cwd=dest, capture_output=True, timeout=600)
except OSError as e:
@@ -642,7 +642,7 @@ def write_launcher(staging, port):
folder = "/" + str(rel.parent) if str(rel.parent) != "." else ""
launch.write_text(
"#!/bin/sh\n"
f"# Starts {port['name']} (written by Port Manager).\n"
f"# Starts {port['name']} (written by Frametanium).\n"
'D="$(cd "$(dirname "$0")" && pwd)"\n'
f'chmod +x "$D/{rel}" 2>/dev/null\n'
f'cd "$D{folder}" && exec "./{rel.name}" "$@"\n')
@@ -996,7 +996,7 @@ def tsv(rows, keys):
class Handler(BaseHTTPRequestHandler):
server_version = "PortManager/2.0"
server_version = "Frametanium/2.0"
def log_message(self, fmt, *args):
sys.stderr.write("%s %s\n" % (self.address_string(), fmt % args))
@@ -1169,7 +1169,7 @@ def main():
threading.Thread(target=checker, daemon=True).start()
server = ThreadingHTTPServer(("0.0.0.0", int(cfg["port"])), Handler)
server.daemon_threads = True
print(f"Port Manager on port {cfg['port']}", flush=True)
print(f"Frametanium on port {cfg['port']}", flush=True)
server.serve_forever()
+3 -3
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Port Manager</title>
<title>Frametanium</title>
<style>
:root { --bg: #0f1216; --panel: #181d23; --line: #2a313a; --text: #e8edf2; --muted: #93a0ad;
--accent: #4fa3ff; --ok: #4cc38a; --warn: #f0b429; --bad: #f2555a; }
@@ -49,7 +49,7 @@
</head>
<body>
<main>
<div class="top"><h1>Port Manager</h1>
<div class="top"><h1>Frametanium</h1>
<div class="row"><button id="updateall">Update all</button><button id="refresh">Check for updates</button></div></div>
<p class="sub">Install and update Steam Frame apps and ports from their GitHub releases. Saves and ROMs are kept.</p>
<p class="notice" id="notice" hidden></p>
@@ -88,7 +88,7 @@ const open = {}; // app ids whose Edit section is open, kep
async function get(path) {
const r = await fetch(path, { headers: hdr() });
if (r.status === 401) {
key = prompt("Port Manager password") || "";
key = prompt("Frametanium password") || "";
try { localStorage.setItem("pmKey", key); } catch (e) {}
return get(path);
}
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Draws Frametanium's library art into art/app (needs Pillow and the DejaVu fonts). Run from the repo root."""
from PIL import Image, ImageDraw, ImageFont
import subprocess, math
NAME = "Frametanium"
bold = subprocess.run(["fc-match", "-f", "%{file}", "DejaVu Sans:bold"], capture_output=True, text=True).stdout
reg = subprocess.run(["fc-match", "-f", "%{file}", "DejaVu Sans"], capture_output=True, text=True).stdout
BG = (15, 18, 22); ACC = (79, 163, 255); TXT = (232, 237, 242); MUT = (147, 160, 173)
def glow(W, H, c):
im = Image.new("RGB", (W, H), BG); d = ImageDraw.Draw(im); R = int(max(W, H) * 0.6)
for r in range(R, 0, -8):
t = r / R; col = tuple(int(BG[i] * t + (25, 45, 75)[i] * (1 - t)) for i in range(3))
d.ellipse([c[0] - r, c[1] - r, c[0] + r, c[1] + r], fill=col)
return im
def icon(d, cx, cy, s):
"""A cartridge with a circular update arrow."""
lw = max(4, s // 16)
w = int(s * 0.78); h = int(s * 0.9); x0 = cx - w // 2; y0 = cy - h // 2
d.rounded_rectangle([x0, y0, x0 + w, y0 + h], radius=s // 10, outline=TXT, width=lw)
d.rectangle([x0 + w * 0.2, y0 + h * 0.12, x0 + w * 0.8, y0 + h * 0.42], outline=TXT, width=lw)
for k in range(5):
x = x0 + w * 0.18 + k * w * 0.16; d.line([(x, y0 + h * 0.84), (x, y0 + h * 0.96)], fill=TXT, width=lw)
r = s * 0.2; ax, ay = cx, cy + h * 0.18
d.arc([ax - r, ay - r, ax + r, ay + r], start=200, end=500, fill=ACC, width=lw)
ang = math.radians(140); ex, ey = ax + r * math.cos(ang), ay + r * math.sin(ang)
d.polygon([(ex - s * 0.08, ey - s * 0.02), (ex + s * 0.05, ey - s * 0.06), (ex + s * 0.01, ey + s * 0.08)], fill=ACC)
def make(W, H, c, s, title=None, txy=None, ts=0, sub=None):
im = glow(W, H, c); d = ImageDraw.Draw(im); icon(d, c[0], c[1], s)
if title:
d.text(txy, title, font=ImageFont.truetype(bold, ts), fill=TXT, anchor="mm")
if sub:
d.text((txy[0], txy[1] + int(ts * 0.95)), sub, font=ImageFont.truetype(reg, int(ts * 0.38)), fill=MUT, anchor="mm")
return im
make(600, 900, (300, 320), 280, NAME, (300, 620), 70, "Apps & ports from GitHub").save("art/app/portrait.png")
make(920, 430, (170, 215), 230, NAME, (600, 190), 64, "Updates from GitHub releases").save("art/app/wide.png")
make(1920, 620, (1500, 310), 330).save("art/app/hero.png")
im = Image.new("RGB", (256, 256), BG); icon(ImageDraw.Draw(im), 128, 128, 190); im.save("art/app/icon.png")
lg = Image.new("RGBA", (1000, 220), (0, 0, 0, 0))
ImageDraw.Draw(lg).text((500, 110), NAME, font=ImageFont.truetype(bold, 120), fill=TXT + (255,), anchor="mm")
lg.save("art/app/logo.png")