feat(apple/library): launch a picked title (step 4 client side)

Tapping a game in the (flagged) library now starts a session that asks the host to
launch it — the picked GameEntry id rides the connect down to the host, which resolves
it against its own library (ede3936).

- PunktfunkConnection.init gains `launchID` and calls the new punktfunk_connect_ex4
  (wrapping it in withOptionalCString; nil = host default).
- Threaded SessionModel.connect(launchID:) → ContentView.connect(_:launchID:) →
  a `launchTitle(host, id)` helper that dismisses the browser and connects.
- LibraryView gains `onLaunch`; cards become buttons that fire it. Wired on every
  platform (ContentView sheet on macOS/iOS, HomeView destination on tvOS) via a new
  `onLaunchTitle` closure on HomeView. Settings footer updated (launch is live now).

Can't compile Swift on the Linux box; CI (apple.yml) verifies. The host side of this
chain is live-validated on the dev box: a client `--launch custom:<id>` made the host
resolve the id and spawn gamescope running the title (see ede3936).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 15:00:58 +00:00
parent ede393602c
commit 024e255476
6 changed files with 50 additions and 22 deletions
@@ -242,26 +242,31 @@ public final class PunktfunkConnection {
compositor: Compositor = .auto,
gamepad: GamepadType = .auto,
bitrateKbps: UInt32 = 0,
launchID: String? = nil,
timeoutMs: UInt32 = 10_000
) throws {
if let pin = pinSHA256, pin.count != 32 { throw PunktfunkClientError.invalidPin }
var observed = [UInt8](repeating: 0, count: 32)
// `launchID` (a host library id like "steam:570") asks the host to launch that title in
// the session; the host resolves it against its own library nil = the host's default.
handle = host.withCString { cs in
withOptionalCString(identity?.certPEM) { cert in
withOptionalCString(identity?.keyPEM) { key in
if let pin = pinSHA256 {
return pin.withUnsafeBytes { p in
punktfunk_connect_ex3(
cs, port, width, height, refreshHz, compositor.rawValue,
gamepad.rawValue, bitrateKbps,
p.bindMemory(to: UInt8.self).baseAddress, &observed,
cert, key, timeoutMs)
withOptionalCString(launchID) { launch in
if let pin = pinSHA256 {
return pin.withUnsafeBytes { p in
punktfunk_connect_ex4(
cs, port, width, height, refreshHz, compositor.rawValue,
gamepad.rawValue, bitrateKbps, launch,
p.bindMemory(to: UInt8.self).baseAddress, &observed,
cert, key, timeoutMs)
}
}
return punktfunk_connect_ex4(
cs, port, width, height, refreshHz, compositor.rawValue,
gamepad.rawValue, bitrateKbps, launch,
nil, &observed, cert, key, timeoutMs)
}
return punktfunk_connect_ex3(
cs, port, width, height, refreshHz, compositor.rawValue,
gamepad.rawValue, bitrateKbps,
nil, &observed, cert, key, timeoutMs)
}
}
}