vibetunnel/docs/platform/macos.md

5.2 KiB

macOS Development

Project Setup

Requirements

  • macOS 14.0+
  • Xcode 16.0+
  • Swift 6.0

Build & Run

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

@MainActor
@Observable
class ServerManager {
    private(set) var isRunning = false
    private(set) var sessions: [Session] = []
}

Protocol-Based Services

@MainActor
protocol VibeTunnelServer: AnyObject {
    var isRunning: Bool { get }
    func start() async throws
    func stop() async
}

SwiftUI Menu Bar

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

// 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

struct SettingsView: View {
    @AppStorage("serverPort") private var port = "4020"
    
    var body: some View {
        Form {
            TextField("Port:", text: $port)
        }
    }
}

Menu Bar App

App Lifecycle

@main
struct VibeTunnelApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        MenuBarExtra("VibeTunnel", systemImage: "terminal") {
            MenuBarView()
        }
        .menuBarExtraStyle(.menu)
    }
}

Status Updates

// 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

<!-- VibeTunnel.entitlements -->
<dict>
    <key>com.apple.security.network.client</key>
    <true/>
    <key>com.apple.security.network.server</key>
    <true/>
    <key>com.apple.security.files.user-selected.read-write</key>
    <true/>
</dict>

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

import Sparkle

class UpdateManager {
    let updater = SPUStandardUpdaterController(
        startingUpdater: true,
        updaterDelegate: nil,
        userDriverDelegate: nil
    )
    
    func checkForUpdates() {
        updater.checkForUpdates()
    }
}

Configuration

<!-- Info.plist -->
<key>SUFeedURL</key>
<string>https://vibetunnel.com/appcast.xml</string>
<key>SUEnableAutomaticChecks</key>
<true/>

Debugging

Console Logs

os_log(.debug, log: .server, "Starting server on port %{public}@", port)

View Logs

# In Console.app
# Filter: subsystem:com.steipete.VibeTunnel

# Or via script
./scripts/vtlog.sh -c ServerManager

Testing

Unit Tests

xcodebuild test \
  -project VibeTunnel.xcodeproj \
  -scheme VibeTunnel \
  -destination 'platform=macOS'

UI Tests

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