mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 01:00:18 +02:00
An iPhone can't run Python or ssh, but the Frame can. The app (ios/, SwiftUI) connects with its own SSH key (Citadel), copies the server and helpers to ~/.cache/frame-control/<version> on the Frame once per version, starts ui/server.py there with FRAME_LOCAL=1 on the Frame's 127.0.0.1, and shows the page through an SSH tunnel. The server exits when the phone disconnects. Server: FRAME_LOCAL=1 puts ui/local-bin on PATH, whose ssh stand-in runs each `ssh frame COMMAND` locally (and serves as rsync's transport), so desktop and phone share one code path. Android display goes through podman exec there, as the Frame has no adb. FRAME_UI_KEY replaces the fixed X-Frame-UI value with a per-session key. Power actions take the Developer Mode password via sudo -S. --port 0 now prints the port it took. Page: a bottom tab bar and safe areas on phones, Play buttons visible on touch screens, saving through the share sheet, SSH/SFTP/Steam Link/remote desktop opening in their iOS apps, and a password dialog for power. App: pairing with the Developer Mode password once (never stored) or with a key the user adds; host key pinned on first use; plain-language connection errors with quiet retries; frame-control://install links; alerts and confirms. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
53 lines
1.8 KiB
Python
53 lines
1.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Pack what Frame Control's server needs to run on the Frame itself (the files the
|
|
desktop app ships, plus ui/local-bin) into one reproducible .tar.gz.
|
|
|
|
The iPhone app copies it to ~/.cache/frame-control/<version> on the Frame and
|
|
starts ui/server.py there. <version> is the SHA-256 of the archive, so a new
|
|
build replaces an old one and an unchanged one isn't copied again.
|
|
|
|
Usage: make_frame_bundle.py OUT.tar.gz (prints the version)
|
|
"""
|
|
import gzip
|
|
import hashlib
|
|
import io
|
|
import sys
|
|
import tarfile
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
PATTERNS = ["ui/*.py", "ui/*.html", "ui/local-bin/*", "scripts/*.sh", "frame/android/*.sh", "frame/android/*.py",
|
|
"frame/devkit-utils/**/*", "apk-catalog/*.py", "apk-catalog/pins.json", "apk-catalog/site/apps.js"]
|
|
|
|
|
|
def files():
|
|
found = set()
|
|
for pattern in PATTERNS:
|
|
for p in ROOT.glob(pattern):
|
|
if p.is_file() and "__pycache__" not in p.parts:
|
|
found.add(p)
|
|
return sorted(found)
|
|
|
|
|
|
def build():
|
|
raw = io.BytesIO()
|
|
with tarfile.open(fileobj=raw, mode="w", format=tarfile.PAX_FORMAT) as tar:
|
|
for p in files():
|
|
info = tarfile.TarInfo(str(p.relative_to(ROOT)))
|
|
data = p.read_bytes()
|
|
info.size, info.mtime, info.uid, info.gid, info.uname, info.gname = len(data), 0, 0, 0, "", ""
|
|
info.mode = 0o755 if p.stat().st_mode & 0o111 else 0o644
|
|
tar.addfile(info, io.BytesIO(data))
|
|
out = io.BytesIO()
|
|
with gzip.GzipFile(fileobj=out, mode="wb", mtime=0) as gz:
|
|
gz.write(raw.getvalue())
|
|
return out.getvalue()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) != 2:
|
|
sys.exit(__doc__)
|
|
data = build()
|
|
Path(sys.argv[1]).write_bytes(data)
|
|
print(hashlib.sha256(data).hexdigest()[:16])
|