Add macOS window UI (ContentView) alongside menu bar

WindowGroup with resizable window (560×520 default): connection toolbar,
feed list with source icons + text selection + copy button, composer with
Cmd+Return shortcut. Both window and menu bar share the same store.
Removed LSUIElement — app now shows in Dock.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 15:27:55 -05:00
parent c4334143b0
commit c73108a126
5 changed files with 145 additions and 7 deletions

View File

@@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
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 */; };
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 */
@@ -19,6 +20,7 @@
6C53AB32EB02089B09AD31D6 /* MacTetherApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MacTetherApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
BDF5D8809D1EC415AB35059B /* MenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -45,6 +47,7 @@
D08DCB1AE8C0173418D178ED /* Sources */ = {
isa = PBXGroup;
children = (
826E98E1B2A0C7A9AC4ED879 /* ContentView.swift */,
7D193BC76AB5D363A4CB1F6C /* Info.plist */,
2EA7FA20BA797A0117F705E7 /* MacTetherApp.swift */,
010C4E8B3DC255487AE12A75 /* MacTetherStore.swift */,
@@ -134,6 +137,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9617ED2468FED9FC38A37FD8 /* ContentView.swift in Sources */,
E1F3C377CB6A58F791A83EAA /* MacTetherApp.swift in Sources */,
9386BB2E2BBBCF245A7E605D /* MacTetherStore.swift in Sources */,
CB143A31027F3CDB3CF5E894 /* MenuView.swift in Sources */,
@@ -277,7 +281,6 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
LSUIElement = YES;
MARKETING_VERSION = 0.1.0;
PRODUCT_BUNDLE_IDENTIFIER = io.pecord.tether.mac;
SDKROOT = macosx;
@@ -298,7 +301,6 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
LSUIElement = YES;
MARKETING_VERSION = 0.1.0;
PRODUCT_BUNDLE_IDENTIFIER = io.pecord.tether.mac;
SDKROOT = macosx;

View File

@@ -0,0 +1,134 @@
import SwiftUI
import TetherKit
struct ContentView: View {
@Bindable var store: MacTetherStore
@State private var draft = ""
var body: some View {
VStack(spacing: 0) {
toolbar
Divider()
feedArea
Divider()
composer
}
.frame(minWidth: 480, minHeight: 400)
.task { store.connectIfSaved() }
}
// MARK: - Toolbar
private var toolbar: some View {
HStack(spacing: 10) {
Circle()
.fill(store.connected ? Color.green : Color.secondary.opacity(0.5))
.frame(width: 10, height: 10)
.animation(.easeInOut, value: store.connected)
if store.connected {
Text("Room \(store.room)").font(.callout).foregroundStyle(.secondary)
} else {
TextField("Server URL", text: $store.serverURL)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 240)
}
Spacer()
if let err = store.lastError, store.hasAttemptedConnect {
Text(err).font(.caption).foregroundStyle(.red)
}
Button(store.connected ? "Disconnect" : "Connect") {
store.connected ? store.disconnect() : store.connect()
}
.buttonStyle(.borderedProminent)
.tint(store.connected ? .secondary : .accentColor)
}
.padding(.horizontal, 16)
.padding(.vertical, 10)
}
// MARK: - Feed
private var feedArea: some View {
List(store.feed) { msg in
feedRow(msg)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 4, leading: 12, bottom: 4, trailing: 12))
}
.listStyle(.plain)
.overlay {
if store.feed.isEmpty {
ContentUnavailableView(
"No messages yet",
systemImage: "link.circle",
description: Text(store.connected
? "Copy anything on a connected device — it'll appear here."
: "Connect to your tether server to start syncing.")
)
}
}
}
private func feedRow(_ msg: Message) -> some View {
HStack(alignment: .top, spacing: 10) {
VStack(alignment: .leading, spacing: 4) {
Text(msg.text ?? "")
.font(.body)
.lineLimit(6)
.textSelection(.enabled)
HStack {
Label(msg.source ?? "unknown", systemImage: sourceIcon(msg.source))
.font(.caption).foregroundStyle(.secondary)
Spacer()
Text(msg.date, style: .time)
.font(.caption).foregroundStyle(.secondary)
}
}
Button {
store.copyToPasteboard(msg)
} label: {
Image(systemName: "doc.on.doc")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
.help("Copy to clipboard")
}
.padding(10)
.background(Color.primary.opacity(0.04), in: RoundedRectangle(cornerRadius: 8))
}
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" }
if s.contains("windows") { return "pc" }
return "desktopcomputer"
}
// MARK: - Composer
private var composer: some View {
HStack(spacing: 10) {
TextField("Type a message…", text: $draft, axis: .vertical)
.textFieldStyle(.roundedBorder)
.lineLimit(1...4)
.onSubmit {
guard !draft.isEmpty else { return }
store.send(draft); draft = ""
}
Button {
store.send(draft); draft = ""
} label: {
Image(systemName: "paperplane.fill")
}
.keyboardShortcut(.return, modifiers: .command)
.buttonStyle(.borderedProminent)
.disabled(!store.connected || draft.isEmpty)
}
.padding(12)
}
}

View File

@@ -18,8 +18,6 @@
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSUIElement</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>

View File

@@ -3,7 +3,14 @@ import SwiftUI
@main
struct MacTetherApp: App {
@State private var store = MacTetherStore()
var body: some Scene {
WindowGroup {
ContentView(store: store)
}
.windowResizability(.contentMinSize)
.defaultSize(width: 560, height: 520)
MenuBarExtra("tether", systemImage: store.connected ? "link.circle.fill" : "link.circle") {
MenuView(store: store)
.frame(width: 320)

View File

@@ -25,15 +25,12 @@ targets:
GENERATE_INFOPLIST_FILE: true
DEVELOPMENT_TEAM: ""
CODE_SIGN_STYLE: Automatic
# Hide from Dock — menu bar only
LSUIElement: true
configs:
Debug:
INFOPLIST_KEY_NSAppTransportSecurity: ""
info:
path: Sources/Info.plist
properties:
LSUIElement: true
NSAppTransportSecurity:
NSAllowsLocalNetworking: true
NSLocalNetworkUsageDescription: "tether connects to your clipboard relay on the local network."