macos: migrate app to the shared tethercore engine (SSE + RTC)
Drop TetherKit's TetherClient/SSEClient/Message from the macOS app; drive tethercore.Engine instead (RoomIdentity still from TetherKit via selective import). The Mac now gets direct P2P over the RTC data channel for free, and the native Swift app is no longer a parallel wire-protocol implementation. Background NSPasteboard polling (auto-send on copy) and write-on-receive are preserved. Feed UI moves to a FeedItem view-model (RTC messages carry ts=0 → fall back to local time). Verified end-to-end against the live server: RECEIVE (bus → Mac pasteboard via pbpaste) and SEND (pbcopy → bus) both confirmed. Generalize core/build-ios.sh → build-apple.sh: builds iOS device + sim + macOS arm64 into one TetherCore.xcframework and vendors it into both apps. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
46
core/build-apple.sh
Executable file
46
core/build-apple.sh
Executable file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds the tethercore Rust engine for Apple targets and refreshes the
|
||||
# artifacts both Xcode apps consume: the Swift bindings + a single
|
||||
# TetherCore.xcframework (iOS device + iOS sim + macOS arm64).
|
||||
# Outputs are git-ignored / regenerated — run after any change to core/src,
|
||||
# then build the apps in Xcode.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
IOS_APP="../ios/TetherApp"
|
||||
MAC_APP="../ios/MacTetherApp"
|
||||
IOS=aarch64-apple-ios
|
||||
SIM=aarch64-apple-ios-sim
|
||||
MAC=aarch64-apple-darwin
|
||||
|
||||
echo "▸ building static libs ($IOS, $SIM, $MAC)…"
|
||||
rustup target add "$IOS" "$SIM" "$MAC" >/dev/null 2>&1 || true
|
||||
cargo build --release --target "$IOS" --lib
|
||||
cargo build --release --target "$SIM" --lib
|
||||
cargo build --release --target "$MAC" --lib
|
||||
|
||||
echo "▸ generating Swift bindings…"
|
||||
cargo run --release --bin uniffi-bindgen -- \
|
||||
generate --library "target/$MAC/release/libtethercore.dylib" \
|
||||
--language swift --out-dir bindings/swift
|
||||
|
||||
echo "▸ assembling TetherCore.xcframework (ios + ios-sim + macos)…"
|
||||
rm -rf build-xcframework && mkdir -p build-xcframework/headers
|
||||
cp bindings/swift/tethercoreFFI.h build-xcframework/headers/
|
||||
cp bindings/swift/tethercoreFFI.modulemap build-xcframework/headers/module.modulemap
|
||||
rm -rf TetherCore.xcframework
|
||||
xcodebuild -create-xcframework \
|
||||
-library "target/$IOS/release/libtethercore.a" -headers build-xcframework/headers \
|
||||
-library "target/$SIM/release/libtethercore.a" -headers build-xcframework/headers \
|
||||
-library "target/$MAC/release/libtethercore.a" -headers build-xcframework/headers \
|
||||
-output TetherCore.xcframework
|
||||
|
||||
echo "▸ vendoring into both apps…"
|
||||
for APP in "$IOS_APP" "$MAC_APP"; do
|
||||
rm -rf "$APP/Vendor/TetherCore.xcframework"
|
||||
mkdir -p "$APP/Vendor"
|
||||
cp -R TetherCore.xcframework "$APP/Vendor/"
|
||||
cp bindings/swift/tethercore.swift "$APP/Sources/tethercore.swift"
|
||||
done
|
||||
|
||||
echo "✅ done — build TetherApp (iOS) and MacTetherApp (macOS) in Xcode."
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds the tethercore Rust engine for iOS and refreshes the artifacts the
|
||||
# Xcode app consumes: the Swift bindings + the TetherCore.xcframework.
|
||||
# These outputs are git-ignored (binaries) / regenerated — run this after any
|
||||
# change to core/src, then build the app in Xcode.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
APP_DIR="../ios/TetherApp"
|
||||
DEVICE=aarch64-apple-ios
|
||||
SIM=aarch64-apple-ios-sim
|
||||
|
||||
echo "▸ building static libs ($DEVICE, $SIM)…"
|
||||
rustup target add "$DEVICE" "$SIM" >/dev/null 2>&1 || true
|
||||
cargo build --release --target "$DEVICE" --lib
|
||||
cargo build --release --target "$SIM" --lib
|
||||
|
||||
echo "▸ generating Swift bindings…"
|
||||
cargo build --release --lib # host dylib for library-mode bindgen
|
||||
cargo run --release --bin uniffi-bindgen -- \
|
||||
generate --library "target/release/libtethercore.dylib" \
|
||||
--language swift --out-dir bindings/swift
|
||||
|
||||
echo "▸ assembling TetherCore.xcframework…"
|
||||
rm -rf build-xcframework && mkdir -p build-xcframework/headers
|
||||
cp bindings/swift/tethercoreFFI.h build-xcframework/headers/
|
||||
cp bindings/swift/tethercoreFFI.modulemap build-xcframework/headers/module.modulemap
|
||||
rm -rf "$APP_DIR/Vendor/TetherCore.xcframework"
|
||||
mkdir -p "$APP_DIR/Vendor"
|
||||
xcodebuild -create-xcframework \
|
||||
-library "target/$DEVICE/release/libtethercore.a" -headers build-xcframework/headers \
|
||||
-library "target/$SIM/release/libtethercore.a" -headers build-xcframework/headers \
|
||||
-output "$APP_DIR/Vendor/TetherCore.xcframework"
|
||||
|
||||
echo "▸ refreshing Swift wrapper in app…"
|
||||
cp bindings/swift/tethercore.swift "$APP_DIR/Sources/tethercore.swift"
|
||||
|
||||
echo "✅ done — open $APP_DIR and build."
|
||||
4
ios/.gitignore
vendored
4
ios/.gitignore
vendored
@@ -18,3 +18,7 @@ TetherApp/Sources/tethercore.swift
|
||||
*.xcuserstate
|
||||
xcuserdata/
|
||||
.swiftpm/
|
||||
|
||||
# macOS app generated artifacts (core/build-apple.sh)
|
||||
MacTetherApp/Vendor/
|
||||
MacTetherApp/Sources/tethercore.swift
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
5AE0D6380E8D23C337DD0EB0 /* TetherKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7A65056F38679ECD9498103E /* TetherKit */; };
|
||||
9386BB2E2BBBCF245A7E605D /* MacTetherStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 010C4E8B3DC255487AE12A75 /* MacTetherStore.swift */; };
|
||||
9617ED2468FED9FC38A37FD8 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 826E98E1B2A0C7A9AC4ED879 /* ContentView.swift */; };
|
||||
B41AF236C08FA6BD15F00807 /* TetherCore.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = EF6782E994649B6E18C307E8 /* TetherCore.xcframework */; };
|
||||
C18BF415C7B42C5FF3235F5E /* tethercore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A8D10256F25095F3FF5047E /* tethercore.swift */; };
|
||||
CB143A31027F3CDB3CF5E894 /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF5D8809D1EC415AB35059B /* MenuView.swift */; };
|
||||
E1F3C377CB6A58F791A83EAA /* MacTetherApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA7FA20BA797A0117F705E7 /* MacTetherApp.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
@@ -21,7 +23,9 @@
|
||||
6F7F88910D568FDE283249B4 /* TetherKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = TetherKit; path = ../TetherKit; sourceTree = SOURCE_ROOT; };
|
||||
7D193BC76AB5D363A4CB1F6C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
826E98E1B2A0C7A9AC4ED879 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
8A8D10256F25095F3FF5047E /* tethercore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tethercore.swift; sourceTree = "<group>"; };
|
||||
BDF5D8809D1EC415AB35059B /* MenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuView.swift; sourceTree = "<group>"; };
|
||||
EF6782E994649B6E18C307E8 /* TetherCore.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = TetherCore.xcframework; path = Vendor/TetherCore.xcframework; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -30,6 +34,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5AE0D6380E8D23C337DD0EB0 /* TetherKit in Frameworks */,
|
||||
B41AF236C08FA6BD15F00807 /* TetherCore.xcframework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -44,6 +49,14 @@
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C74C31B92D5DD6D8CCD0F873 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EF6782E994649B6E18C307E8 /* TetherCore.xcframework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D08DCB1AE8C0173418D178ED /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -52,6 +65,7 @@
|
||||
2EA7FA20BA797A0117F705E7 /* MacTetherApp.swift */,
|
||||
010C4E8B3DC255487AE12A75 /* MacTetherStore.swift */,
|
||||
BDF5D8809D1EC415AB35059B /* MenuView.swift */,
|
||||
8A8D10256F25095F3FF5047E /* tethercore.swift */,
|
||||
);
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
@@ -69,6 +83,7 @@
|
||||
children = (
|
||||
E0AC15A5A587EEB6DB5D715F /* Packages */,
|
||||
D08DCB1AE8C0173418D178ED /* Sources */,
|
||||
C74C31B92D5DD6D8CCD0F873 /* Frameworks */,
|
||||
B17B25DB8865282691E0B024 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
@@ -141,6 +156,7 @@
|
||||
E1F3C377CB6A58F791A83EAA /* MacTetherApp.swift in Sources */,
|
||||
9386BB2E2BBBCF245A7E605D /* MacTetherStore.swift in Sources */,
|
||||
CB143A31027F3CDB3CF5E894 /* MenuView.swift in Sources */,
|
||||
C18BF415C7B42C5FF3235F5E /* tethercore.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -274,6 +290,10 @@
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"Vendor\"",
|
||||
);
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Sources/Info.plist;
|
||||
INFOPLIST_KEY_NSAppTransportSecurity = "";
|
||||
@@ -282,6 +302,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.1.0;
|
||||
OTHER_LDFLAGS = "-framework Security -framework SystemConfiguration -lresolv";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.pecord.tether.mac;
|
||||
SDKROOT = macosx;
|
||||
};
|
||||
@@ -295,6 +316,10 @@
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"Vendor\"",
|
||||
);
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Sources/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -302,6 +327,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.1.0;
|
||||
OTHER_LDFLAGS = "-framework Security -framework SystemConfiguration -lresolv";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.pecord.tether.mac;
|
||||
SDKROOT = macosx;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import SwiftUI
|
||||
import TetherKit
|
||||
|
||||
struct ContentView: View {
|
||||
@Bindable var store: MacTetherStore
|
||||
@@ -72,23 +71,23 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func feedRow(_ msg: Message) -> some View {
|
||||
private func feedRow(_ item: FeedItem) -> some View {
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(msg.text ?? "")
|
||||
Text(item.text)
|
||||
.font(.body)
|
||||
.lineLimit(6)
|
||||
.textSelection(.enabled)
|
||||
HStack {
|
||||
Label(msg.source ?? "unknown", systemImage: sourceIcon(msg.source))
|
||||
Label(item.source, systemImage: sourceIcon(item.source))
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
Spacer()
|
||||
Text(msg.date, style: .time)
|
||||
Text(item.date, style: .time)
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
Button {
|
||||
store.copyToPasteboard(msg)
|
||||
store.copyToPasteboard(item)
|
||||
} label: {
|
||||
Image(systemName: "doc.on.doc")
|
||||
.foregroundStyle(.secondary)
|
||||
@@ -100,8 +99,8 @@ struct ContentView: View {
|
||||
.background(Color.primary.opacity(0.04), in: RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
|
||||
private func sourceIcon(_ source: String?) -> String {
|
||||
let s = source ?? ""
|
||||
private func sourceIcon(_ source: String) -> String {
|
||||
let s = source
|
||||
if s.contains("iphone") || s.contains("ios") { return "iphone" }
|
||||
if s.contains("ipad") { return "ipad" }
|
||||
if s.contains("macos") || s.contains("mac") { return "laptopcomputer" }
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import TetherKit
|
||||
import enum TetherKit.RoomIdentity // selective: avoids TetherKit.Message clashing
|
||||
// with the engine's Message (compiled into this target)
|
||||
// Engine + Message + MessageHandler come from the generated tethercore.swift.
|
||||
|
||||
/// macOS side: same TetherClient wire layer, but we CAN poll NSPasteboard
|
||||
/// in the background — so clipboard changes auto-send without user action.
|
||||
/// UI-facing clipboard item. Decouples the view from the FFI `Message` record.
|
||||
struct FeedItem: Identifiable, Equatable {
|
||||
let id = UUID()
|
||||
let text: String
|
||||
let source: String
|
||||
let date: Date
|
||||
}
|
||||
|
||||
/// macOS side: drives the shared `tethercore.Engine` (SSE + RTC). Unlike iOS we
|
||||
/// CAN poll NSPasteboard in the background, so clipboard changes auto-send.
|
||||
@MainActor @Observable
|
||||
final class MacTetherStore {
|
||||
var serverURL: String = UserDefaults.standard.string(forKey: "serverURL") ?? "http://tether.lan:8765"
|
||||
var room: String = RoomIdentity.derive()
|
||||
var feed: [Message] = []
|
||||
var feed: [FeedItem] = []
|
||||
var connected = false
|
||||
var lastError: String?
|
||||
var hasAttemptedConnect = false
|
||||
|
||||
private let deviceID = Host.current().localizedName ?? UUID().uuidString
|
||||
private var client: TetherClient?
|
||||
private var receiveTask: Task<Void, Never>?
|
||||
private var engine: Engine?
|
||||
private var bridge: EngineBridge?
|
||||
private var pollTask: Task<Void, Never>?
|
||||
private var lastChangeCount = NSPasteboard.general.changeCount
|
||||
|
||||
@@ -25,7 +35,8 @@ final class MacTetherStore {
|
||||
}
|
||||
|
||||
func connect() {
|
||||
guard let base = URL(string: serverURL.trimmingCharacters(in: .whitespaces)) else {
|
||||
let url = serverURL.trimmingCharacters(in: .whitespaces)
|
||||
guard !url.isEmpty else {
|
||||
hasAttemptedConnect = true
|
||||
lastError = "Invalid server URL."
|
||||
return
|
||||
@@ -34,61 +45,79 @@ final class MacTetherStore {
|
||||
UserDefaults.standard.set(serverURL, forKey: "serverURL")
|
||||
disconnect()
|
||||
|
||||
let source = "macos-\(ProcessInfo.processInfo.hostName)"
|
||||
let cfg = TetherClient.Config(baseURL: base, room: room,
|
||||
from: deviceID, source: "macos-app")
|
||||
let c = TetherClient(config: cfg)
|
||||
client = c
|
||||
let b = EngineBridge(store: self)
|
||||
let e = Engine(server: url, room: room, from: deviceID, source: "macos")
|
||||
e.start(handler: b)
|
||||
engine = e
|
||||
bridge = b
|
||||
connected = true
|
||||
lastError = nil
|
||||
|
||||
// Receive: clipboard messages from peers → write to NSPasteboard.
|
||||
receiveTask = Task {
|
||||
for await msg in await c.clipboardStream() {
|
||||
feed.insert(msg, at: 0)
|
||||
if feed.count > 100 { feed.removeLast() }
|
||||
if let text = msg.text {
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(text, forType: .string)
|
||||
lastChangeCount = NSPasteboard.general.changeCount // don't echo back
|
||||
}
|
||||
}
|
||||
connected = false
|
||||
}
|
||||
|
||||
// Send: poll NSPasteboard for local changes → push to room.
|
||||
lastChangeCount = NSPasteboard.general.changeCount
|
||||
pollTask = Task {
|
||||
pollTask = Task { [weak self] in
|
||||
while !Task.isCancelled {
|
||||
try? await Task.sleep(for: .milliseconds(500))
|
||||
guard let self else { return }
|
||||
let count = NSPasteboard.general.changeCount
|
||||
guard count != lastChangeCount else { continue }
|
||||
lastChangeCount = count
|
||||
guard count != self.lastChangeCount else { continue }
|
||||
self.lastChangeCount = count
|
||||
if let text = NSPasteboard.general.string(forType: .string), !text.isEmpty {
|
||||
try? await c.send(text)
|
||||
self.engine?.send(text: text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func disconnect() {
|
||||
receiveTask?.cancel(); receiveTask = nil
|
||||
pollTask?.cancel(); pollTask = nil
|
||||
engine?.stop()
|
||||
engine = nil
|
||||
bridge = nil
|
||||
pollTask?.cancel()
|
||||
pollTask = nil
|
||||
connected = false
|
||||
}
|
||||
|
||||
func send(_ text: String) {
|
||||
guard let c = client else { return }
|
||||
Task {
|
||||
do { try await c.send(text) }
|
||||
catch { lastError = "Send failed: \(error.localizedDescription)" }
|
||||
engine?.send(text: text)
|
||||
}
|
||||
|
||||
func copyToPasteboard(_ item: FeedItem) {
|
||||
writePasteboard(item.text)
|
||||
}
|
||||
|
||||
private func writePasteboard(_ text: String) {
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(text, forType: .string)
|
||||
lastChangeCount = NSPasteboard.general.changeCount // don't echo our own write back
|
||||
}
|
||||
|
||||
// Called from the engine bridge, already on the main actor.
|
||||
fileprivate func ingest(_ item: FeedItem) {
|
||||
feed.insert(item, at: 0)
|
||||
if feed.count > 100 { feed.removeLast() }
|
||||
writePasteboard(item.text) // mirror received clipboard onto this Mac
|
||||
}
|
||||
|
||||
fileprivate func setConnected(_ value: Bool) {
|
||||
connected = value
|
||||
}
|
||||
}
|
||||
|
||||
func copyToPasteboard(_ msg: Message) {
|
||||
guard let text = msg.text else { return }
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(text, forType: .string)
|
||||
lastChangeCount = NSPasteboard.general.changeCount
|
||||
/// Adapts the engine's callback interface (fired on the Rust runtime thread)
|
||||
/// onto the main actor for SwiftUI.
|
||||
private final class EngineBridge: MessageHandler {
|
||||
weak var store: MacTetherStore?
|
||||
init(store: MacTetherStore) { self.store = store }
|
||||
|
||||
func onMessage(msg: Message) {
|
||||
// RTC-delivered messages carry ts=0 (no server stamp) — use now.
|
||||
let date = msg.ts > 0 ? Date(timeIntervalSince1970: Double(msg.ts) / 1000.0) : Date()
|
||||
let item = FeedItem(text: msg.text, source: msg.source.isEmpty ? "unknown" : msg.source, date: date)
|
||||
Task { @MainActor in self.store?.ingest(item) }
|
||||
}
|
||||
|
||||
func onStatus(connected: Bool) {
|
||||
Task { @MainActor in self.store?.setConnected(connected) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import SwiftUI
|
||||
import TetherKit
|
||||
|
||||
struct MenuView: View {
|
||||
@Bindable var store: MacTetherStore
|
||||
@@ -63,18 +62,18 @@ struct MenuView: View {
|
||||
.frame(minHeight: 120, maxHeight: 240)
|
||||
}
|
||||
|
||||
private func feedRow(_ msg: Message) -> some View {
|
||||
private func feedRow(_ item: FeedItem) -> some View {
|
||||
HStack(alignment: .top, spacing: 6) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(msg.text ?? "")
|
||||
Text(item.text)
|
||||
.font(.caption).lineLimit(3)
|
||||
HStack {
|
||||
Text(msg.source ?? "?").font(.caption2).foregroundStyle(.secondary)
|
||||
Text(item.source).font(.caption2).foregroundStyle(.secondary)
|
||||
Spacer()
|
||||
Text(msg.date, style: .time).font(.caption2).foregroundStyle(.secondary)
|
||||
Text(item.date, style: .time).font(.caption2).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
Button { store.copyToPasteboard(msg) } label: {
|
||||
Button { store.copyToPasteboard(item) } label: {
|
||||
Image(systemName: "doc.on.doc").font(.caption2)
|
||||
}
|
||||
.buttonStyle(.plain).foregroundStyle(.secondary)
|
||||
|
||||
@@ -16,10 +16,14 @@ targets:
|
||||
sources:
|
||||
- Sources
|
||||
dependencies:
|
||||
- package: TetherKit
|
||||
- package: TetherKit # RoomIdentity only — networking now lives in TetherCore
|
||||
- framework: Vendor/TetherCore.xcframework
|
||||
embed: false # static lib, nothing to embed
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: io.pecord.tether.mac
|
||||
# Rust staticlib pulls in SecRandomCopyBytes (ring) + system config (reqwest).
|
||||
OTHER_LDFLAGS: "-framework Security -framework SystemConfiguration -lresolv"
|
||||
MARKETING_VERSION: "0.1.0"
|
||||
CURRENT_PROJECT_VERSION: "1"
|
||||
GENERATE_INFOPLIST_FILE: true
|
||||
|
||||
Reference in New Issue
Block a user