Skip to content

Integration Guide

This guide covers CrossPlatformTimer utilities and integration patterns for building complete applications with OSClient and SwiftNCurses. If you're just learning the individual APIs, check the API reference first. This is where the pieces come together.

CrossPlatformTimer API

Timer Creation

import CrossPlatformTimer

// Create a timer
let timer = createCompatibleTimer(
    interval: 1.0,
    repeats: true
) {
    print("Timer fired!")
}

// One-shot timer
let oneShot = createCompatibleTimer(
    interval: 5.0,
    repeats: false
) {
    print("One-time action")
}

Timer Management

// Platform-specific timer handling
#if canImport(Darwin)
// macOS/iOS timer implementation
let timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: repeats, block: action)
#else
// Linux timer implementation
let timer = DispatchSource.makeTimerSource(queue: .main)
timer.schedule(deadline: .now() + interval, repeating: repeats ? interval : .never)
timer.setEventHandler(handler: action)
timer.resume()
#endif

High-Performance Timing

// For animation timing (60+ FPS)
let animationTimer = createCompatibleTimer(interval: 1.0/60.0, repeats: true) {
    // Update animation frame
    updateFrame()
}

// For periodic background tasks
let backgroundTimer = createCompatibleTimer(interval: 30.0, repeats: true) {
    // Perform background maintenance
    performMaintenance()
}

Complete Application Example

Full OpenStack TUI Application

import OSClient
import SwiftNCurses
import CrossPlatformTimer

@main
struct MyOpenStackApp {
    static func main() async {
        let screen = SwiftNCurses.initializeScreen()
        defer { SwiftNCurses.cleanup(screen) }

        // Initialize OpenStack client
        let client = try await OpenStackClient.connect(
            config: OpenStackConfig(authUrl: "https://keystone.example.com:5000/v3"),
            credentials: OpenStackCredentials(
                username: "admin",
                password: "secret",
                projectName: "admin"
            )
        )

        // Create UI surface
        let surface = SwiftNCurses.surface(from: screen)
        let bounds = Rect(x: 0, y: 0, width: 80, height: 24)

        // Set up refresh timer
        let refreshTimer = createCompatibleTimer(interval: 5.0, repeats: true) {
            Task {
                await updateServerList(client: client, surface: surface, bounds: bounds)
            }
        }

        // Main application loop
        var running = true
        while running {
            let key = SwiftNCurses.getInput(screen)
            if key == 113 { // 'q' key
                running = false
            }
        }
    }
}

func updateServerList(client: OpenStackClient, surface: Surface, bounds: Rect) async {
    do {
        let servers = try await client.nova.servers.list()
        let serverNames = servers.map { $0.name }

        let listComponent = List(items: serverNames)
        await SwiftNCurses.render(listComponent, on: surface, in: bounds)
        SwiftNCurses.refresh(surface.screen)
    } catch {
        let errorText = Text("Error: \(error.localizedDescription)").color(.red)
        await SwiftNCurses.render(errorText, on: surface, in: bounds)
    }
}

Common Integration Patterns

Pattern 1: Server List with Auto-Refresh

actor ServerListView {
    private let client: OpenStackClient
    private var servers: [Server] = []
    private var selectedIndex: Int = 0
    private var refreshTimer: Timer?

    init(client: OpenStackClient) {
        self.client = client
    }

    func startAutoRefresh() {
        refreshTimer = createCompatibleTimer(interval: 5.0, repeats: true) {
            Task {
                await self.refresh()
            }
        }
    }

    func refresh() async {
        do {
            servers = try await client.nova.servers.list()
        } catch {
            print("Error refreshing servers: \(error)")
        }
    }

    func render(on surface: Surface, in bounds: Rect) async {
        let serverNames = servers.map { $0.name }
        let list = List(items: serverNames)
            .selectedIndex(selectedIndex)
            .scrollable(true)

        await SwiftNCurses.render(list, on: surface, in: bounds)
    }

    func moveUp() {
        selectedIndex = max(0, selectedIndex - 1)
    }

    func moveDown() {
        selectedIndex = min(servers.count - 1, selectedIndex + 1)
    }

    func selectedServer() -> Server? {
        guard selectedIndex < servers.count else { return nil }
        return servers[selectedIndex]
    }
}

Pattern 2: Multi-View Application

enum AppView {
    case dashboard
    case servers
    case networks
    case volumes
}

actor Application {
    private let client: OpenStackClient
    private let screen: Screen
    private var currentView: AppView = .dashboard
    private var running = true

    init(client: OpenStackClient, screen: Screen) {
        self.client = client
        self.screen = screen
    }

    func run() async {
        while running {
            await render()
            await handleInput()
        }
    }

    private func render() async {
        let surface = SwiftNCurses.surface(from: screen)
        let bounds = Rect(x: 0, y: 0, width: 80, height: 24)

        switch currentView {
        case .dashboard:
            await renderDashboard(on: surface, in: bounds)
        case .servers:
            await renderServers(on: surface, in: bounds)
        case .networks:
            await renderNetworks(on: surface, in: bounds)
        case .volumes:
            await renderVolumes(on: surface, in: bounds)
        }

        SwiftNCurses.refresh(screen)
    }

    private func handleInput() async {
        let key = SwiftNCurses.getInput(screen)

        switch key {
        case Int32(UnicodeScalar("d").value):
            currentView = .dashboard
        case Int32(UnicodeScalar("s").value):
            currentView = .servers
        case Int32(UnicodeScalar("n").value):
            currentView = .networks
        case Int32(UnicodeScalar("v").value):
            currentView = .volumes
        case Int32(UnicodeScalar("q").value):
            running = false
        default:
            break
        }
    }

    private func renderDashboard(on surface: Surface, in bounds: Rect) async {
        let text = Text("Dashboard View - Press 's' for servers, 'n' for networks, 'v' for volumes")
            .bold()
        await SwiftNCurses.render(text, on: surface, in: bounds)
    }

    private func renderServers(on surface: Surface, in bounds: Rect) async {
        do {
            let servers = try await client.nova.servers.list()
            let columns = [
                TableColumn(header: "Name", width: 30) { $0.name },
                TableColumn(header: "Status", width: 15) { $0.status.rawValue },
                TableColumn(header: "Created", width: 20) { formatDate($0.created) }
            ]

            let table = Table(data: servers, columns: columns)
            await SwiftNCurses.render(table, on: surface, in: bounds)
        } catch {
            let errorText = Text("Error loading servers: \(error)").color(.red)
            await SwiftNCurses.render(errorText, on: surface, in: bounds)
        }
    }

    private func renderNetworks(on surface: Surface, in bounds: Rect) async {
        // Similar to renderServers
    }

    private func renderVolumes(on surface: Surface, in bounds: Rect) async {
        // Similar to renderServers
    }
}

func formatDate(_ date: Date) -> String {
    let formatter = DateFormatter()
    formatter.dateStyle = .short
    formatter.timeStyle = .short
    return formatter.string(from: date)
}

Pattern 3: Form-Based Resource Creation

actor ServerCreateView {
    private let client: OpenStackClient
    private var name: String = ""
    private var flavorId: String = ""
    private var imageId: String = ""
    private var currentField: Int = 0

    init(client: OpenStackClient) {
        self.client = client
    }

    func render(on surface: Surface, in bounds: Rect) async {
        let form = Form {
            FormField.text(
                label: "Server Name",
                value: name,
                isSelected: currentField == 0,
                isRequired: true
            )
            FormField.selector(
                label: "Flavor",
                items: await loadFlavors(),
                selectedId: flavorId,
                isSelected: currentField == 1
            )
            FormField.selector(
                label: "Image",
                items: await loadImages(),
                selectedId: imageId,
                isSelected: currentField == 2
            )
        }

        await SwiftNCurses.render(form, on: surface, in: bounds)
    }

    private func loadFlavors() async -> [Flavor] {
        do {
            return try await client.nova.flavors.list()
        } catch {
            return []
        }
    }

    private func loadImages() async -> [Image] {
        do {
            return try await client.glance.images.list()
        } catch {
            return []
        }
    }

    func nextField() {
        currentField = min(2, currentField + 1)
    }

    func previousField() {
        currentField = max(0, currentField - 1)
    }

    func submit() async throws {
        guard !name.isEmpty, !flavorId.isEmpty, !imageId.isEmpty else {
            throw ValidationError.incompleteForm
        }

        let server = try await client.nova.servers.create(
            name: name,
            flavorRef: flavorId,
            imageRef: imageId
        )

        print("Created server: \(server.id)")
    }
}

enum ValidationError: Error {
    case incompleteForm
}

Pattern 4: Real-Time Status Monitoring

actor StatusMonitor {
    private let client: OpenStackClient
    private var healthMetrics: [String: Any] = [:]
    private var monitorTimer: Timer?

    init(client: OpenStackClient) {
        self.client = client
    }

    func startMonitoring() {
        monitorTimer = createCompatibleTimer(interval: 10.0, repeats: true) {
            Task {
                await self.updateMetrics()
            }
        }
    }

    func stopMonitoring() {
        monitorTimer?.invalidate()
        monitorTimer = nil
    }

    private func updateMetrics() async {
        do {
            // Get cache statistics
            let cacheStats = await client.cacheManager.statistics()
            healthMetrics["cacheHitRate"] = cacheStats.hitRate
            healthMetrics["cacheSize"] = cacheStats.currentSize

            // Get performance metrics
            let perfMonitor = await client.performanceMonitor
            let perfMetrics = await perfMonitor.metrics()
            healthMetrics["apiCallCount"] = perfMetrics.apiCallCount
            healthMetrics["averageLatency"] = perfMetrics.averageLatency

            // Count resources
            let serverCount = try await client.nova.servers.list().count
            healthMetrics["serverCount"] = serverCount
        } catch {
            print("Error updating metrics: \(error)")
        }
    }

    func render(on surface: Surface, in bounds: Rect) async {
        let metricsText = """
        Cache Hit Rate: \(String(format: "%.1f%%", (healthMetrics["cacheHitRate"] as? Double ?? 0) * 100))
        Cache Size: \(healthMetrics["cacheSize"] as? Int ?? 0) bytes
        API Calls: \(healthMetrics["apiCallCount"] as? Int ?? 0)
        Avg Latency: \(String(format: "%.2f", healthMetrics["averageLatency"] as? TimeInterval ?? 0))s
        Servers: \(healthMetrics["serverCount"] as? Int ?? 0)
        """

        let text = Text(metricsText).color(.green)
        await SwiftNCurses.render(text, on: surface, in: bounds)
    }
}

Using Individual Packages

OSClient Only

import OSClient

let client = try await OpenStackClient.connect(
    config: OpenStackConfig(authUrl: "https://keystone.example.com:5000/v3"),
    credentials: OpenStackCredentials(
        username: "admin",
        password: "secret",
        projectName: "admin"
    )
)

let servers = try await client.nova.servers.list()
for server in servers {
    print("\(server.name): \(server.status)")
}

SwiftNCurses Only

import SwiftNCurses

let screen = SwiftNCurses.initializeScreen()
defer { SwiftNCurses.cleanup(screen) }

let surface = SwiftNCurses.surface(from: screen)
let bounds = Rect(x: 0, y: 0, width: 80, height: 24)

await SwiftNCurses.render(
    Text("Hello, World!").bold().color(.blue),
    on: surface,
    in: bounds
)

SwiftNCurses.refresh(screen)

// Wait for input
_ = SwiftNCurses.getInput(screen)

CrossPlatformTimer Only

import CrossPlatformTimer

let timer = createCompatibleTimer(interval: 1.0, repeats: true) {
    print("Tick: \(Date())")
}

// Keep running for 10 seconds
Thread.sleep(forTimeInterval: 10.0)

timer.invalidate()

Best Practices

1. Separate Concerns with Actors

// Good: Each view is an actor
actor ServerListView { }
actor NetworkListView { }
actor DashboardView { }

// Coordinate with a main application actor
actor Application {
    private let serverView: ServerListView
    private let networkView: NetworkListView
    private let dashboardView: DashboardView
}

2. Handle Errors Gracefully

// Good: Show user-friendly error messages
do {
    let servers = try await client.nova.servers.list()
} catch OpenStackError.authentication(let message) {
    await showError("Authentication failed: \(message)")
} catch OpenStackError.timeout {
    await showError("Request timed out. Check your connection.")
} catch {
    await showError("An error occurred: \(error.localizedDescription)")
}

3. Use Timers for Background Updates

// Good: Periodic refresh with timer
let refreshTimer = createCompatibleTimer(interval: 5.0, repeats: true) {
    Task {
        await refreshData()
    }
}

// Don't: Busy loop
// while true {
//     await refreshData()
//     Thread.sleep(forTimeInterval: 5.0)  // Blocks thread
// }

4. Clean Up Resources

// Good: Use defer for cleanup
func runApp() async {
    let screen = SwiftNCurses.initializeScreen()
    defer { SwiftNCurses.cleanup(screen) }

    let timer = createCompatibleTimer(...)
    defer { timer.invalidate() }

    // App code here
}

5. Optimize Rendering

// Good: Only render when data changes
var lastRenderedServers: [Server] = []

func render() async {
    guard servers != lastRenderedServers else { return }

    await SwiftNCurses.render(serverList, on: surface, in: bounds)
    SwiftNCurses.refresh(screen)

    lastRenderedServers = servers
}

// Don't: Render on every loop iteration
// while running {
//     await SwiftNCurses.render(...)  // Wasteful if nothing changed
// }

Testing Integration

Mock OpenStack Client

#if DEBUG
actor MockOpenStackClient: OpenStackClient {
    var mockServers: [Server] = []

    func nova.servers.list() async throws -> [Server] {
        return mockServers
    }
}
#endif

Integration Tests

func testServerListIntegration() async throws {
    let mockClient = MockOpenStackClient()
    mockClient.mockServers = [
        Server(id: "1", name: "test-server", status: .active)
    ]

    let view = ServerListView(client: mockClient)
    await view.refresh()

    let servers = await view.servers
    XCTAssertEqual(servers.count, 1)
    XCTAssertEqual(servers[0].name, "test-server")
}

Troubleshooting

Common Integration Issues

Issue: Timer not firing

// Problem: Timer not retained
func startTimer() {
    let timer = createCompatibleTimer(...)  // Goes out of scope
}

// Solution: Keep reference
class MyApp {
    var timer: Timer?

    func startTimer() {
        timer = createCompatibleTimer(...)
    }
}

Issue: Screen not updating

// Problem: Forgot to call refresh
await SwiftNCurses.render(component, on: surface, in: bounds)
// Missing: SwiftNCurses.refresh(screen)

// Solution: Always refresh after rendering
await SwiftNCurses.render(component, on: surface, in: bounds)
SwiftNCurses.refresh(screen)  // Now screen updates

Issue: Actor isolation errors

// Problem: Accessing actor state from non-isolated context
let servers = view.servers  // Error: actor-isolated property

// Solution: Use await
let servers = await view.servers  // Correct

See Also: