iPhone and iPad app: the same features, served from the Frame

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>
This commit is contained in:
saphidandClaude Opus 5.5 committed 2026-09-27 11:18:15 +10:00
1 parent 0f770dc88a
commit 13187e8f6a
33 files changed
+2459 -34

No files matched your search

@@ -0,0 +1,54 @@
import CryptoKit
import XCTest
@testable import Frame_Control
final class InstallLinkTests: XCTestCase {
func testAcceptsManifestAndURLLinks() {
XCTAssertEqual(InstallLink("frame-control://install?manifest=https://example.com/app.json"),
InstallLink("frame-control://install/?manifest=https://example.com/app.json"))
XCTAssertEqual(InstallLink("frame-control://install?url=https://example.com/a.apk")?.kind, .url)
XCTAssertEqual(InstallLink("FRAME-CONTROL://install?manifest=https://example.com/m.json")?.target, "https://example.com/m.json")
}
func testRejectsAnythingElse() {
for raw in ["frame-control://other?url=https://example.com/a.apk",
"frame-control://install?url=ftp://example.com/a.apk",
"frame-control://install?url=https://user:pw@example.com/a.apk",
"frame-control://install?url=https://example.com/a&manifest=https://example.com/b",
"frame-control://install?url=https://a.example/x&url=https://b.example/y",
"frame-control://install?url=",
"frame-control://install/deeper?url=https://example.com/a.apk",
"https://example.com/?url=https://example.com/a.apk",
"frame-control://install?url=https://example.com/" + String(repeating: "a", count: 2100)] {
XCTAssertNil(InstallLink(raw), raw)
}
}
}
final class HeadsetServerTests: XCTestCase {
func testReadsThePortTheServerPrints() {
XCTAssertEqual(HeadsetServer.port(in: "Frame Control on http://127.0.0.1:41234 (alias: frame; Ctrl-C to stop)\n"), 41234)
XCTAssertNil(HeadsetServer.port(in: "Traceback (most recent call last):"))
}
func testBundleIsInTheApp() throws {
let bundle = try HeadsetServer.Bundle.fromApp()
XCTAssertGreaterThan(bundle.data.count, 100_000)
XCTAssertEqual(bundle.version.count, 16)
}
}
final class KeyTests: XCTestCase {
func testAuthorizedKeysLine() {
let line = DeviceKey.authorizedKeysLine(Curve25519.Signing.PrivateKey(), comment: "frame-control@iPhone")
let parts = line.split(separator: " ")
XCTAssertEqual(parts.count, 3)
XCTAssertEqual(parts[0], "ssh-ed25519")
XCTAssertEqual(Data(base64Encoded: String(parts[1]))?.count, 51) // string "ssh-ed25519" + 32-byte key
XCTAssertEqual(parts[2], "frame-control@iPhone")
}
func testShellQuote() {
XCTAssertEqual(shellQuote("it's"), "'it'\\''s'")
}
}