feat(apple/clipboard): macOS client half of the shared clipboard (Phase 1 §5)
ci / web (pull_request) Successful in 48s
ci / docs-site (pull_request) Successful in 53s
apple / swift (pull_request) Successful in 1m16s
apple / screenshots (pull_request) Has been skipped
android / android (pull_request) Has been cancelled
ci / rust (pull_request) Has been cancelled
ci / bench (pull_request) Has been cancelled
windows / build (x86_64-pc-windows-msvc) (pull_request) Has been cancelled
windows / build (aarch64-pc-windows-msvc) (pull_request) Has been cancelled

The NSPasteboard bridge completing Phase 1 (design/clipboard-and-file-transfer.md
§5) — with the host backends on this branch, copy/paste now crosses the wire in
both directions on macOS. Lazy in both directions:

- PunktfunkConnection grows the clipboard plane: its own clipboardLock (close()
  joins it like the other pullers), hostCaps/hostSupportsClipboard from the
  Welcome, the typed ClipEvent vocabulary, and the six ABI wrappers
  (clipControl/clipOffer/clipFetch/clipServe/clipCancel/nextClipboard — borrowed
  event payloads copied out before the next poll).
- ClipboardSync (PunktfunkKit, macOS-only): one drain thread bridging
  NSPasteboard.general ↔ the QUIC clipboard plane. Local copies announce format
  lists via a 500 ms changeCount poll (+ immediate on app activation); bytes
  leave only on a host FetchRequest, answered from the live pasteboard and
  seq-guarded against staleness. Host copies install one NSPasteboardItem whose
  data provider fires only when a Mac app actually pastes, then blocks its
  provider thread (never main) on a 10 s-bounded fetch. Concealed/Transient
  pasteboards (password managers) are never announced; our own writes are
  changeCount-suppressed (§3.4). Text/RTF/HTML/PNG; files ride Phase 2.
- UI: per-host "Share clipboard with this host" toggle (StoredHost.clipboardSync,
  optional for saved-JSON forward-compat — wire-format tests extended), a
  mid-session Share/Stop Sharing Clipboard item in the Stream menu (⌃⌥⇧C,
  greyed without HOST_CAP_CLIPBOARD), SessionModel owning the lifecycle
  (start on streaming after the trust gate, drain joined off-main on teardown).

swift build + swift test green (macOS). Requires the ABI v8 xcframework
(scripts/build-xcframework.sh).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-17 13:40:47 +02:00
parent da0578771e
commit 5d0e23d6a5
8 changed files with 647 additions and 2 deletions
@@ -34,11 +34,16 @@ public struct StoredHost: Identifiable, Codable, Hashable {
/// client can send a magic packet to wake the host later (when it's asleep and no longer
/// advertising). Optional (same forward-compat reason as `mgmtPort`); nil until first learned.
public var macAddresses: [String]?
/// Share the clipboard with this host (macOS sessions; design/clipboard-and-file-transfer.md
/// §5.3). Opt-in per host: nil/false = off (nil also keeps older saved JSON decoding same
/// forward-compat reason as `mgmtPort`). Honored only when the host advertises
/// `HOST_CAP_CLIPBOARD`.
public var clipboardSync: Bool?
public init(
id: UUID = UUID(), name: String, address: String, port: UInt16 = 9777,
pinnedSHA256: Data? = nil, lastConnected: Date? = nil, mgmtPort: UInt16? = nil,
macAddresses: [String]? = nil
macAddresses: [String]? = nil, clipboardSync: Bool? = nil
) {
self.id = id
self.name = name
@@ -48,6 +53,7 @@ public struct StoredHost: Identifiable, Codable, Hashable {
self.lastConnected = lastConnected
self.mgmtPort = mgmtPort
self.macAddresses = macAddresses
self.clipboardSync = clipboardSync
}
public var displayName: String { name.isEmpty ? address : name }