# macOS Development
## Project Setup
### Requirements
- macOS 14.0+
- Xcode 16.0+
- Swift 6.0
### Build & Run
```bash
cd mac
# Debug build
xcodebuild -project VibeTunnel.xcodeproj -scheme VibeTunnel
# Release build
./scripts/build.sh
# With code signing
./scripts/build.sh --sign
# Run directly
open build/Release/VibeTunnel.app
```
## Architecture
### Core Components
| Component | Location | Purpose |
|-----------|----------|---------|
| ServerManager | `Core/Services/ServerManager.swift` | Server lifecycle |
| SessionMonitor | `Core/Services/SessionMonitor.swift` | Track sessions |
| TTYForwardManager | `Core/Services/TTYForwardManager.swift` | CLI integration |
| MenuBarViewModel | `Presentation/ViewModels/MenuBarViewModel.swift` | UI state |
### Key Patterns
**Observable State**
```swift
@MainActor
@Observable
class ServerManager {
private(set) var isRunning = false
private(set) var sessions: [Session] = []
}
```
**Protocol-Based Services**
```swift
@MainActor
protocol VibeTunnelServer: AnyObject {
var isRunning: Bool { get }
func start() async throws
func stop() async
}
```
**SwiftUI Menu Bar**
```swift
struct MenuBarView: View {
@StateObject private var viewModel = MenuBarViewModel()
var body: some View {
Menu("VT", systemImage: "terminal") {
ForEach(viewModel.sessions) { session in
SessionRow(session: session)
}
}
}
}
```
## Server Integration
### Embedded Server
```
VibeTunnel.app/
└── Contents/
├── MacOS/
│ └── VibeTunnel # Main executable
└── Resources/
└── server/
└── bun-server # Embedded Bun binary
```
### Server Launch
```swift
// ServerManager.swift
func start() async throws {
let serverPath = Bundle.main.resourcePath! + "/server/bun-server"
process = Process()
process.executableURL = URL(fileURLWithPath: serverPath)
process.arguments = ["--port", port]
try process.run()
}
```
## Settings Management
### UserDefaults Keys
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| serverPort | String | "4020" | Server port |
| autostart | Bool | false | Launch at login |
| allowLAN | Bool | false | LAN connections |
| useDevServer | Bool | false | Development mode |
### Settings Window
```swift
struct SettingsView: View {
@AppStorage("serverPort") private var port = "4020"
var body: some View {
Form {
TextField("Port:", text: $port)
}
}
}
```
## Menu Bar App
### App Lifecycle
```swift
@main
struct VibeTunnelApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
MenuBarExtra("VibeTunnel", systemImage: "terminal") {
MenuBarView()
}
.menuBarExtraStyle(.menu)
}
}
```
### Status Updates
```swift
// Update menu bar icon based on state
func updateStatusItem() {
if serverManager.isRunning {
statusItem.button?.image = NSImage(systemSymbolName: "terminal.fill")
} else {
statusItem.button?.image = NSImage(systemSymbolName: "terminal")
}
}
```
## Code Signing
### Entitlements
```xml
com.apple.security.network.client
com.apple.security.network.server
com.apple.security.files.user-selected.read-write
```
### Build Settings
```
# version.xcconfig
MARKETING_VERSION = 1.0.0
CURRENT_PROJECT_VERSION = 100
# Shared.xcconfig
CODE_SIGN_IDENTITY = Developer ID Application
DEVELOPMENT_TEAM = TEAMID
```
## Sparkle Updates
### Integration
```swift
import Sparkle
class UpdateManager {
let updater = SPUStandardUpdaterController(
startingUpdater: true,
updaterDelegate: nil,
userDriverDelegate: nil
)
func checkForUpdates() {
updater.checkForUpdates()
}
}
```
### Configuration
```xml
SUFeedURL
https://vibetunnel.com/appcast.xml
SUEnableAutomaticChecks
```
## Debugging
### Console Logs
```swift
os_log(.debug, log: .server, "Starting server on port %{public}@", port)
```
### View Logs
```bash
# In Console.app
# Filter: subsystem:com.steipete.VibeTunnel
# Or via script
./scripts/vtlog.sh -c ServerManager
```
## Testing
### Unit Tests
```bash
xcodebuild test \
-project VibeTunnel.xcodeproj \
-scheme VibeTunnel \
-destination 'platform=macOS'
```
### UI Tests
```swift
class VibeTunnelUITests: XCTestCase {
func testServerStart() throws {
let app = XCUIApplication()
app.launch()
app.menuBarItems["VibeTunnel"].click()
app.menuItems["Start Server"].click()
XCTAssertTrue(app.menuItems["Stop Server"].exists)
}
}
```
## Common Issues
| Issue | Solution |
|-------|----------|
| Server won't start | Check port availability |
| Menu bar not showing | Check LSUIElement in Info.plist |
| Updates not working | Verify Sparkle feed URL |
| Permissions denied | Add entitlements |
## See Also
- [Architecture](../core/architecture.md)
- [Development Guide](../guides/development.md)
- [iOS Companion](ios.md)