mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 10:00:34 +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>
28 lines
1.3 KiB
Swift
28 lines
1.3 KiB
Swift
import Foundation
|
|
|
|
/// frame-control://install?manifest=URL or ?url=URL (docs/web-install.md), the same
|
|
/// first filter as app/install-link.js. The server on the Frame applies the full
|
|
/// rules (HTTPS, no private addresses, redirects) before fetching anything.
|
|
struct InstallLink: Equatable {
|
|
enum Kind: String { case manifest, url }
|
|
let kind: Kind
|
|
let target: String
|
|
|
|
static let scheme = "frame-control"
|
|
private static let maxLink = 4096
|
|
private static let maxURL = 2048
|
|
|
|
init?(_ raw: String) {
|
|
guard raw.count <= Self.maxLink, raw.lowercased().hasPrefix("\(Self.scheme):"),
|
|
let link = URLComponents(string: raw), link.scheme?.lowercased() == Self.scheme,
|
|
link.host?.lowercased() == "install", ["", "/"].contains(link.path) else { return nil }
|
|
let items = link.queryItems ?? []
|
|
guard items.count == 1, let item = items.first, let kind = Kind(rawValue: item.name),
|
|
let target = item.value, !target.isEmpty, target.count <= Self.maxURL,
|
|
let url = URLComponents(string: target), ["https", "http"].contains(url.scheme?.lowercased() ?? ""),
|
|
url.host?.isEmpty == false, url.user == nil, url.password == nil else { return nil }
|
|
self.kind = kind
|
|
self.target = target
|
|
}
|
|
}
|