diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index fefecb91..a5851ce4 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -206,6 +206,9 @@ jobs: -scheme VibeTunnel-Mac \ -configuration Debug \ -destination "platform=macOS" \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO \ | xcbeautify || { echo "::error::Tests failed" exit 1 diff --git a/ios/VibeTunnel/App/VibeTunnelApp.swift b/ios/VibeTunnel/App/VibeTunnelApp.swift index 6fc1acda..e54c5a70 100644 --- a/ios/VibeTunnel/App/VibeTunnelApp.swift +++ b/ios/VibeTunnel/App/VibeTunnelApp.swift @@ -28,10 +28,10 @@ struct VibeTunnelApp: App { } #if targetEnvironment(macCatalyst) .macCatalystWindowStyle(getStoredWindowStyle()) - #endif + #endif } } - + #if targetEnvironment(macCatalyst) private func getStoredWindowStyle() -> MacWindowStyle { let styleRaw = UserDefaults.standard.string(forKey: "macWindowStyle") ?? "standard" @@ -45,7 +45,8 @@ struct VibeTunnelApp: App { if url.host == "session", let sessionId = url.pathComponents.last, - !sessionId.isEmpty { + !sessionId.isEmpty + { navigationManager.navigateToSession(sessionId) } } @@ -75,7 +76,8 @@ class ConnectionManager { private func loadSavedConnection() { if let data = UserDefaults.standard.data(forKey: "savedServerConfig"), - let config = try? JSONDecoder().decode(ServerConfig.self, from: data) { + let config = try? JSONDecoder().decode(ServerConfig.self, from: data) + { self.serverConfig = config } } diff --git a/ios/VibeTunnel/Configuration/AppConfig.swift b/ios/VibeTunnel/Configuration/AppConfig.swift index 7736f336..64cfe01e 100644 --- a/ios/VibeTunnel/Configuration/AppConfig.swift +++ b/ios/VibeTunnel/Configuration/AppConfig.swift @@ -6,12 +6,12 @@ enum AppConfig { /// Change this to control verbosity of logs static func configureLogging() { #if DEBUG - // In debug builds, default to info level to reduce noise - // Change to .verbose only when debugging binary protocol issues - Logger.globalLevel = .info + // In debug builds, default to info level to reduce noise + // Change to .verbose only when debugging binary protocol issues + Logger.globalLevel = .info #else - // In release builds, only show warnings and errors - Logger.globalLevel = .warning + // In release builds, only show warnings and errors + Logger.globalLevel = .warning #endif } } diff --git a/ios/VibeTunnel/Models/CastFile.swift b/ios/VibeTunnel/Models/CastFile.swift index d6dba9cd..62f4a7c2 100644 --- a/ios/VibeTunnel/Models/CastFile.swift +++ b/ios/VibeTunnel/Models/CastFile.swift @@ -181,7 +181,8 @@ class CastRecorder { let eventArray: [Any] = [event.time, event.type, event.data] if let jsonData = try? JSONSerialization.data(withJSONObject: eventArray), - let jsonString = String(data: jsonData, encoding: .utf8) { + let jsonString = String(data: jsonData, encoding: .utf8) + { castContent += jsonString + "\n" } } @@ -292,7 +293,7 @@ class CastPlayer { } } } - + /// Modern async version of play that supports cancellation and error handling. /// /// - Parameter onEvent: Async closure called for each event during playback. @@ -305,12 +306,12 @@ class CastPlayer { for event in events { // Check for cancellation try Task.checkCancellation() - + // Wait for the appropriate time if event.time > 0 { try await Task.sleep(nanoseconds: UInt64(event.time * 1_000_000_000)) } - + await onEvent(event) } } diff --git a/ios/VibeTunnel/Models/ServerProfile.swift b/ios/VibeTunnel/Models/ServerProfile.swift index 5f0a2ced..28246063 100644 --- a/ios/VibeTunnel/Models/ServerProfile.swift +++ b/ios/VibeTunnel/Models/ServerProfile.swift @@ -37,20 +37,20 @@ struct ServerProfile: Identifiable, Codable, Equatable { /// Create a ServerConfig from this profile func toServerConfig(password: String? = nil) -> ServerConfig? { guard let urlComponents = URLComponents(string: url), - let host = urlComponents.host else { + let host = urlComponents.host + else { return nil } - + // Determine default port based on scheme - let defaultPort: Int - if let scheme = urlComponents.scheme?.lowercased() { - defaultPort = scheme == "https" ? 443 : 80 + let defaultPort: Int = if let scheme = urlComponents.scheme?.lowercased() { + scheme == "https" ? 443 : 80 } else { - defaultPort = 80 + 80 } - + let port = urlComponents.port ?? defaultPort - + return ServerConfig( host: host, port: port, @@ -68,7 +68,8 @@ extension ServerProfile { /// Load all saved profiles from UserDefaults static func loadAll() -> [ServerProfile] { guard let data = UserDefaults.standard.data(forKey: storageKey), - let profiles = try? JSONDecoder().decode([ServerProfile].self, from: data) else { + let profiles = try? JSONDecoder().decode([ServerProfile].self, from: data) + else { return [] } return profiles @@ -117,7 +118,8 @@ extension ServerProfile { static func suggestedName(for url: String) -> String { if let urlComponents = URLComponents(string: url), - let host = urlComponents.host { + let host = urlComponents.host + { // Remove common suffixes let cleanHost = host .replacingOccurrences(of: ".local", with: "") diff --git a/ios/VibeTunnel/Models/TerminalData.swift b/ios/VibeTunnel/Models/TerminalData.swift index 601d56d6..4690383c 100644 --- a/ios/VibeTunnel/Models/TerminalData.swift +++ b/ios/VibeTunnel/Models/TerminalData.swift @@ -45,7 +45,8 @@ enum TerminalEvent { let exitString = array[0] as? String, exitString == "exit", let exitCode = array[1] as? Int, - let sessionId = array[2] as? String { + let sessionId = array[2] as? String + { self = .exit(code: exitCode, sessionId: sessionId) return } diff --git a/ios/VibeTunnel/Models/TerminalRenderer.swift b/ios/VibeTunnel/Models/TerminalRenderer.swift index 52c1d3c6..60f96088 100644 --- a/ios/VibeTunnel/Models/TerminalRenderer.swift +++ b/ios/VibeTunnel/Models/TerminalRenderer.swift @@ -27,7 +27,8 @@ enum TerminalRenderer: String, CaseIterable, Codable { static var selected: Self { get { if let rawValue = UserDefaults.standard.string(forKey: "selectedTerminalRenderer"), - let renderer = Self(rawValue: rawValue) { + let renderer = Self(rawValue: rawValue) + { return renderer } return .swiftTerm // Default diff --git a/ios/VibeTunnel/Services/APIClient.swift b/ios/VibeTunnel/Services/APIClient.swift index db978050..8becb775 100644 --- a/ios/VibeTunnel/Services/APIClient.swift +++ b/ios/VibeTunnel/Services/APIClient.swift @@ -384,7 +384,8 @@ class APIClient: APIClientProtocol { // This is the header if let version = json["version"] as? Int, let width = json["width"] as? Int, - let height = json["height"] as? Int { + let height = json["height"] as? Int + { header = AsciinemaHeader( version: version, width: width, @@ -401,7 +402,8 @@ class APIClient: APIClientProtocol { if json.count >= 3, let timestamp = json[0] as? Double, let typeStr = json[1] as? String, - let eventData = json[2] as? String { + let eventData = json[2] as? String + { let eventType: AsciinemaEvent.EventType switch typeStr { case "o": eventType = .output @@ -479,7 +481,8 @@ class APIClient: APIClientProtocol { showHidden: Bool = false, gitFilter: String = "all" ) - async throws -> DirectoryListing { + async throws -> DirectoryListing + { guard let baseURL else { throw APIError.noServerConfigured } diff --git a/ios/VibeTunnel/Services/BufferWebSocketClient.swift b/ios/VibeTunnel/Services/BufferWebSocketClient.swift index 167d46fe..67bed58a 100644 --- a/ios/VibeTunnel/Services/BufferWebSocketClient.swift +++ b/ios/VibeTunnel/Services/BufferWebSocketClient.swift @@ -47,7 +47,7 @@ enum WebSocketError: Error { @Observable class BufferWebSocketClient: NSObject { static let shared = BufferWebSocketClient() - + private let logger = Logger(category: "BufferWebSocket") /// Magic byte for binary messages private static let bufferMagicByte: UInt8 = 0xBF @@ -114,7 +114,8 @@ class BufferWebSocketClient: NSObject { // Add authentication header if needed if let config = UserDefaults.standard.data(forKey: "savedServerConfig"), let serverConfig = try? JSONDecoder().decode(ServerConfig.self, from: config), - let authHeader = serverConfig.authorizationHeader { + let authHeader = serverConfig.authorizationHeader + { headers["Authorization"] = authHeader } @@ -211,7 +212,8 @@ class BufferWebSocketClient: NSObject { // Decode terminal event if let event = decodeTerminalEvent(from: messageData), - let handler = subscriptions[sessionId] { + let handler = subscriptions[sessionId] + { logger.verbose("Dispatching event to handler") handler(event) } else { @@ -598,14 +600,16 @@ class BufferWebSocketClient: NSObject { (0xFF00...0xFF60).contains(value) || // Fullwidth Forms (0xFFE0...0xFFE6).contains(value) || // Fullwidth Forms (0x20000...0x2FFFD).contains(value) || // CJK Extension B-F - (0x30000...0x3FFFD).contains(value) { // CJK Extension G + (0x30000...0x3FFFD).contains(value) + { // CJK Extension G return 2 } // Zero-width characters if (0x200B...0x200F).contains(value) || // Zero-width spaces (0xFE00...0xFE0F).contains(value) || // Variation selectors - scalar.properties.isJoinControl { + scalar.properties.isJoinControl + { return 0 } diff --git a/ios/VibeTunnel/Services/KeychainService.swift b/ios/VibeTunnel/Services/KeychainService.swift index 761f13f3..2a66c996 100644 --- a/ios/VibeTunnel/Services/KeychainService.swift +++ b/ios/VibeTunnel/Services/KeychainService.swift @@ -4,30 +4,30 @@ import Security /// Service for securely storing credentials in the iOS Keychain enum KeychainService { private static let serviceName = "com.vibetunnel.ios" - + enum KeychainError: Error { case unexpectedData case unexpectedPasswordData case unhandledError(status: OSStatus) case itemNotFound } - + /// Save a password for a server profile static func savePassword(_ password: String, for profileId: UUID) throws { let account = "server-\(profileId.uuidString)" guard let passwordData = password.data(using: .utf8) else { throw KeychainError.unexpectedPasswordData } - + // Check if password already exists let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: serviceName, kSecAttrAccount as String: account ] - + let status = SecItemCopyMatching(query as CFDictionary, nil) - + if status == errSecItemNotFound { // Add new password let attributes: [String: Any] = [ @@ -37,7 +37,7 @@ enum KeychainService { kSecValueData as String: passwordData, kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly ] - + let addStatus = SecItemAdd(attributes as CFDictionary, nil) guard addStatus == errSecSuccess else { throw KeychainError.unhandledError(status: addStatus) @@ -47,7 +47,7 @@ enum KeychainService { let attributes: [String: Any] = [ kSecValueData as String: passwordData ] - + let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary) guard updateStatus == errSecSuccess else { throw KeychainError.unhandledError(status: updateStatus) @@ -56,11 +56,11 @@ enum KeychainService { throw KeychainError.unhandledError(status: status) } } - + /// Retrieve a password for a server profile static func getPassword(for profileId: UUID) throws -> String { let account = "server-\(profileId.uuidString)" - + let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: serviceName, @@ -68,48 +68,49 @@ enum KeychainService { kSecMatchLimit as String: kSecMatchLimitOne, kSecReturnData as String: true ] - + var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) - + guard status == errSecSuccess else { if status == errSecItemNotFound { throw KeychainError.itemNotFound } throw KeychainError.unhandledError(status: status) } - + guard let data = result as? Data, - let password = String(data: data, encoding: .utf8) else { + let password = String(data: data, encoding: .utf8) + else { throw KeychainError.unexpectedData } - + return password } - + /// Delete a password for a server profile static func deletePassword(for profileId: UUID) throws { let account = "server-\(profileId.uuidString)" - + let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: serviceName, kSecAttrAccount as String: account ] - + let status = SecItemDelete(query as CFDictionary) guard status == errSecSuccess || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) } } - + /// Delete all passwords for the app static func deleteAllPasswords() throws { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: serviceName ] - + let status = SecItemDelete(query as CFDictionary) guard status == errSecSuccess || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) diff --git a/ios/VibeTunnel/Services/LivePreviewManager.swift b/ios/VibeTunnel/Services/LivePreviewManager.swift index 25d3159f..62fca4b9 100644 --- a/ios/VibeTunnel/Services/LivePreviewManager.swift +++ b/ios/VibeTunnel/Services/LivePreviewManager.swift @@ -9,25 +9,25 @@ import SwiftUI @Observable final class LivePreviewManager { static let shared = LivePreviewManager() - + private let logger = Logger(category: "LivePreviewManager") private let bufferClient = BufferWebSocketClient.shared private var subscriptions: [String: LivePreviewSubscription] = [:] private var updateTimers: [String: Timer] = [:] - + /// Maximum number of concurrent live previews private let maxConcurrentPreviews = 6 - + /// Update interval for previews (in seconds) private let updateInterval: TimeInterval = 1.0 - + private init() { // Ensure WebSocket is connected when manager is created if !bufferClient.isConnected { bufferClient.connect() } } - + /// Subscribe to live updates for a session. func subscribe(to sessionId: String) -> LivePreviewSubscription { // Check if we already have a subscription @@ -35,30 +35,30 @@ final class LivePreviewManager { existing.referenceCount += 1 return existing } - + // Create new subscription let subscription = LivePreviewSubscription(sessionId: sessionId) subscriptions[sessionId] = subscription - + // Manage concurrent preview limit if subscriptions.count > maxConcurrentPreviews { // Remove oldest subscriptions that have no references let sortedSubs = subscriptions.values .filter { $0.referenceCount == 0 } .sorted { $0.subscriptionTime < $1.subscriptionTime } - + if let oldest = sortedSubs.first { unsubscribe(from: oldest.sessionId) } } - + // Set up WebSocket subscription with throttling var lastUpdateTime: Date = .distantPast var pendingSnapshot: BufferSnapshot? - + bufferClient.subscribe(to: sessionId) { [weak self, weak subscription] event in guard let self, let subscription else { return } - + Task { @MainActor in switch event { case .bufferUpdate(let snapshot): @@ -72,59 +72,60 @@ final class LivePreviewManager { } else { // Store pending update pendingSnapshot = snapshot - + // Schedule delayed update if not already scheduled if self.updateTimers[sessionId] == nil { - let timer = Timer.scheduledTimer(withTimeInterval: self.updateInterval, repeats: false) { _ in - Task { @MainActor in - if let pending = pendingSnapshot { - subscription.latestSnapshot = pending - subscription.lastUpdate = Date() - pendingSnapshot = nil + let timer = Timer + .scheduledTimer(withTimeInterval: self.updateInterval, repeats: false) { _ in + Task { @MainActor in + if let pending = pendingSnapshot { + subscription.latestSnapshot = pending + subscription.lastUpdate = Date() + pendingSnapshot = nil + } + self.updateTimers.removeValue(forKey: sessionId) } - self.updateTimers.removeValue(forKey: sessionId) } - } self.updateTimers[sessionId] = timer } } - + case .exit: subscription.isSessionActive = false - + default: break } } } - + return subscription } - + /// Unsubscribe from a session's live updates. func unsubscribe(from sessionId: String) { guard let subscription = subscriptions[sessionId] else { return } - + subscription.referenceCount -= 1 - + if subscription.referenceCount <= 0 { // Clean up updateTimers[sessionId]?.invalidate() updateTimers.removeValue(forKey: sessionId) bufferClient.unsubscribe(from: sessionId) subscriptions.removeValue(forKey: sessionId) - + logger.debug("Unsubscribed from session: \(sessionId)") } } - + /// Clean up all subscriptions. func cleanup() { for timer in updateTimers.values { timer.invalidate() } updateTimers.removeAll() - + for sessionId in subscriptions.keys { bufferClient.unsubscribe(from: sessionId) } @@ -138,12 +139,12 @@ final class LivePreviewManager { final class LivePreviewSubscription { let sessionId: String let subscriptionTime = Date() - + var latestSnapshot: BufferSnapshot? var lastUpdate = Date() var isSessionActive = true var referenceCount = 1 - + init(sessionId: String) { self.sessionId = sessionId } @@ -153,9 +154,9 @@ final class LivePreviewSubscription { struct LivePreviewModifier: ViewModifier { let sessionId: String let isEnabled: Bool - + @State private var subscription: LivePreviewSubscription? - + func body(content: Content) -> some View { content .onAppear { @@ -173,7 +174,7 @@ struct LivePreviewModifier: ViewModifier { } } -// Environment key for passing subscription down the view hierarchy +/// Environment key for passing subscription down the view hierarchy private struct LivePreviewSubscriptionKey: EnvironmentKey { static let defaultValue: LivePreviewSubscription? = nil } diff --git a/ios/VibeTunnel/Services/QuickLookManager.swift b/ios/VibeTunnel/Services/QuickLookManager.swift index fef20a6c..e571e7f4 100644 --- a/ios/VibeTunnel/Services/QuickLookManager.swift +++ b/ios/VibeTunnel/Services/QuickLookManager.swift @@ -84,7 +84,8 @@ class QuickLookManager: NSObject, ObservableObject { for file in files { if let creationDate = try? file.resourceValues(forKeys: [.creationDateKey]).creationDate, - creationDate < oneHourAgo { + creationDate < oneHourAgo + { try? FileManager.default.removeItem(at: file) } } diff --git a/ios/VibeTunnel/Services/ReconnectionManager.swift b/ios/VibeTunnel/Services/ReconnectionManager.swift index 0629741d..f1b8a13d 100644 --- a/ios/VibeTunnel/Services/ReconnectionManager.swift +++ b/ios/VibeTunnel/Services/ReconnectionManager.swift @@ -9,16 +9,16 @@ class ReconnectionManager { private let maxRetries = 5 private var currentRetry = 0 private var reconnectionTask: Task? - + var isReconnecting = false var nextRetryTime: Date? var lastError: Error? - + init(connectionManager: ConnectionManager) { self.connectionManager = connectionManager setupNetworkMonitoring() } - + private func setupNetworkMonitoring() { // Listen for network changes NotificationCenter.default.addObserver( @@ -28,7 +28,7 @@ class ReconnectionManager { object: nil ) } - + @objc private func networkStatusChanged() { if NetworkMonitor.shared.isConnected && !connectionManager.isConnected { @@ -36,21 +36,21 @@ class ReconnectionManager { startReconnection() } } - + func startReconnection() { guard !isReconnecting, let serverConfig = connectionManager.serverConfig else { return } - + isReconnecting = true currentRetry = 0 lastError = nil - + reconnectionTask?.cancel() reconnectionTask = Task { await performReconnection(config: serverConfig) } } - + func stopReconnection() { isReconnecting = false currentRetry = 0 @@ -58,7 +58,7 @@ class ReconnectionManager { reconnectionTask?.cancel() reconnectionTask = nil } - + private func performReconnection(config: ServerConfig) async { while isReconnecting && currentRetry < maxRetries { // Check if we still have network @@ -67,41 +67,41 @@ class ReconnectionManager { try? await Task.sleep(for: .seconds(5)) continue } - + do { // Attempt connection _ = try await APIClient.shared.getSessions() - + // Success! connectionManager.isConnected = true isReconnecting = false currentRetry = 0 nextRetryTime = nil lastError = nil - + // Update last connection time connectionManager.saveConnection(config) - + return } catch { lastError = error currentRetry += 1 - + if currentRetry < maxRetries { // Calculate exponential backoff let backoffSeconds = min(pow(2.0, Double(currentRetry - 1)), 60.0) nextRetryTime = Date().addingTimeInterval(backoffSeconds) - + try? await Task.sleep(for: .seconds(backoffSeconds)) } } } - + // Max retries reached isReconnecting = false connectionManager.disconnect() } - + deinit { NotificationCenter.default.removeObserver(self) } @@ -111,7 +111,13 @@ class ReconnectionManager { extension ReconnectionManager { /// Calculate the next retry delay using exponential backoff - static func calculateBackoff(attempt: Int, baseDelay: TimeInterval = 1.0, maxDelay: TimeInterval = 60.0) -> TimeInterval { + static func calculateBackoff( + attempt: Int, + baseDelay: TimeInterval = 1.0, + maxDelay: TimeInterval = 60.0 + ) + -> TimeInterval + { let exponentialDelay = baseDelay * pow(2.0, Double(attempt - 1)) return min(exponentialDelay, maxDelay) } diff --git a/ios/VibeTunnel/Services/SSEClient.swift b/ios/VibeTunnel/Services/SSEClient.swift index 34809751..097bb703 100644 --- a/ios/VibeTunnel/Services/SSEClient.swift +++ b/ios/VibeTunnel/Services/SSEClient.swift @@ -117,13 +117,15 @@ final class SSEClient: NSObject, @unchecked Sendable { // Check for exit event if let firstElement = array[0] as? String, firstElement == "exit", let exitCode = array[1] as? Int, - let sessionId = array[2] as? String { + let sessionId = array[2] as? String + { delegate?.sseClient(self, didReceiveEvent: .exit(exitCode: exitCode, sessionId: sessionId)) } // Regular terminal output else if let timestamp = array[0] as? Double, let type = array[1] as? String, - let outputData = array[2] as? String { + let outputData = array[2] as? String + { delegate?.sseClient( self, didReceiveEvent: .terminalOutput(timestamp: timestamp, type: type, data: outputData) diff --git a/ios/VibeTunnel/Utils/ErrorHandling.swift b/ios/VibeTunnel/Utils/ErrorHandling.swift index e22ac960..8ad6d9ba 100644 --- a/ios/VibeTunnel/Utils/ErrorHandling.swift +++ b/ios/VibeTunnel/Utils/ErrorHandling.swift @@ -6,7 +6,7 @@ import SwiftUI struct ErrorAlertModifier: ViewModifier { @Binding var error: Error? let onDismiss: (() -> Void)? - + func body(content: Content) -> some View { content .alert( @@ -29,7 +29,9 @@ extension View { func errorAlert( error: Binding, onDismiss: (() -> Void)? = nil - ) -> some View { + ) + -> some View + { modifier(ErrorAlertModifier(error: error, onDismiss: onDismiss)) } } @@ -70,22 +72,22 @@ extension APIError: RecoverableError { var recoverySuggestion: String? { switch self { case .noServerConfigured: - return "Please configure a server connection in Settings." + "Please configure a server connection in Settings." case .networkError: - return "Check your internet connection and try again." + "Check your internet connection and try again." case .serverError(let code, _): switch code { case 401: - return "Check your authentication credentials in Settings." + "Check your authentication credentials in Settings." case 500...599: - return "The server is experiencing issues. Please try again later." + "The server is experiencing issues. Please try again later." default: - return nil + nil } case .resizeDisabledByServer: - return "Terminal resizing is not supported by this server." + "Terminal resizing is not supported by this server." default: - return nil + nil } } } @@ -97,7 +99,7 @@ struct ErrorBanner: View { let message: String let isOffline: Bool let onDismiss: (() -> Void)? - + init( message: String, isOffline: Bool = false, @@ -107,18 +109,18 @@ struct ErrorBanner: View { self.isOffline = isOffline self.onDismiss = onDismiss } - + var body: some View { HStack(spacing: Theme.Spacing.small) { Image(systemName: isOffline ? "wifi.exclamationmark" : "exclamationmark.triangle.fill") .font(.system(size: 14)) - + Text(message) .font(Theme.Typography.terminalSystem(size: 13)) .fixedSize(horizontal: false, vertical: true) - + Spacer() - + if let onDismiss { Button(action: onDismiss) { Image(systemName: "xmark.circle.fill") @@ -151,7 +153,9 @@ extension Task where Failure == Error { priority: TaskPriority? = nil, errorHandler: @escaping @Sendable (Error) -> Void, operation: @escaping @Sendable () async throws -> T - ) -> Task { + ) + -> Task + { Task(priority: priority) { do { return try await operation() diff --git a/ios/VibeTunnel/Utils/Logger.swift b/ios/VibeTunnel/Utils/Logger.swift index a2e1c2c0..a2cadf48 100644 --- a/ios/VibeTunnel/Utils/Logger.swift +++ b/ios/VibeTunnel/Utils/Logger.swift @@ -24,9 +24,9 @@ struct Logger { // Global log level - only messages at this level or higher will be printed #if DEBUG - nonisolated(unsafe) static var globalLevel: LogLevel = .info // Default to info level in debug builds + nonisolated(unsafe) static var globalLevel: LogLevel = .info // Default to info level in debug builds #else - nonisolated(unsafe) static var globalLevel: LogLevel = .warning // Only warnings and errors in release + nonisolated(unsafe) static var globalLevel: LogLevel = .warning // Only warnings and errors in release #endif init(category: String) { diff --git a/ios/VibeTunnel/Utils/MacCatalystWindow.swift b/ios/VibeTunnel/Utils/MacCatalystWindow.swift index b4059aac..f5ca9bf0 100644 --- a/ios/VibeTunnel/Utils/MacCatalystWindow.swift +++ b/ios/VibeTunnel/Utils/MacCatalystWindow.swift @@ -1,13 +1,13 @@ import SwiftUI #if targetEnvironment(macCatalyst) -import UIKit import Dynamic +import UIKit // MARK: - Window Style enum MacWindowStyle { - case standard // Normal title bar with traffic lights - case inline // Hidden title bar with repositioned traffic lights + case standard // Normal title bar with traffic lights + case inline // Hidden title bar with repositioned traffic lights } // MARK: - UIWindow Extension @@ -26,49 +26,50 @@ extension UIWindow { @MainActor class MacCatalystWindowManager: ObservableObject { static let shared = MacCatalystWindowManager() - + @Published var windowStyle: MacWindowStyle = .standard - + private var window: UIWindow? private var windowResizeObserver: NSObjectProtocol? private var windowDidBecomeKeyObserver: NSObjectProtocol? private let logger = Logger(category: "MacCatalystWindow") - + // Traffic light button configuration private let trafficLightInset = CGPoint(x: 20, y: 20) private let trafficLightSpacing: CGFloat = 20 - + private init() {} - + /// Configure the window with the specified style func configureWindow(_ window: UIWindow, style: MacWindowStyle) { self.window = window self.windowStyle = style - + // Wait for window to be fully initialized DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { self.applyWindowStyle(style) } - + // Observe window events setupWindowObservers() } - + /// Switch between window styles at runtime func setWindowStyle(_ style: MacWindowStyle) { windowStyle = style applyWindowStyle(style) } - + private func applyWindowStyle(_ style: MacWindowStyle) { - guard let window = window, - let nsWindow = window.nsWindow else { + guard let window, + let nsWindow = window.nsWindow + else { logger.warning("Unable to access NSWindow") return } - + let dynamic = Dynamic(nsWindow) - + switch style { case .standard: applyStandardStyle(dynamic) @@ -76,70 +77,79 @@ class MacCatalystWindowManager: ObservableObject { applyInlineStyle(dynamic, window: window) } } - + private func applyStandardStyle(_ nsWindow: Dynamic) { logger.info("Applying standard window style") - + // Show title bar nsWindow.titlebarAppearsTransparent = false nsWindow.titleVisibility = Dynamic.NSWindowTitleVisibility.visible - nsWindow.styleMask = nsWindow.styleMask.asObject! as! UInt | Dynamic.NSWindowStyleMask.titled.asObject! as! UInt - + guard let currentMask = nsWindow.styleMask.asObject as? UInt, + let titledMask = Dynamic.NSWindowStyleMask.titled.asObject as? UInt else { + logger.error("Failed to get window style masks") + return + } + nsWindow.styleMask = currentMask | titledMask + // Reset traffic light positions resetTrafficLightPositions(nsWindow) - + // Show all buttons for i in 0...2 { let button = nsWindow.standardWindowButton(i) button.isHidden = false } } - + private func applyInlineStyle(_ nsWindow: Dynamic, window: UIWindow) { logger.info("Applying inline window style") - + // Make title bar transparent and hide title nsWindow.titlebarAppearsTransparent = true nsWindow.titleVisibility = Dynamic.NSWindowTitleVisibility.hidden nsWindow.backgroundColor = Dynamic.NSColor.clearColor - + // Keep the titled style mask to preserve traffic lights - let currentMask = nsWindow.styleMask.asObject! as! UInt - nsWindow.styleMask = currentMask | Dynamic.NSWindowStyleMask.titled.asObject! as! UInt - + guard let currentMask = nsWindow.styleMask.asObject as? UInt, + let titledMask = Dynamic.NSWindowStyleMask.titled.asObject as? UInt else { + logger.error("Failed to get window style masks") + return + } + nsWindow.styleMask = currentMask | titledMask + // Reposition traffic lights repositionTrafficLights(nsWindow, window: window) } - + private func repositionTrafficLights(_ nsWindow: Dynamic, window: UIWindow) { // Access the buttons (0=close, 1=minimize, 2=zoom) let closeButton = nsWindow.standardWindowButton(0) let minButton = nsWindow.standardWindowButton(1) let zoomButton = nsWindow.standardWindowButton(2) - + // Get button size let buttonFrame = closeButton.frame let buttonSize = (buttonFrame.size.width.asDouble ?? 14.0) as CGFloat - + // Calculate positions let yPosition = window.frame.height - trafficLightInset.y - buttonSize - + // Set new positions closeButton.setFrameOrigin(Dynamic.NSMakePoint(trafficLightInset.x, yPosition)) minButton.setFrameOrigin(Dynamic.NSMakePoint(trafficLightInset.x + trafficLightSpacing, yPosition)) zoomButton.setFrameOrigin(Dynamic.NSMakePoint(trafficLightInset.x + (trafficLightSpacing * 2), yPosition)) - + // Make sure buttons are visible closeButton.isHidden = false minButton.isHidden = false zoomButton.isHidden = false - + // Update tracking areas for hover effects updateTrafficLightTrackingAreas(nsWindow) - + logger.debug("Repositioned traffic lights to inline positions") } - + private func resetTrafficLightPositions(_ nsWindow: Dynamic) { // Get the superview of the traffic lights let closeButton = nsWindow.standardWindowButton(0) @@ -149,31 +159,36 @@ class MacCatalystWindowManager: ObservableObject { superview.layoutIfNeeded() } } - + private func updateTrafficLightTrackingAreas(_ nsWindow: Dynamic) { // Update tracking areas for each button to ensure hover effects work for i in 0...2 { let button = nsWindow.standardWindowButton(i) - + // Remove old tracking areas if let trackingAreas = button.trackingAreas { for area in trackingAreas.asArray ?? [] { button.removeTrackingArea(area) } } - + // Add new tracking area at the button's current position let trackingRect = button.bounds - let options = Dynamic.NSTrackingAreaOptions.mouseEnteredAndExited.asObject! as! UInt | - Dynamic.NSTrackingAreaOptions.activeAlways.asObject! as! UInt - + guard let mouseEnteredAndExited = Dynamic.NSTrackingAreaOptions.mouseEnteredAndExited.asObject as? UInt, + let activeAlways = Dynamic.NSTrackingAreaOptions.activeAlways.asObject as? UInt + else { + logger.error("Failed to get tracking area options") + return + } + let options = mouseEnteredAndExited | activeAlways + let trackingArea = Dynamic.NSTrackingArea.alloc() .initWithRect(trackingRect, options: options, owner: button, userInfo: nil) - + button.addTrackingArea(trackingArea) } } - + private func setupWindowObservers() { // Clean up existing observers if let observer = windowResizeObserver { @@ -182,58 +197,59 @@ class MacCatalystWindowManager: ObservableObject { if let observer = windowDidBecomeKeyObserver { NotificationCenter.default.removeObserver(observer) } - + // Observe window resize events windowResizeObserver = NotificationCenter.default.addObserver( forName: NSNotification.Name("NSWindowDidResizeNotification"), object: nil, queue: .main ) { [weak self] notification in - guard let self = self, + guard let self, self.windowStyle == .inline, let window = self.window, let notificationWindow = notification.object as? NSObject, let currentNSWindow = window.nsWindow, notificationWindow == currentNSWindow else { return } - + // Reapply inline style on resize DispatchQueue.main.async { self.applyWindowStyle(.inline) } } - + // Observe window becoming key windowDidBecomeKeyObserver = NotificationCenter.default.addObserver( forName: UIWindow.didBecomeKeyNotification, object: window, queue: .main ) { [weak self] _ in - guard let self = self, + guard let self, self.windowStyle == .inline else { return } - + // Reapply inline style when window becomes key DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { self.applyWindowStyle(.inline) } } - + // Also observe the NS notification for tracking area updates NotificationCenter.default.addObserver( forName: NSNotification.Name("NSViewDidUpdateTrackingAreasNotification"), object: nil, queue: .main ) { [weak self] _ in - guard let self = self, + guard let self, self.windowStyle == .inline else { return } - + // Reposition if needed if let window = self.window, - let nsWindow = window.nsWindow { + let nsWindow = window.nsWindow + { self.repositionTrafficLights(Dynamic(nsWindow), window: window) } } } - + deinit { if let observer = windowResizeObserver { NotificationCenter.default.removeObserver(observer) @@ -249,20 +265,21 @@ class MacCatalystWindowManager: ObservableObject { struct MacCatalystWindowStyle: ViewModifier { let style: MacWindowStyle @StateObject private var windowManager = MacCatalystWindowManager.shared - + func body(content: Content) -> some View { content .onAppear { setupWindow() } } - + private func setupWindow() { guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let window = windowScene.windows.first else { + let window = windowScene.windows.first + else { return } - + windowManager.configureWindow(window, style: style) } } diff --git a/ios/VibeTunnel/ViewModels/ServerProfilesViewModel.swift b/ios/VibeTunnel/ViewModels/ServerProfilesViewModel.swift index 6efd97f9..ec5f5508 100644 --- a/ios/VibeTunnel/ViewModels/ServerProfilesViewModel.swift +++ b/ios/VibeTunnel/ViewModels/ServerProfilesViewModel.swift @@ -8,44 +8,44 @@ class ServerProfilesViewModel { var profiles: [ServerProfile] = [] var isLoading = false var errorMessage: String? - + init() { loadProfiles() } - + func loadProfiles() { profiles = ServerProfile.loadAll().sorted { profile1, profile2 in // Sort by last connected (most recent first), then by name if let date1 = profile1.lastConnected, let date2 = profile2.lastConnected { - return date1 > date2 + date1 > date2 } else if profile1.lastConnected != nil { - return true + true } else if profile2.lastConnected != nil { - return false + false } else { - return profile1.name < profile2.name + profile1.name < profile2.name } } } - + func addProfile(_ profile: ServerProfile, password: String? = nil) async throws { ServerProfile.save(profile) - + // Save password to keychain if provided - if let password = password, !password.isEmpty { + if let password, !password.isEmpty { try KeychainService.savePassword(password, for: profile.id) } - + loadProfiles() } - + func updateProfile(_ profile: ServerProfile, password: String? = nil) async throws { var updatedProfile = profile updatedProfile.updatedAt = Date() ServerProfile.save(updatedProfile) - + // Update password if provided - if let password = password { + if let password { if password.isEmpty { // Delete password if empty try KeychainService.deletePassword(for: profile.id) @@ -54,19 +54,19 @@ class ServerProfilesViewModel { try KeychainService.savePassword(password, for: profile.id) } } - + loadProfiles() } - + func deleteProfile(_ profile: ServerProfile) async throws { ServerProfile.delete(profile) - + // Delete password from keychain try KeychainService.deletePassword(for: profile.id) - + loadProfiles() } - + func getPassword(for profile: ServerProfile) -> String? { do { return try KeychainService.getPassword(for: profile.id) @@ -75,29 +75,29 @@ class ServerProfilesViewModel { return nil } } - + func connectToProfile(_ profile: ServerProfile, connectionManager: ConnectionManager) async throws { isLoading = true errorMessage = nil - + defer { isLoading = false } - + // Get password from keychain if needed let password = profile.requiresAuth ? getPassword(for: profile) : nil - + // Create server config guard let config = profile.toServerConfig(password: password) else { throw APIError.invalidURL } - + // Save connection connectionManager.saveConnection(config) - + // Test connection do { _ = try await APIClient.shared.getSessions() connectionManager.isConnected = true - + // Update last connected time ServerProfile.updateLastConnected(for: profile.id) loadProfiles() @@ -106,17 +106,17 @@ class ServerProfilesViewModel { throw error } } - + func testConnection(for profile: ServerProfile) async -> Bool { let password = profile.requiresAuth ? getPassword(for: profile) : nil guard let config = profile.toServerConfig(password: password) else { return false } - + // Save the config temporarily to test let connectionManager = ConnectionManager() connectionManager.saveConnection(config) - + do { _ = try await APIClient.shared.getSessions() return true @@ -132,21 +132,22 @@ extension ServerProfilesViewModel { func createProfileFromURL(_ urlString: String) -> ServerProfile? { // Clean up the URL var cleanURL = urlString.trimmingCharacters(in: .whitespacesAndNewlines) - + // Add http:// if no scheme is present if !cleanURL.contains("://") { cleanURL = "http://\(cleanURL)" } - + // Validate URL guard let url = URL(string: cleanURL), - let _ = url.host else { + let _ = url.host + else { return nil } - + // Generate suggested name let suggestedName = ServerProfile.suggestedName(for: cleanURL) - + return ServerProfile( name: suggestedName, url: cleanURL, diff --git a/ios/VibeTunnel/Views/Connection/ConnectionView.swift b/ios/VibeTunnel/Views/Connection/ConnectionView.swift index 5356d676..4febedd1 100644 --- a/ios/VibeTunnel/Views/Connection/ConnectionView.swift +++ b/ios/VibeTunnel/Views/Connection/ConnectionView.swift @@ -121,7 +121,8 @@ class ConnectionViewModel { func loadLastConnection() { if let config = UserDefaults.standard.data(forKey: "savedServerConfig"), - let serverConfig = try? JSONDecoder().decode(ServerConfig.self, from: config) { + let serverConfig = try? JSONDecoder().decode(ServerConfig.self, from: config) + { self.host = serverConfig.host self.port = String(serverConfig.port) self.name = serverConfig.name ?? "" @@ -162,7 +163,8 @@ class ConnectionViewModel { let (_, response) = try await URLSession.shared.data(for: request) if let httpResponse = response as? HTTPURLResponse, - httpResponse.statusCode == 200 { + httpResponse.statusCode == 200 + { onSuccess(config) } else { errorMessage = "Failed to connect to server" diff --git a/ios/VibeTunnel/Views/Connection/EnhancedConnectionView.swift b/ios/VibeTunnel/Views/Connection/EnhancedConnectionView.swift index b6159078..0a1bfb8a 100644 --- a/ios/VibeTunnel/Views/Connection/EnhancedConnectionView.swift +++ b/ios/VibeTunnel/Views/Connection/EnhancedConnectionView.swift @@ -12,53 +12,53 @@ struct EnhancedConnectionView: View { @State private var showingNewServerForm = false @State private var selectedProfile: ServerProfile? @State private var showingProfileEditor = false - + #if targetEnvironment(macCatalyst) @StateObject private var windowManager = MacCatalystWindowManager.shared #endif - + var body: some View { NavigationStack { ZStack { ScrollView { - VStack(spacing: Theme.Spacing.extraLarge) { - // Logo and Title - headerView - .padding(.top, { - #if targetEnvironment(macCatalyst) - return windowManager.windowStyle == .inline ? 60 : 40 - #else - return 40 - #endif - }()) - - // Quick Connect Section - if !profilesViewModel.profiles.isEmpty && !showingNewServerForm { - quickConnectSection - .opacity(contentOpacity) - .onAppear { - withAnimation(Theme.Animation.smooth.delay(0.3)) { - contentOpacity = 1.0 + VStack(spacing: Theme.Spacing.extraLarge) { + // Logo and Title + headerView + .padding(.top, { + #if targetEnvironment(macCatalyst) + return windowManager.windowStyle == .inline ? 60 : 40 + #else + return 40 + #endif + }()) + + // Quick Connect Section + if !profilesViewModel.profiles.isEmpty && !showingNewServerForm { + quickConnectSection + .opacity(contentOpacity) + .onAppear { + withAnimation(Theme.Animation.smooth.delay(0.3)) { + contentOpacity = 1.0 + } } - } - } - - // New Connection Form - if showingNewServerForm || profilesViewModel.profiles.isEmpty { - newConnectionSection - .opacity(contentOpacity) - .onAppear { - withAnimation(Theme.Animation.smooth.delay(0.3)) { - contentOpacity = 1.0 + } + + // New Connection Form + if showingNewServerForm || profilesViewModel.profiles.isEmpty { + newConnectionSection + .opacity(contentOpacity) + .onAppear { + withAnimation(Theme.Animation.smooth.delay(0.3)) { + contentOpacity = 1.0 + } } - } + } + + Spacer(minLength: 50) } - - Spacer(minLength: 50) + .padding() } - .padding() - } - .scrollBounceBehavior(.basedOnSize) + .scrollBounceBehavior(.basedOnSize) } .toolbar(.hidden, for: .navigationBar) .background(Theme.Colors.terminalBackground.ignoresSafeArea()) @@ -86,9 +86,9 @@ struct EnhancedConnectionView: View { profilesViewModel.loadProfiles() } } - + // MARK: - Header View - + private var headerView: some View { VStack(spacing: Theme.Spacing.large) { ZStack { @@ -98,7 +98,7 @@ struct EnhancedConnectionView: View { .foregroundColor(Theme.Colors.primaryAccent) .blur(radius: 20) .opacity(0.5) - + // Main icon Image(systemName: "terminal.fill") .font(.system(size: 80)) @@ -111,35 +111,35 @@ struct EnhancedConnectionView: View { logoScale = 1.0 } } - + VStack(spacing: Theme.Spacing.small) { Text("VibeTunnel") .font(.system(size: 42, weight: .bold, design: .rounded)) .foregroundColor(Theme.Colors.terminalForeground) - + Text("Terminal Multiplexer") .font(Theme.Typography.terminalSystem(size: 16)) .foregroundColor(Theme.Colors.terminalForeground.opacity(0.7)) .tracking(2) - + // Network status ConnectionStatusView() .padding(.top, Theme.Spacing.small) } } } - + // MARK: - Quick Connect Section - + private var quickConnectSection: some View { VStack(alignment: .leading, spacing: Theme.Spacing.medium) { HStack { Text("Saved Servers") .font(Theme.Typography.terminalSystem(size: 18, weight: .semibold)) .foregroundColor(Theme.Colors.terminalForeground) - + Spacer() - + Button(action: { withAnimation { showingNewServerForm.toggle() @@ -150,7 +150,7 @@ struct EnhancedConnectionView: View { .foregroundColor(Theme.Colors.primaryAccent) } } - + VStack(spacing: Theme.Spacing.small) { ForEach(profilesViewModel.profiles) { profile in ServerProfileCard( @@ -167,9 +167,9 @@ struct EnhancedConnectionView: View { } } } - + // MARK: - New Connection Section - + private var newConnectionSection: some View { VStack(spacing: Theme.Spacing.large) { if !profilesViewModel.profiles.isEmpty { @@ -177,11 +177,11 @@ struct EnhancedConnectionView: View { Text("New Server Connection") .font(Theme.Typography.terminalSystem(size: 18, weight: .semibold)) .foregroundColor(Theme.Colors.terminalForeground) - + Spacer() } } - + ServerConfigForm( host: $viewModel.host, port: $viewModel.port, @@ -191,7 +191,7 @@ struct EnhancedConnectionView: View { errorMessage: viewModel.errorMessage, onConnect: saveAndConnect ) - + if !profilesViewModel.profiles.isEmpty { Button(action: { withAnimation { @@ -206,15 +206,15 @@ struct EnhancedConnectionView: View { } } } - + // MARK: - Actions - + private func connectToProfile(_ profile: ServerProfile) { guard networkMonitor.isConnected else { viewModel.errorMessage = "No internet connection available" return } - + Task { do { try await profilesViewModel.connectToProfile(profile, connectionManager: connectionManager) @@ -223,33 +223,33 @@ struct EnhancedConnectionView: View { } } } - + private func saveAndConnect() { guard networkMonitor.isConnected else { viewModel.errorMessage = "No internet connection available" return } - + // Create profile from form data let urlString = viewModel.port.isEmpty ? viewModel.host : "\(viewModel.host):\(viewModel.port)" guard let profile = profilesViewModel.createProfileFromURL(urlString) else { viewModel.errorMessage = "Invalid server URL" return } - + var updatedProfile = profile updatedProfile.name = viewModel.name.isEmpty ? profile.name : viewModel.name updatedProfile.requiresAuth = !viewModel.password.isEmpty updatedProfile.username = updatedProfile.requiresAuth ? "admin" : nil - + // Save profile and password Task { try await profilesViewModel.addProfile(updatedProfile, password: viewModel.password) - + // Connect connectToProfile(updatedProfile) } - + // Reset form viewModel = ConnectionViewModel() showingNewServerForm = false @@ -263,9 +263,9 @@ struct ServerProfileCard: View { let isLoading: Bool let onConnect: () -> Void let onEdit: () -> Void - + @State private var isPressed = false - + var body: some View { HStack(spacing: Theme.Spacing.medium) { // Icon @@ -275,34 +275,34 @@ struct ServerProfileCard: View { .frame(width: 40, height: 40) .background(Theme.Colors.primaryAccent.opacity(0.1)) .cornerRadius(Theme.CornerRadius.small) - + // Server Info VStack(alignment: .leading, spacing: 2) { Text(profile.name) .font(Theme.Typography.terminalSystem(size: 16, weight: .medium)) .foregroundColor(Theme.Colors.terminalForeground) - + HStack(spacing: 4) { Text(profile.url) .font(Theme.Typography.terminalSystem(size: 12)) .foregroundColor(Theme.Colors.secondaryText) - + if profile.requiresAuth { Image(systemName: "lock.fill") .font(.system(size: 10)) .foregroundColor(Theme.Colors.warningAccent) } } - + if let lastConnected = profile.lastConnected { Text(RelativeDateTimeFormatter().localizedString(for: lastConnected, relativeTo: Date())) .font(Theme.Typography.terminalSystem(size: 11)) .foregroundColor(Theme.Colors.secondaryText.opacity(0.7)) } } - + Spacer() - + // Action Buttons HStack(spacing: Theme.Spacing.small) { Button(action: onEdit) { @@ -311,7 +311,7 @@ struct ServerProfileCard: View { .foregroundColor(Theme.Colors.secondaryText) } .buttonStyle(.plain) - + Button(action: onConnect) { HStack(spacing: 4) { if isLoading { @@ -354,12 +354,12 @@ struct ServerProfileEditView: View { @State var profile: ServerProfile let onSave: (ServerProfile, String?) -> Void let onDelete: () -> Void - + @State private var password: String = "" @State private var showingDeleteConfirmation = false @Environment(\.dismiss) private var dismiss - + var body: some View { NavigationStack { Form { @@ -371,12 +371,12 @@ struct ServerProfileEditView: View { .font(.system(size: 24)) .foregroundColor(Theme.Colors.primaryAccent) } - + TextField("Name", text: $profile.name) TextField("URL", text: $profile.url) - + Toggle("Requires Authentication", isOn: $profile.requiresAuth) - + if profile.requiresAuth { TextField("Username", text: Binding( get: { profile.username ?? "admin" }, @@ -386,7 +386,7 @@ struct ServerProfileEditView: View { .textContentType(.password) } } - + Section { Button(role: .destructive, action: { showingDeleteConfirmation = true @@ -404,7 +404,7 @@ struct ServerProfileEditView: View { dismiss() } } - + ToolbarItem(placement: .navigationBarTrailing) { Button("Save") { onSave(profile, profile.requiresAuth ? password : nil) @@ -418,7 +418,7 @@ struct ServerProfileEditView: View { onDelete() dismiss() } - Button("Cancel", role: .cancel) { } + Button("Cancel", role: .cancel) {} } message: { Text("Are you sure you want to delete \"\(profile.name)\"? This action cannot be undone.") } @@ -426,7 +426,8 @@ struct ServerProfileEditView: View { .task { // Load existing password from keychain if profile.requiresAuth, - let existingPassword = try? KeychainService.getPassword(for: profile.id) { + let existingPassword = try? KeychainService.getPassword(for: profile.id) + { password = existingPassword } } diff --git a/ios/VibeTunnel/Views/Connection/ServerConfigForm.swift b/ios/VibeTunnel/Views/Connection/ServerConfigForm.swift index 66765efb..0a5ef5b9 100644 --- a/ios/VibeTunnel/Views/Connection/ServerConfigForm.swift +++ b/ios/VibeTunnel/Views/Connection/ServerConfigForm.swift @@ -143,13 +143,13 @@ struct ServerConfigForm: View { } }) .foregroundColor(isConnecting || !networkMonitor.isConnected ? Theme.Colors.terminalForeground : Theme - .Colors.primaryAccent + .Colors.primaryAccent ) .padding(.vertical, Theme.Spacing.medium) .background( RoundedRectangle(cornerRadius: Theme.CornerRadius.medium) .fill(isConnecting || !networkMonitor.isConnected ? Theme.Colors.cardBackground : Theme.Colors - .terminalBackground + .terminalBackground ) ) .overlay( @@ -218,7 +218,8 @@ struct ServerConfigForm: View { private func loadRecentServers() { // Load recent servers from UserDefaults if let data = UserDefaults.standard.data(forKey: "recentServers"), - let servers = try? JSONDecoder().decode([ServerConfig].self, from: data) { + let servers = try? JSONDecoder().decode([ServerConfig].self, from: data) + { recentServers = servers } } diff --git a/ios/VibeTunnel/Views/FileBrowser/FilePreviewView.swift b/ios/VibeTunnel/Views/FileBrowser/FilePreviewView.swift index 8902c7c1..14ed5229 100644 --- a/ios/VibeTunnel/Views/FileBrowser/FilePreviewView.swift +++ b/ios/VibeTunnel/Views/FileBrowser/FilePreviewView.swift @@ -88,7 +88,8 @@ struct FilePreviewView: View { case .image: if let content = preview.content, let data = Data(base64Encoded: content), - let uiImage = UIImage(data: data) { + let uiImage = UIImage(data: data) + { Image(uiImage: uiImage) .resizable() .aspectRatio(contentMode: .fit) diff --git a/ios/VibeTunnel/Views/FileBrowserView.swift b/ios/VibeTunnel/Views/FileBrowserView.swift index 503f0ce8..ab6c4abf 100644 --- a/ios/VibeTunnel/Views/FileBrowserView.swift +++ b/ios/VibeTunnel/Views/FileBrowserView.swift @@ -109,14 +109,14 @@ struct FileBrowserView: View { .font(.custom("SF Mono", size: 12)) } .foregroundColor(viewModel.gitFilter == .changed ? Theme.Colors.successAccent : Theme.Colors - .terminalGray + .terminalGray ) .padding(.horizontal, 12) .padding(.vertical, 6) .background( RoundedRectangle(cornerRadius: 6) .fill(viewModel.gitFilter == .changed ? Theme.Colors.successAccent.opacity(0.2) : Theme.Colors - .terminalGray.opacity(0.1) + .terminalGray.opacity(0.1) ) ) } @@ -140,7 +140,7 @@ struct FileBrowserView: View { .background( RoundedRectangle(cornerRadius: 6) .fill(viewModel.showHidden ? Theme.Colors.terminalAccent.opacity(0.2) : Theme.Colors - .terminalGray.opacity(0.1) + .terminalGray.opacity(0.1) ) ) } @@ -566,7 +566,7 @@ struct FileBrowserRow: View { Text(name) .font(.custom("SF Mono", size: 14)) .foregroundColor(isParent ? Theme.Colors - .terminalAccent : (isDirectory ? Theme.Colors.terminalWhite : Theme.Colors.terminalGray) + .terminalAccent : (isDirectory ? Theme.Colors.terminalWhite : Theme.Colors.terminalGray) ) .lineLimit(1) .truncationMode(.middle) diff --git a/ios/VibeTunnel/Views/Sessions/SessionCardView.swift b/ios/VibeTunnel/Views/Sessions/SessionCardView.swift index 7330c409..7e264579 100644 --- a/ios/VibeTunnel/Views/Sessions/SessionCardView.swift +++ b/ios/VibeTunnel/Views/Sessions/SessionCardView.swift @@ -18,14 +18,15 @@ struct SessionCardView: View { @State private var scale: CGFloat = 1.0 @State private var rotation: Double = 0 @State private var brightness: Double = 1.0 - + @Environment(\.livePreviewSubscription) private var livePreview private var displayWorkingDir: String { // Convert absolute paths back to ~ notation for display let homePrefix = "/Users/" if session.workingDir.hasPrefix(homePrefix), - let userEndIndex = session.workingDir[homePrefix.endIndex...].firstIndex(of: "/") { + let userEndIndex = session.workingDir[homePrefix.endIndex...].firstIndex(of: "/") + { let restOfPath = String(session.workingDir[userEndIndex...]) return "~\(restOfPath)" } @@ -61,7 +62,7 @@ struct SessionCardView: View { Image(systemName: session.isRunning ? "xmark.circle" : "trash.circle") .font(.system(size: 18)) .foregroundColor(session.isRunning ? Theme.Colors.errorAccent : Theme.Colors - .terminalForeground.opacity(0.6) + .terminalForeground.opacity(0.6) ) } }) @@ -106,15 +107,15 @@ struct SessionCardView: View { HStack(spacing: 4) { Circle() .fill(session.isRunning ? Theme.Colors.successAccent : Theme.Colors.terminalForeground - .opacity(0.3) + .opacity(0.3) ) .frame(width: 6, height: 6) Text(session.isRunning ? "running" : "exited") .font(Theme.Typography.terminalSystem(size: 10)) .foregroundColor(session.isRunning ? Theme.Colors.successAccent : Theme.Colors - .terminalForeground.opacity(0.5) + .terminalForeground.opacity(0.5) ) - + // Live preview indicator if session.isRunning && livePreview?.latestSnapshot != nil { HStack(spacing: 2) { @@ -256,9 +257,9 @@ struct SessionCardView: View { opacity = 1.0 } } - + // MARK: - View Components - + @ViewBuilder private var commandInfoView: some View { VStack(alignment: .leading, spacing: 4) { @@ -294,7 +295,7 @@ struct SessionCardView: View { } .padding(Theme.Spacing.small) } - + @ViewBuilder private func staticSnapshotView(_ snapshot: TerminalSnapshot) -> some View { ScrollView(.vertical, showsIndicators: false) { @@ -327,7 +328,7 @@ struct SessionCardView: View { } .padding(Theme.Spacing.small) } - + @ViewBuilder private func exitedSessionView(_ snapshot: TerminalSnapshot) -> some View { ScrollView(.vertical, showsIndicators: false) { diff --git a/ios/VibeTunnel/Views/Sessions/SessionCreateView.swift b/ios/VibeTunnel/Views/Sessions/SessionCreateView.swift index 1c148be1..16b0c83d 100644 --- a/ios/VibeTunnel/Views/Sessions/SessionCreateView.swift +++ b/ios/VibeTunnel/Views/Sessions/SessionCreateView.swift @@ -117,9 +117,9 @@ struct SessionCreateView: View { if presentedError != nil { ErrorBanner( message: presentedError?.error.localizedDescription ?? "An error occurred" - ) { - presentedError = nil - } + ) { + presentedError = nil + } .overlay( RoundedRectangle(cornerRadius: Theme.CornerRadius.small) .stroke(Theme.Colors.errorAccent.opacity(0.3), lineWidth: 1) @@ -156,14 +156,14 @@ struct SessionCreateView: View { .font(Theme.Typography.terminalSystem(size: 13)) } .foregroundColor(workingDirectory == dir ? Theme.Colors - .terminalBackground : Theme.Colors.terminalForeground + .terminalBackground : Theme.Colors.terminalForeground ) .padding(.horizontal, 12) .padding(.vertical, 8) .background( RoundedRectangle(cornerRadius: Theme.CornerRadius.small) .fill(workingDirectory == dir ? Theme.Colors - .primaryAccent : Theme.Colors.cardBorder.opacity(0.1) + .primaryAccent : Theme.Colors.cardBorder.opacity(0.1) ) ) .overlay( @@ -209,16 +209,16 @@ struct SessionCreateView: View { Spacer() } .foregroundColor(command == item.command ? Theme.Colors - .terminalBackground : Theme.Colors - .terminalForeground + .terminalBackground : Theme.Colors + .terminalForeground ) .padding(.horizontal, Theme.Spacing.medium) .padding(.vertical, 14) .background( RoundedRectangle(cornerRadius: Theme.CornerRadius.medium) .fill(command == item.command ? Theme.Colors.primaryAccent : Theme - .Colors - .cardBackground + .Colors + .cardBackground ) ) .overlay( @@ -283,7 +283,7 @@ struct SessionCreateView: View { Text("Create") .font(.system(size: 17, weight: .semibold)) .foregroundColor(command.isEmpty ? Theme.Colors.primaryAccent.opacity(0.5) : Theme - .Colors.primaryAccent + .Colors.primaryAccent ) } }) diff --git a/ios/VibeTunnel/Views/Sessions/SessionListView.swift b/ios/VibeTunnel/Views/Sessions/SessionListView.swift index 5697386e..bb5f4f15 100644 --- a/ios/VibeTunnel/Views/Sessions/SessionListView.swift +++ b/ios/VibeTunnel/Views/Sessions/SessionListView.swift @@ -187,7 +187,7 @@ struct SessionListView: View { .searchable(text: $searchText, prompt: "Search sessions") .task { await viewModel.loadSessions() - + // Refresh every 3 seconds while !Task.isCancelled { try? await Task.sleep(nanoseconds: 3_000_000_000) // 3 seconds @@ -200,7 +200,8 @@ struct SessionListView: View { .onChange(of: navigationManager.shouldNavigateToSession) { _, shouldNavigate in if shouldNavigate, let sessionId = navigationManager.selectedSessionId, - let session = viewModel.sessions.first(where: { $0.id == sessionId }) { + let session = viewModel.sessions.first(where: { $0.id == sessionId }) + { selectedSession = session navigationManager.clearNavigation() } diff --git a/ios/VibeTunnel/Views/Settings/SettingsView.swift b/ios/VibeTunnel/Views/Settings/SettingsView.swift index 338f4f70..2306d622 100644 --- a/ios/VibeTunnel/Views/Settings/SettingsView.swift +++ b/ios/VibeTunnel/Views/Settings/SettingsView.swift @@ -40,7 +40,7 @@ struct SettingsView: View { .frame(maxWidth: .infinity) .padding(.vertical, Theme.Spacing.medium) .foregroundColor(selectedTab == tab ? Theme.Colors.primaryAccent : Theme.Colors - .terminalForeground.opacity(0.5) + .terminalForeground.opacity(0.5) ) .background( selectedTab == tab ? Theme.Colors.primaryAccent.opacity(0.1) : Color.clear @@ -172,7 +172,7 @@ struct GeneralSettingsView: View { .padding() .background(Theme.Colors.cardBackground) .cornerRadius(Theme.CornerRadius.card) - + // Live Previews Toggle(isOn: $enableLivePreviews) { HStack { @@ -207,12 +207,12 @@ struct AdvancedSettingsView: View { @AppStorage("debugModeEnabled") private var debugModeEnabled = false @State private var showingSystemLogs = false - + #if targetEnvironment(macCatalyst) @AppStorage("macWindowStyle") private var macWindowStyleRaw = "standard" @StateObject private var windowManager = MacCatalystWindowManager.shared - + private var macWindowStyle: MacWindowStyle { macWindowStyleRaw == "inline" ? .inline : .standard } @@ -273,14 +273,14 @@ struct AdvancedSettingsView: View { Text("Mac Catalyst") .font(.headline) .foregroundColor(Theme.Colors.terminalForeground) - + VStack(spacing: Theme.Spacing.medium) { // Window Style Picker VStack(alignment: .leading, spacing: Theme.Spacing.small) { Text("Window Style") .font(Theme.Typography.terminalSystem(size: 14)) .foregroundColor(Theme.Colors.terminalForeground.opacity(0.7)) - + Picker("Window Style", selection: $macWindowStyleRaw) { Label("Standard", systemImage: "macwindow") .tag("standard") @@ -292,12 +292,13 @@ struct AdvancedSettingsView: View { let style: MacWindowStyle = newValue == "inline" ? .inline : .standard windowManager.setWindowStyle(style) } - + Text(macWindowStyle == .inline ? - "Traffic light buttons appear inline with content" : - "Standard macOS title bar with traffic lights") - .font(Theme.Typography.terminalSystem(size: 12)) - .foregroundColor(Theme.Colors.terminalForeground.opacity(0.6)) + "Traffic light buttons appear inline with content" : + "Standard macOS title bar with traffic lights" + ) + .font(Theme.Typography.terminalSystem(size: 12)) + .foregroundColor(Theme.Colors.terminalForeground.opacity(0.6)) } .padding() .background(Theme.Colors.cardBackground) @@ -350,11 +351,11 @@ struct AboutSettingsView: View { private var appVersion: String { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" } - + private var buildNumber: String { Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown" } - + var body: some View { VStack(spacing: Theme.Spacing.xlarge) { // App icon and info @@ -364,19 +365,19 @@ struct AboutSettingsView: View { .frame(width: 100, height: 100) .cornerRadius(22) .shadow(color: Theme.Colors.primaryAccent.opacity(0.3), radius: 10, y: 5) - + VStack(spacing: Theme.Spacing.small) { Text("VibeTunnel") .font(.largeTitle) .fontWeight(.bold) - + Text("Version \(appVersion) (\(buildNumber))") .font(Theme.Typography.terminalSystem(size: 14)) .foregroundColor(Theme.Colors.secondaryText) } } .padding(.top, Theme.Spacing.large) - + // Links section VStack(spacing: Theme.Spacing.medium) { LinkRow( @@ -385,21 +386,21 @@ struct AboutSettingsView: View { subtitle: "vibetunnel.sh", url: URL(string: "https://vibetunnel.sh") ) - + LinkRow( icon: "doc.text", title: "Documentation", subtitle: "Learn how to use VibeTunnel", url: URL(string: "https://docs.vibetunnel.sh") ) - + LinkRow( icon: "exclamationmark.bubble", title: "Report an Issue", subtitle: "Help us improve", url: URL(string: "https://github.com/vibetunnel/vibetunnel/issues") ) - + LinkRow( icon: "heart", title: "Rate on App Store", @@ -407,20 +408,20 @@ struct AboutSettingsView: View { url: URL(string: "https://apps.apple.com/app/vibetunnel") ) } - + // Credits VStack(spacing: Theme.Spacing.small) { Text("Made with ❤️ by the VibeTunnel team") .font(Theme.Typography.terminalSystem(size: 12)) .foregroundColor(Theme.Colors.secondaryText) .multilineTextAlignment(.center) - + Text("© 2024 VibeTunnel. All rights reserved.") .font(Theme.Typography.terminalSystem(size: 11)) .foregroundColor(Theme.Colors.secondaryText.opacity(0.7)) } .padding(.top, Theme.Spacing.large) - + Spacer() } } @@ -431,10 +432,10 @@ struct LinkRow: View { let title: String let subtitle: String let url: URL? - + var body: some View { Button(action: { - if let url = url { + if let url { UIApplication.shared.open(url) } }) { @@ -443,19 +444,19 @@ struct LinkRow: View { .font(.system(size: 20)) .foregroundColor(Theme.Colors.primaryAccent) .frame(width: 30) - + VStack(alignment: .leading, spacing: 2) { Text(title) .font(Theme.Typography.terminalSystem(size: 14)) .foregroundColor(Theme.Colors.terminalForeground) - + Text(subtitle) .font(Theme.Typography.terminalSystem(size: 12)) .foregroundColor(Theme.Colors.secondaryText) } - + Spacer() - + Image(systemName: "arrow.up.right.square") .font(.system(size: 16)) .foregroundColor(Theme.Colors.secondaryText.opacity(0.5)) diff --git a/ios/VibeTunnel/Views/SystemLogsView.swift b/ios/VibeTunnel/Views/SystemLogsView.swift index a9dbd8a7..c43243b3 100644 --- a/ios/VibeTunnel/Views/SystemLogsView.swift +++ b/ios/VibeTunnel/Views/SystemLogsView.swift @@ -296,7 +296,8 @@ struct SystemLogsView: View { // Present it if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let window = windowScene.windows.first, - let rootVC = window.rootViewController { + let rootVC = window.rootViewController + { rootVC.present(activityVC, animated: true) } } diff --git a/ios/VibeTunnel/Views/Terminal/AdvancedKeyboardView.swift b/ios/VibeTunnel/Views/Terminal/AdvancedKeyboardView.swift index 623169e7..464cf4e6 100644 --- a/ios/VibeTunnel/Views/Terminal/AdvancedKeyboardView.swift +++ b/ios/VibeTunnel/Views/Terminal/AdvancedKeyboardView.swift @@ -233,7 +233,8 @@ struct CtrlKeyButton: View { Button(action: { // Calculate control character (Ctrl+A = 1, Ctrl+B = 2, etc.) if let scalar = char.unicodeScalars.first, - let ctrlScalar = UnicodeScalar(scalar.value - 64) { + let ctrlScalar = UnicodeScalar(scalar.value - 64) + { let ctrlChar = Character(ctrlScalar) onPress(String(ctrlChar)) } diff --git a/ios/VibeTunnel/Views/Terminal/CtrlKeyGrid.swift b/ios/VibeTunnel/Views/Terminal/CtrlKeyGrid.swift index 4897d80f..90d8782e 100644 --- a/ios/VibeTunnel/Views/Terminal/CtrlKeyGrid.swift +++ b/ios/VibeTunnel/Views/Terminal/CtrlKeyGrid.swift @@ -6,8 +6,8 @@ private let logger = Logger(category: "CtrlKeyGrid") struct CtrlKeyGrid: View { @Binding var isPresented: Bool let onKeyPress: (String) -> Void - - // Common Ctrl combinations organized by category + + /// Common Ctrl combinations organized by category let navigationKeys = [ ("A", "Beginning of line"), ("E", "End of line"), @@ -16,7 +16,7 @@ struct CtrlKeyGrid: View { ("P", "Previous command"), ("N", "Next command") ] - + let editingKeys = [ ("D", "Delete character"), ("H", "Backspace"), @@ -25,7 +25,7 @@ struct CtrlKeyGrid: View { ("K", "Delete to end"), ("Y", "Paste") ] - + let processKeys = [ ("C", "Interrupt (SIGINT)"), ("Z", "Suspend (SIGTSTP)"), @@ -34,7 +34,7 @@ struct CtrlKeyGrid: View { ("Q", "Resume output"), ("L", "Clear screen") ] - + let searchKeys = [ ("R", "Search history"), ("T", "Transpose chars"), @@ -43,9 +43,9 @@ struct CtrlKeyGrid: View { ("G", "Cancel command"), ("O", "Execute + new line") ] - + @State private var selectedCategory = 0 - + var body: some View { NavigationStack { VStack(spacing: 0) { @@ -58,7 +58,7 @@ struct CtrlKeyGrid: View { } .pickerStyle(SegmentedPickerStyle()) .padding() - + // Key grid ScrollView { LazyVGrid(columns: [ @@ -70,18 +70,18 @@ struct CtrlKeyGrid: View { CtrlGridKeyButton( key: key, description: description - ) { sendCtrlKey(key) } + ) { sendCtrlKey(key) } } } .padding() } - + // Quick reference VStack(alignment: .leading, spacing: Theme.Spacing.small) { Text("Tip: Long press any key to see its function") .font(Theme.Typography.terminalSystem(size: 12)) .foregroundColor(Theme.Colors.secondaryText) - + Text("These shortcuts work in most terminal applications") .font(Theme.Typography.terminalSystem(size: 11)) .foregroundColor(Theme.Colors.secondaryText.opacity(0.7)) @@ -103,17 +103,17 @@ struct CtrlKeyGrid: View { } .preferredColorScheme(.dark) } - + private var currentKeys: [(String, String)] { switch selectedCategory { - case 0: return navigationKeys - case 1: return editingKeys - case 2: return processKeys - case 3: return searchKeys - default: return navigationKeys + case 0: navigationKeys + case 1: editingKeys + case 2: processKeys + case 3: searchKeys + default: navigationKeys } } - + private func sendCtrlKey(_ key: String) { // Convert letter to control character if let charCode = key.first?.asciiValue { @@ -123,7 +123,7 @@ struct CtrlKeyGrid: View { Task { @MainActor in HapticFeedback.impact(.medium) } - + // Auto-dismiss for common keys if ["C", "D", "Z"].contains(key) { isPresented = false @@ -138,17 +138,17 @@ struct CtrlGridKeyButton: View { let key: String let description: String let onPress: () -> Void - + @State private var isPressed = false @State private var showingTooltip = false - + var body: some View { Button(action: onPress) { VStack(spacing: 4) { Text("^" + key) .font(Theme.Typography.terminalSystem(size: 20, weight: .bold)) .foregroundColor(isPressed ? .white : Theme.Colors.primaryAccent) - + Text("Ctrl+" + key) .font(Theme.Typography.terminalSystem(size: 10)) .foregroundColor(isPressed ? .white.opacity(0.8) : Theme.Colors.secondaryText) @@ -184,7 +184,7 @@ struct CtrlGridKeyButton: View { Task { @MainActor in HapticFeedback.impact(.light) } - + // Hide tooltip after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { showingTooltip = false @@ -195,7 +195,7 @@ struct CtrlGridKeyButton: View { Text("Ctrl+" + key) .font(Theme.Typography.terminalSystem(size: 14, weight: .bold)) .foregroundColor(Theme.Colors.primaryAccent) - + Text(description) .font(Theme.Typography.terminalSystem(size: 12)) .foregroundColor(Theme.Colors.terminalForeground) diff --git a/ios/VibeTunnel/Views/Terminal/FontSizeSheet.swift b/ios/VibeTunnel/Views/Terminal/FontSizeSheet.swift index ff7b96d3..913aa80c 100644 --- a/ios/VibeTunnel/Views/Terminal/FontSizeSheet.swift +++ b/ios/VibeTunnel/Views/Terminal/FontSizeSheet.swift @@ -81,14 +81,14 @@ struct FontSizeSheet: View { Text("\(Int(size))") .font(.system(size: 14, weight: .medium)) .foregroundColor(fontSize == size ? Theme.Colors.terminalBackground : Theme.Colors - .terminalForeground + .terminalForeground ) .frame(maxWidth: .infinity) .padding(.vertical, Theme.Spacing.small) .background( RoundedRectangle(cornerRadius: Theme.CornerRadius.small) .fill(fontSize == size ? Theme.Colors.primaryAccent : Theme.Colors - .cardBorder.opacity(0.3) + .cardBorder.opacity(0.3) ) ) .overlay( diff --git a/ios/VibeTunnel/Views/Terminal/FullscreenTextInput.swift b/ios/VibeTunnel/Views/Terminal/FullscreenTextInput.swift index 394cdabf..b00882e2 100644 --- a/ios/VibeTunnel/Views/Terminal/FullscreenTextInput.swift +++ b/ios/VibeTunnel/Views/Terminal/FullscreenTextInput.swift @@ -9,7 +9,7 @@ struct FullscreenTextInput: View { @State private var text: String = "" @FocusState private var isFocused: Bool @State private var showingOptions = false - + var body: some View { NavigationStack { VStack(spacing: 0) { @@ -28,7 +28,7 @@ struct FullscreenTextInput: View { .background(Theme.Colors.cardBackground) .cornerRadius(Theme.CornerRadius.medium) .padding() - + // Quick actions HStack(spacing: Theme.Spacing.medium) { // Template commands @@ -36,25 +36,25 @@ struct FullscreenTextInput: View { Button(action: { insertTemplate("ls -la") }, label: { Label("List Files", systemImage: "folder") }) - + Button(action: { insertTemplate("cd ") }, label: { Label("Change Directory", systemImage: "arrow.right.square") }) - + Button(action: { insertTemplate("git status") }, label: { Label("Git Status", systemImage: "arrow.triangle.branch") }) - + Button(action: { insertTemplate("sudo ") }, label: { Label("Sudo Command", systemImage: "lock") }) - + Divider() - + Button(action: { insertTemplate("ssh ") }, label: { Label("SSH Connect", systemImage: "network") }) - + Button(action: { insertTemplate("docker ps") }, label: { Label("Docker List", systemImage: "shippingbox") }) @@ -63,14 +63,14 @@ struct FullscreenTextInput: View { .font(Theme.Typography.terminalSystem(size: 14)) } .buttonStyle(.bordered) - + Spacer() - + // Character count Text("\(text.count) characters") .font(Theme.Typography.terminalSystem(size: 12)) .foregroundColor(Theme.Colors.secondaryText) - + // Clear button if !text.isEmpty { Button(action: { @@ -84,10 +84,10 @@ struct FullscreenTextInput: View { } .padding(.horizontal) .padding(.bottom, Theme.Spacing.small) - + Divider() .background(Theme.Colors.cardBorder) - + // Input options VStack(spacing: Theme.Spacing.small) { // Common special characters @@ -110,7 +110,7 @@ struct FullscreenTextInput: View { } .padding(.horizontal) } - + // Submit options HStack(spacing: Theme.Spacing.medium) { // Execute immediately @@ -128,7 +128,7 @@ struct FullscreenTextInput: View { .background(Theme.Colors.primaryAccent) .cornerRadius(Theme.CornerRadius.medium) }) - + // Insert without executing Button(action: { insertAndClose() @@ -163,7 +163,7 @@ struct FullscreenTextInput: View { } .foregroundColor(Theme.Colors.primaryAccent) } - + ToolbarItem(placement: .navigationBarTrailing) { Button(action: { showingOptions.toggle() }, label: { Image(systemName: "ellipsis.circle") @@ -177,17 +177,17 @@ struct FullscreenTextInput: View { isFocused = true } } - + private func insertText(_ text: String) { self.text.append(text) HapticFeedback.impact(.light) } - + private func insertTemplate(_ template: String) { self.text = template HapticFeedback.impact(.light) } - + private func submitAndClose() { if !text.isEmpty { onSubmit(text + "\n") // Add newline to execute @@ -195,7 +195,7 @@ struct FullscreenTextInput: View { } isPresented = false } - + private func insertAndClose() { if !text.isEmpty { onSubmit(text) // Don't add newline, just insert diff --git a/ios/VibeTunnel/Views/Terminal/QuickFontSizeButtons.swift b/ios/VibeTunnel/Views/Terminal/QuickFontSizeButtons.swift index 1a722081..c9fbd052 100644 --- a/ios/VibeTunnel/Views/Terminal/QuickFontSizeButtons.swift +++ b/ios/VibeTunnel/Views/Terminal/QuickFontSizeButtons.swift @@ -5,14 +5,16 @@ struct QuickFontSizeButtons: View { @Binding var fontSize: CGFloat let minSize: CGFloat = 8 let maxSize: CGFloat = 32 - + var body: some View { HStack(spacing: 0) { // Decrease button Button(action: decreaseFontSize) { Image(systemName: "minus") .font(.system(size: 14, weight: .medium)) - .foregroundColor(fontSize > minSize ? Theme.Colors.primaryAccent : Theme.Colors.secondaryText.opacity(0.5)) + .foregroundColor(fontSize > minSize ? Theme.Colors.primaryAccent : Theme.Colors.secondaryText + .opacity(0.5) + ) .frame(width: 30, height: 30) .background(Theme.Colors.cardBackground) .overlay( @@ -21,7 +23,7 @@ struct QuickFontSizeButtons: View { ) } .disabled(fontSize <= minSize) - + // Current size display Text("\(Int(fontSize))") .font(Theme.Typography.terminalSystem(size: 12, weight: .medium)) @@ -36,12 +38,14 @@ struct QuickFontSizeButtons: View { .background(Theme.Colors.cardBorder) } ) - + // Increase button Button(action: increaseFontSize) { Image(systemName: "plus") .font(.system(size: 14, weight: .medium)) - .foregroundColor(fontSize < maxSize ? Theme.Colors.primaryAccent : Theme.Colors.secondaryText.opacity(0.5)) + .foregroundColor(fontSize < maxSize ? Theme.Colors.primaryAccent : Theme.Colors.secondaryText + .opacity(0.5) + ) .frame(width: 30, height: 30) .background(Theme.Colors.cardBackground) .overlay( @@ -55,12 +59,12 @@ struct QuickFontSizeButtons: View { .cornerRadius(Theme.CornerRadius.small) .shadow(color: Theme.CardShadow.color, radius: 2, y: 1) } - + private func decreaseFontSize() { fontSize = max(minSize, fontSize - 1) HapticFeedback.impact(.light) } - + private func increaseFontSize() { fontSize = min(maxSize, fontSize + 1) HapticFeedback.impact(.light) diff --git a/ios/VibeTunnel/Views/Terminal/TerminalBufferPreview.swift b/ios/VibeTunnel/Views/Terminal/TerminalBufferPreview.swift index 828b9fcf..fe226624 100644 --- a/ios/VibeTunnel/Views/Terminal/TerminalBufferPreview.swift +++ b/ios/VibeTunnel/Views/Terminal/TerminalBufferPreview.swift @@ -7,12 +7,12 @@ import SwiftUI struct TerminalBufferPreview: View { let snapshot: BufferSnapshot let fontSize: CGFloat - + init(snapshot: BufferSnapshot, fontSize: CGFloat = 10) { self.snapshot = snapshot self.fontSize = fontSize } - + var body: some View { GeometryReader { _ in ScrollViewReader { scrollProxy in @@ -52,7 +52,7 @@ struct TerminalBufferPreview: View { .background(Theme.Colors.terminalBackground) .cornerRadius(Theme.CornerRadius.small) } - + @ViewBuilder private func cellView(for cell: BufferCell) -> some View { Text(cell.char.isEmpty ? " " : cell.char) @@ -61,14 +61,14 @@ struct TerminalBufferPreview: View { .background(backgroundColor(for: cell)) .frame(width: fontSize * 0.6 * CGFloat(max(1, cell.width))) } - + private func foregroundColor(for cell: BufferCell) -> Color { guard let fg = cell.fg else { return Theme.Colors.terminalForeground } - + // Check if RGB color (has alpha channel flag) - if (fg & 0xFF000000) != 0 { + if (fg & 0xFF00_0000) != 0 { // RGB color let red = Double((fg >> 16) & 0xFF) / 255.0 let green = Double((fg >> 8) & 0xFF) / 255.0 @@ -79,14 +79,14 @@ struct TerminalBufferPreview: View { return paletteColor(fg) } } - + private func backgroundColor(for cell: BufferCell) -> Color { guard let bg = cell.bg else { return .clear } - + // Check if RGB color (has alpha channel flag) - if (bg & 0xFF000000) != 0 { + if (bg & 0xFF00_0000) != 0 { // RGB color let red = Double((bg >> 16) & 0xFF) / 255.0 let green = Double((bg >> 8) & 0xFF) / 255.0 @@ -97,7 +97,7 @@ struct TerminalBufferPreview: View { return paletteColor(bg) } } - + private func paletteColor(_ index: Int) -> Color { // ANSI 256-color palette switch index { @@ -133,17 +133,17 @@ struct TerminalBufferPreview: View { struct CompactTerminalPreview: View { let snapshot: BufferSnapshot let maxLines: Int - + init(snapshot: BufferSnapshot, maxLines: Int = 6) { self.snapshot = snapshot self.maxLines = maxLines } - + var body: some View { VStack(alignment: .leading, spacing: 1) { // Get the last non-empty lines let visibleLines = getVisibleLines() - + ForEach(Array(visibleLines.enumerated()), id: \.offset) { _, line in Text(line) .font(Theme.Typography.terminalSystem(size: 10)) @@ -151,25 +151,25 @@ struct CompactTerminalPreview: View { .lineLimit(1) .truncationMode(.tail) } - + Spacer(minLength: 0) } .frame(maxWidth: .infinity, alignment: .leading) .padding(8) } - + private func getVisibleLines() -> [String] { var lines: [String] = [] - + // Start from the bottom and work up to find non-empty lines for row in (0..= maxLines { @@ -177,12 +177,12 @@ struct CompactTerminalPreview: View { } } } - + // If we have fewer lines than maxLines, add empty lines at the top while lines.count < min(maxLines, snapshot.rows) && lines.count < maxLines { lines.insert("", at: 0) } - + return lines } } diff --git a/ios/VibeTunnel/Views/Terminal/TerminalHostingView.swift b/ios/VibeTunnel/Views/Terminal/TerminalHostingView.swift index 48a744b0..7c93b438 100644 --- a/ios/VibeTunnel/Views/Terminal/TerminalHostingView.swift +++ b/ios/VibeTunnel/Views/Terminal/TerminalHostingView.swift @@ -383,7 +383,8 @@ struct TerminalHostingView: UIViewRepresentable { from oldSnapshot: BufferSnapshot, to newSnapshot: BufferSnapshot ) - -> String { + -> String + { var output = "" var currentFg: Int? var currentBg: Int? diff --git a/ios/VibeTunnel/Views/Terminal/TerminalThemeSheet.swift b/ios/VibeTunnel/Views/Terminal/TerminalThemeSheet.swift index bcf04aa2..2751d071 100644 --- a/ios/VibeTunnel/Views/Terminal/TerminalThemeSheet.swift +++ b/ios/VibeTunnel/Views/Terminal/TerminalThemeSheet.swift @@ -74,8 +74,8 @@ struct TerminalThemeSheet: View { .background( RoundedRectangle(cornerRadius: Theme.CornerRadius.medium) .fill(selectedTheme.id == theme.id - ? Theme.Colors.primaryAccent.opacity(0.1) - : Theme.Colors.cardBorder.opacity(0.1) + ? Theme.Colors.primaryAccent.opacity(0.1) + : Theme.Colors.cardBorder.opacity(0.1) ) ) .overlay( diff --git a/ios/VibeTunnel/Views/Terminal/TerminalView.swift b/ios/VibeTunnel/Views/Terminal/TerminalView.swift index 9c9f56c4..2a3c9f36 100644 --- a/ios/VibeTunnel/Views/Terminal/TerminalView.swift +++ b/ios/VibeTunnel/Views/Terminal/TerminalView.swift @@ -112,7 +112,9 @@ struct TerminalView: View { } ) .task { - for await notification in NotificationCenter.default.notifications(named: UIResponder.keyboardWillShowNotification) { + for await notification in NotificationCenter.default + .notifications(named: UIResponder.keyboardWillShowNotification) + { if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect { withAnimation(Theme.Animation.standard) { keyboardHeight = keyboardFrame.height @@ -290,11 +292,11 @@ struct TerminalView: View { Button(action: { viewModel.clearTerminal() }, label: { Label("Clear", systemImage: "clear") }) - + Button(action: { showingFullscreenInput = true }, label: { Label("Compose Command", systemImage: "text.viewfinder") }) - + Button(action: { showingCtrlKeyGrid = true }, label: { Label("Ctrl Shortcuts", systemImage: "command.square") }) @@ -524,10 +526,10 @@ struct TerminalView: View { .overlay( ScrollToBottomButton( isVisible: showScrollToBottom - ) { - viewModel.scrollToBottom() - showScrollToBottom = false - } + ) { + viewModel.scrollToBottom() + showScrollToBottom = false + } .padding(.bottom, Theme.Spacing.large) .padding(.leading, Theme.Spacing.large), alignment: .bottomLeading @@ -698,7 +700,7 @@ class TerminalViewModel { logger.info("Terminal initialized: \(width)x\(height)") terminalCols = width terminalRows = height - // The terminal will be resized when created + // The terminal will be resized when created case .output(_, let data): // Feed output data directly to the terminal @@ -723,7 +725,8 @@ class TerminalViewModel { let parts = dimensions.split(separator: "x") if parts.count == 2, let cols = Int(parts[0]), - let rows = Int(parts[1]) { + let rows = Int(parts[1]) + { // Update terminal dimensions terminalCols = cols terminalRows = rows diff --git a/ios/VibeTunnel/Views/Terminal/TerminalWidthSheet.swift b/ios/VibeTunnel/Views/Terminal/TerminalWidthSheet.swift index 441b9326..de492ff2 100644 --- a/ios/VibeTunnel/Views/Terminal/TerminalWidthSheet.swift +++ b/ios/VibeTunnel/Views/Terminal/TerminalWidthSheet.swift @@ -142,8 +142,8 @@ struct TerminalWidthSheet: View { .background( RoundedRectangle(cornerRadius: Theme.CornerRadius.medium) .fill(selectedWidth == preset.columns - ? Theme.Colors.primaryAccent.opacity(0.1) - : Theme.Colors.cardBorder.opacity(0.1) + ? Theme.Colors.primaryAccent.opacity(0.1) + : Theme.Colors.cardBorder.opacity(0.1) ) ) .overlay( diff --git a/ios/VibeTunnel/Views/Terminal/WidthSelectorPopover.swift b/ios/VibeTunnel/Views/Terminal/WidthSelectorPopover.swift index 96770091..6e238fb2 100644 --- a/ios/VibeTunnel/Views/Terminal/WidthSelectorPopover.swift +++ b/ios/VibeTunnel/Views/Terminal/WidthSelectorPopover.swift @@ -44,8 +44,8 @@ struct WidthSelectorPopover: View { let customWidths = TerminalWidthManager.shared.customWidths if !customWidths.isEmpty { Section(header: Text("Recent Custom Widths") - .font(.caption) - .foregroundColor(Theme.Colors.terminalForeground.opacity(0.7)) + .font(.caption) + .foregroundColor(Theme.Colors.terminalForeground.opacity(0.7)) ) { ForEach(customWidths, id: \.self) { width in WidthPresetRow( diff --git a/ios/VibeTunnel/Views/Terminal/XtermWebView.swift b/ios/VibeTunnel/Views/Terminal/XtermWebView.swift index bbae2c38..22c6576f 100644 --- a/ios/VibeTunnel/Views/Terminal/XtermWebView.swift +++ b/ios/VibeTunnel/Views/Terminal/XtermWebView.swift @@ -251,7 +251,8 @@ struct XtermWebView: UIViewRepresentable { case "terminalResize": if let dict = message.body as? [String: Any], let cols = dict["cols"] as? Int, - let rows = dict["rows"] as? Int { + let rows = dict["rows"] as? Int + { parent.onResize(cols, rows) } diff --git a/ios/run-tests.sh b/ios/run-tests.sh index d3290b04..3ea26512 100755 --- a/ios/run-tests.sh +++ b/ios/run-tests.sh @@ -47,6 +47,9 @@ if command -v xcpretty &> /dev/null; then -scheme VibeTunnel-iOS \ -destination "platform=iOS Simulator,id=$SIMULATOR_ID" \ -resultBundlePath TestResults.xcresult \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO \ 2>&1 | xcpretty || { EXIT_CODE=$? echo "Tests failed with exit code: $EXIT_CODE" @@ -68,6 +71,9 @@ else -scheme VibeTunnel-iOS \ -destination "platform=iOS Simulator,id=$SIMULATOR_ID" \ -resultBundlePath TestResults.xcresult \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO \ || { EXIT_CODE=$? echo "Tests failed with exit code: $EXIT_CODE" diff --git a/mac/VibeTunnel/Core/Extensions/EnvironmentValues+Services.swift b/mac/VibeTunnel/Core/Extensions/EnvironmentValues+Services.swift index 1aa3b3dd..2d565dd9 100644 --- a/mac/VibeTunnel/Core/Extensions/EnvironmentValues+Services.swift +++ b/mac/VibeTunnel/Core/Extensions/EnvironmentValues+Services.swift @@ -53,7 +53,7 @@ extension View { systemPermissionManager: SystemPermissionManager? = nil, terminalLauncher: TerminalLauncher? = nil ) - -> some View + -> some View { self .environment(\.serverManager, serverManager ?? ServerManager.shared) diff --git a/mac/VibeTunnel/Core/Managers/DockIconManager.swift b/mac/VibeTunnel/Core/Managers/DockIconManager.swift index e20e975c..15336be5 100644 --- a/mac/VibeTunnel/Core/Managers/DockIconManager.swift +++ b/mac/VibeTunnel/Core/Managers/DockIconManager.swift @@ -56,7 +56,8 @@ final class DockIconManager: NSObject, @unchecked Sendable { // Log window details for debugging // for window in visibleWindows { - // logger.debug(" Visible window: \(window.title.isEmpty ? "(untitled)" : window.title, privacy: .public)") + // logger.debug(" Visible window: \(window.title.isEmpty ? "(untitled)" : window.title, privacy: + // .public)") // } // Show dock if user wants it shown OR if any windows are open diff --git a/mac/VibeTunnel/Core/Models/UpdateChannel.swift b/mac/VibeTunnel/Core/Models/UpdateChannel.swift index d692edea..988853dc 100644 --- a/mac/VibeTunnel/Core/Models/UpdateChannel.swift +++ b/mac/VibeTunnel/Core/Models/UpdateChannel.swift @@ -41,7 +41,7 @@ public enum UpdateChannel: String, CaseIterable, Codable, Sendable { /// Static URLs to ensure they're validated at compile time private static let stableAppcastURL: URL = { guard let url = - URL(string: "https://stats.store/api/v1/appcast/appcast.xml") + URL(string: "https://stats.store/api/v1/appcast/appcast.xml") else { fatalError("Invalid stable appcast URL - this should never happen with a hardcoded URL") } @@ -50,9 +50,9 @@ public enum UpdateChannel: String, CaseIterable, Codable, Sendable { private static let prereleaseAppcastURL: URL = { guard let url = - URL( - string: "https://stats.store/api/v1/appcast/appcast-prerelease.xml" - ) + URL( + string: "https://stats.store/api/v1/appcast/appcast-prerelease.xml" + ) else { fatalError("Invalid prerelease appcast URL - this should never happen with a hardcoded URL") } diff --git a/mac/VibeTunnel/Core/Services/BunServer.swift b/mac/VibeTunnel/Core/Services/BunServer.swift index 21b06851..8549b640 100644 --- a/mac/VibeTunnel/Core/Services/BunServer.swift +++ b/mac/VibeTunnel/Core/Services/BunServer.swift @@ -480,7 +480,7 @@ final class BunServer { seconds: TimeInterval, operation: @escaping @Sendable () async -> T ) - async -> T? + async -> T? { await withTaskGroup(of: T?.self) { group in group.addTask { diff --git a/mac/VibeTunnel/Core/Services/DashboardKeychain.swift b/mac/VibeTunnel/Core/Services/DashboardKeychain.swift index 62709032..01ea807b 100644 --- a/mac/VibeTunnel/Core/Services/DashboardKeychain.swift +++ b/mac/VibeTunnel/Core/Services/DashboardKeychain.swift @@ -20,33 +20,33 @@ final class DashboardKeychain { /// Get the dashboard password from keychain func getPassword() -> String? { #if DEBUG - // In debug builds, skip keychain access to avoid authorization dialogs - logger - .info( - "Debug mode: Skipping keychain password retrieval. Password will only persist during current app session." - ) - return nil + // In debug builds, skip keychain access to avoid authorization dialogs + logger + .info( + "Debug mode: Skipping keychain password retrieval. Password will only persist during current app session." + ) + return nil #else - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: service, - kSecAttrAccount as String: account, - kSecReturnData as String: true - ] + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: account, + kSecReturnData as String: true + ] - var result: AnyObject? - let status = SecItemCopyMatching(query as CFDictionary, &result) + var result: AnyObject? + let status = SecItemCopyMatching(query as CFDictionary, &result) - guard status == errSecSuccess, - let data = result as? Data, - let password = String(data: data, encoding: .utf8) - else { - logger.debug("No password found in keychain") - return nil - } + guard status == errSecSuccess, + let data = result as? Data, + let password = String(data: data, encoding: .utf8) + else { + logger.debug("No password found in keychain") + return nil + } - logger.debug("Password retrieved from keychain") - return password + logger.debug("Password retrieved from keychain") + return password #endif } @@ -103,12 +103,12 @@ final class DashboardKeychain { logger.info("Password \(success ? "saved to" : "failed to save to") keychain") #if DEBUG - if success { - logger - .info( - "Debug mode: Password saved to keychain but will not persist across app restarts. The password will only be available during this session to avoid keychain authorization dialogs during development." - ) - } + if success { + logger + .info( + "Debug mode: Password saved to keychain but will not persist across app restarts. The password will only be available during this session to avoid keychain authorization dialogs during development." + ) + } #endif return success diff --git a/mac/VibeTunnel/Core/Services/NgrokService.swift b/mac/VibeTunnel/Core/Services/NgrokService.swift index 90c1a7d7..3490e33e 100644 --- a/mac/VibeTunnel/Core/Services/NgrokService.swift +++ b/mac/VibeTunnel/Core/Services/NgrokService.swift @@ -291,7 +291,7 @@ final class NgrokService: NgrokTunnelProtocol { seconds: TimeInterval, operation: @Sendable @escaping () async throws -> T ) - async throws -> T + async throws -> T { try await withThrowingTaskGroup(of: T.self) { group in group.addTask { diff --git a/mac/VibeTunnel/Core/Services/ServerManager.swift b/mac/VibeTunnel/Core/Services/ServerManager.swift index 8fae7c75..8d63c874 100644 --- a/mac/VibeTunnel/Core/Services/ServerManager.swift +++ b/mac/VibeTunnel/Core/Services/ServerManager.swift @@ -62,7 +62,7 @@ class ServerManager { get { let mode = DashboardAccessMode(rawValue: UserDefaults.standard.string(forKey: "dashboardAccessMode") ?? "" ) ?? - .localhost + .localhost return mode.bindAddress } set { diff --git a/mac/VibeTunnel/Core/Services/SparkleUpdaterManager.swift b/mac/VibeTunnel/Core/Services/SparkleUpdaterManager.swift index c04c6763..5d320a37 100644 --- a/mac/VibeTunnel/Core/Services/SparkleUpdaterManager.swift +++ b/mac/VibeTunnel/Core/Services/SparkleUpdaterManager.swift @@ -52,38 +52,38 @@ public final class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate { // Initialize Sparkle with standard configuration #if DEBUG - // In debug mode, start the updater for testing - updaterController = SPUStandardUpdaterController( - startingUpdater: true, - updaterDelegate: self, - userDriverDelegate: userDriverDelegate - ) + // In debug mode, start the updater for testing + updaterController = SPUStandardUpdaterController( + startingUpdater: true, + updaterDelegate: self, + userDriverDelegate: userDriverDelegate + ) #else - updaterController = SPUStandardUpdaterController( - startingUpdater: true, - updaterDelegate: self, - userDriverDelegate: userDriverDelegate - ) + updaterController = SPUStandardUpdaterController( + startingUpdater: true, + updaterDelegate: self, + userDriverDelegate: userDriverDelegate + ) #endif // Configure automatic updates if let updater = updaterController?.updater { #if DEBUG - // Enable automatic checks in debug too - updater.automaticallyChecksForUpdates = true - updater.automaticallyDownloadsUpdates = false - logger.info("Sparkle updater initialized in DEBUG mode - automatic updates enabled for testing") + // Enable automatic checks in debug too + updater.automaticallyChecksForUpdates = true + updater.automaticallyDownloadsUpdates = false + logger.info("Sparkle updater initialized in DEBUG mode - automatic updates enabled for testing") #else - // Enable automatic checking for updates - updater.automaticallyChecksForUpdates = true + // Enable automatic checking for updates + updater.automaticallyChecksForUpdates = true - // Enable automatic downloading of updates - updater.automaticallyDownloadsUpdates = true + // Enable automatic downloading of updates + updater.automaticallyDownloadsUpdates = true - // Set update check interval to 24 hours - updater.updateCheckInterval = 86_400 + // Set update check interval to 24 hours + updater.updateCheckInterval = 86_400 - logger.info("Sparkle updater initialized successfully with automatic downloads enabled") + logger.info("Sparkle updater initialized successfully with automatic downloads enabled") #endif // Start the updater for both debug and release builds diff --git a/mac/VibeTunnel/Core/Services/SparkleUserDriverDelegate.swift b/mac/VibeTunnel/Core/Services/SparkleUserDriverDelegate.swift index d34a92b2..81e5538d 100644 --- a/mac/VibeTunnel/Core/Services/SparkleUserDriverDelegate.swift +++ b/mac/VibeTunnel/Core/Services/SparkleUserDriverDelegate.swift @@ -39,7 +39,7 @@ final class SparkleUserDriverDelegate: NSObject, @preconcurrency SPUStandardUser _ update: SUAppcastItem, andInImmediateFocus immediateFocus: Bool ) - -> Bool + -> Bool { logger.info("Should handle showing update: \(update.displayVersionString), immediate: \(immediateFocus)") @@ -211,7 +211,7 @@ final class SparkleUserDriverDelegate: NSObject, @preconcurrency SPUStandardUser case "LATER_ACTION": logger.info("User tapped 'Remind Me Later' in notification") - // The next reminder is already scheduled + // The next reminder is already scheduled default: break diff --git a/mac/VibeTunnel/Core/Services/TerminalManager.swift b/mac/VibeTunnel/Core/Services/TerminalManager.swift index caaee9a0..a9ef8011 100644 --- a/mac/VibeTunnel/Core/Services/TerminalManager.swift +++ b/mac/VibeTunnel/Core/Services/TerminalManager.swift @@ -134,7 +134,7 @@ actor TerminalManager { seconds: TimeInterval, operation: @escaping @Sendable () async throws -> T ) - async throws -> T + async throws -> T { try await withThrowingTaskGroup(of: T.self) { group in group.addTask { diff --git a/mac/VibeTunnel/Core/Services/WindowTracker.swift b/mac/VibeTunnel/Core/Services/WindowTracker.swift index 04864670..59fb68ae 100644 --- a/mac/VibeTunnel/Core/Services/WindowTracker.swift +++ b/mac/VibeTunnel/Core/Services/WindowTracker.swift @@ -150,7 +150,7 @@ final class WindowTracker { tabReference: String?, tabID: String? ) - -> WindowInfo? + -> WindowInfo? { let allWindows = Self.getAllTerminalWindows() diff --git a/mac/VibeTunnel/Core/Utilities/ErrorHandling.swift b/mac/VibeTunnel/Core/Utilities/ErrorHandling.swift index 6dc6bdd1..f065d32a 100644 --- a/mac/VibeTunnel/Core/Utilities/ErrorHandling.swift +++ b/mac/VibeTunnel/Core/Utilities/ErrorHandling.swift @@ -32,7 +32,7 @@ extension View { error: Binding, onDismiss: (() -> Void)? = nil ) - -> some View + -> some View { modifier(ErrorAlertModifier(error: error, title: title, onDismiss: onDismiss)) } @@ -49,7 +49,7 @@ extension Task where Failure == Error { errorBinding: Binding, operation: @escaping () async throws -> T ) - -> Task + -> Task { Task(priority: priority) { do { diff --git a/mac/VibeTunnel/Presentation/Utilities/CommonViewModifiers.swift b/mac/VibeTunnel/Presentation/Utilities/CommonViewModifiers.swift index aee17742..d64d1c67 100644 --- a/mac/VibeTunnel/Presentation/Utilities/CommonViewModifiers.swift +++ b/mac/VibeTunnel/Presentation/Utilities/CommonViewModifiers.swift @@ -12,7 +12,7 @@ extension View { horizontal: CGFloat = 16, vertical: CGFloat = 14 ) - -> some View + -> some View { self .padding(.horizontal, horizontal) diff --git a/mac/VibeTunnel/Presentation/Views/MenuBarView.swift b/mac/VibeTunnel/Presentation/Views/MenuBarView.swift index 9f01dedf..df1f050d 100644 --- a/mac/VibeTunnel/Presentation/Views/MenuBarView.swift +++ b/mac/VibeTunnel/Presentation/Views/MenuBarView.swift @@ -54,7 +54,7 @@ struct MenuBarView: View { // Show Tutorial Button(action: { #if !SWIFT_PACKAGE - AppDelegate.showWelcomeScreen() + AppDelegate.showWelcomeScreen() #endif }, label: { HStack { diff --git a/mac/VibeTunnel/Presentation/Views/Settings/DebugSettingsView.swift b/mac/VibeTunnel/Presentation/Views/Settings/DebugSettingsView.swift index 323622d1..af34f113 100644 --- a/mac/VibeTunnel/Presentation/Views/Settings/DebugSettingsView.swift +++ b/mac/VibeTunnel/Presentation/Views/Settings/DebugSettingsView.swift @@ -327,7 +327,7 @@ private struct DeveloperToolsSection: View { Spacer() Button("Show Welcome") { #if !SWIFT_PACKAGE - AppDelegate.showWelcomeScreen() + AppDelegate.showWelcomeScreen() #endif } .buttonStyle(.bordered) diff --git a/mac/VibeTunnel/Presentation/Views/Shared/GlowingAppIcon.swift b/mac/VibeTunnel/Presentation/Views/Shared/GlowingAppIcon.swift index ed370864..6549a5e8 100644 --- a/mac/VibeTunnel/Presentation/Views/Shared/GlowingAppIcon.swift +++ b/mac/VibeTunnel/Presentation/Views/Shared/GlowingAppIcon.swift @@ -171,7 +171,7 @@ struct GlowingAppIcon: View { enableInteraction: true, glowIntensity: 0.3 ) { - print("Icon clicked!") + // Icon clicked - action handled here } } .padding() diff --git a/mac/VibeTunnel/Presentation/Views/Welcome/ProtectDashboardPageView.swift b/mac/VibeTunnel/Presentation/Views/Welcome/ProtectDashboardPageView.swift index ecc8865f..ca1788e6 100644 --- a/mac/VibeTunnel/Presentation/Views/Welcome/ProtectDashboardPageView.swift +++ b/mac/VibeTunnel/Presentation/Views/Welcome/ProtectDashboardPageView.swift @@ -124,7 +124,7 @@ struct ProtectDashboardPageView: View { // When password is set for the first time, automatically switch to network mode let currentMode = DashboardAccessMode(rawValue: UserDefaults.standard - .string(forKey: "dashboardAccessMode") ?? "" + .string(forKey: "dashboardAccessMode") ?? "" ) ?? .localhost if currentMode == .localhost { UserDefaults.standard.set(DashboardAccessMode.network.rawValue, forKey: "dashboardAccessMode") diff --git a/mac/VibeTunnel/Utilities/ApplicationMover.swift b/mac/VibeTunnel/Utilities/ApplicationMover.swift index 96ca12e0..a34e79f0 100644 --- a/mac/VibeTunnel/Utilities/ApplicationMover.swift +++ b/mac/VibeTunnel/Utilities/ApplicationMover.swift @@ -172,8 +172,8 @@ final class ApplicationMover { logger.debug("ApplicationMover: hdiutil returned \(data.count) bytes") guard let plist = try PropertyListSerialization - .propertyList(from: data, options: [], format: nil) as? [String: Any], - let images = plist["images"] as? [[String: Any]] + .propertyList(from: data, options: [], format: nil) as? [String: Any], + let images = plist["images"] as? [[String: Any]] else { logger.debug("ApplicationMover: No disk images found in hdiutil output") return nil diff --git a/mac/VibeTunnel/Utilities/TerminalLauncher.swift b/mac/VibeTunnel/Utilities/TerminalLauncher.swift index 5b3ce02f..6d7d0188 100644 --- a/mac/VibeTunnel/Utilities/TerminalLauncher.swift +++ b/mac/VibeTunnel/Utilities/TerminalLauncher.swift @@ -383,7 +383,7 @@ final class TerminalLauncher { var runningTerminals: [Terminal] = [] for terminal in Terminal.allCases - where runningApps.contains(where: { $0.bundleIdentifier == terminal.bundleIdentifier }) + where runningApps.contains(where: { $0.bundleIdentifier == terminal.bundleIdentifier }) { runningTerminals.append(terminal) logger.debug("Detected running terminal: \(terminal.rawValue)") @@ -416,7 +416,7 @@ final class TerminalLauncher { _ config: TerminalLaunchConfig, sessionId: String? = nil ) - throws -> TerminalLaunchResult + throws -> TerminalLaunchResult { logger.debug("Launch config - command: \(config.command)") logger.debug("Launch config - fullCommand: \(config.fullCommand)") @@ -519,7 +519,7 @@ final class TerminalLauncher { if process.terminationStatus != 0 { throw TerminalLauncherError - .processLaunchFailed("Process exited with status \(process.terminationStatus)") + .processLaunchFailed("Process exited with status \(process.terminationStatus)") } } catch { logger.error("Failed to launch terminal: \(error.localizedDescription)") @@ -676,7 +676,7 @@ final class TerminalLauncher { sessionId: String, vibetunnelPath: String? = nil ) - throws + throws { // Expand tilde in working directory path let expandedWorkingDir = (workingDirectory as NSString).expandingTildeInPath @@ -801,7 +801,7 @@ final class TerminalLauncher { workingDir: String, sessionId: String? = nil ) - -> String + -> String { // Bun executable has fwd command built-in logger.info("Using Bun executable for session creation") diff --git a/mac/VibeTunnel/VibeTunnelApp.swift b/mac/VibeTunnel/VibeTunnelApp.swift index 364db14f..e2bb83f9 100644 --- a/mac/VibeTunnel/VibeTunnelApp.swift +++ b/mac/VibeTunnel/VibeTunnelApp.swift @@ -21,81 +21,81 @@ struct VibeTunnelApp: App { var body: some Scene { #if os(macOS) - // Hidden WindowGroup to make Settings work in MenuBarExtra-only apps - // This is a workaround for FB10184971 - WindowGroup("HiddenWindow") { - HiddenWindowView() - } - .windowResizability(.contentSize) - .defaultSize(width: 1, height: 1) - .windowStyle(.hiddenTitleBar) + // Hidden WindowGroup to make Settings work in MenuBarExtra-only apps + // This is a workaround for FB10184971 + WindowGroup("HiddenWindow") { + HiddenWindowView() + } + .windowResizability(.contentSize) + .defaultSize(width: 1, height: 1) + .windowStyle(.hiddenTitleBar) - // Welcome Window - WindowGroup("Welcome", id: "welcome") { - WelcomeView() + // Welcome Window + WindowGroup("Welcome", id: "welcome") { + WelcomeView() + .environment(sessionMonitor) + .environment(serverManager) + .environment(ngrokService) + .environment(permissionManager) + .environment(terminalLauncher) + } + .windowResizability(.contentSize) + .defaultSize(width: 580, height: 480) + .windowStyle(.hiddenTitleBar) + + // Session Detail Window + WindowGroup("Session Details", id: "session-detail", for: String.self) { $sessionId in + if let sessionId, + let session = sessionMonitor.sessions[sessionId] + { + SessionDetailView(session: session) .environment(sessionMonitor) .environment(serverManager) .environment(ngrokService) .environment(permissionManager) .environment(terminalLauncher) + } else { + Text("Session not found") + .frame(width: 400, height: 300) } - .windowResizability(.contentSize) - .defaultSize(width: 580, height: 480) - .windowStyle(.hiddenTitleBar) + } + .windowResizability(.contentSize) - // Session Detail Window - WindowGroup("Session Details", id: "session-detail", for: String.self) { $sessionId in - if let sessionId, - let session = sessionMonitor.sessions[sessionId] - { - SessionDetailView(session: session) - .environment(sessionMonitor) - .environment(serverManager) - .environment(ngrokService) - .environment(permissionManager) - .environment(terminalLauncher) - } else { - Text("Session not found") - .frame(width: 400, height: 300) - } - } - .windowResizability(.contentSize) - - Settings { - SettingsView() - .environment(sessionMonitor) - .environment(serverManager) - .environment(ngrokService) - .environment(permissionManager) - .environment(terminalLauncher) - } - .commands { - CommandGroup(after: .appInfo) { - Button("About VibeTunnel") { - SettingsOpener.openSettings() - // Navigate to About tab after settings opens - Task { - try? await Task.sleep(for: .milliseconds(100)) - NotificationCenter.default.post( - name: .openSettingsTab, - object: SettingsTab.about - ) - } + Settings { + SettingsView() + .environment(sessionMonitor) + .environment(serverManager) + .environment(ngrokService) + .environment(permissionManager) + .environment(terminalLauncher) + } + .commands { + CommandGroup(after: .appInfo) { + Button("About VibeTunnel") { + SettingsOpener.openSettings() + // Navigate to About tab after settings opens + Task { + try? await Task.sleep(for: .milliseconds(100)) + NotificationCenter.default.post( + name: .openSettingsTab, + object: SettingsTab.about + ) } } } + } - MenuBarExtra { - MenuBarView() - .environment(sessionMonitor) - .environment(serverManager) - .environment(ngrokService) - .environment(permissionManager) - .environment(terminalLauncher) - } label: { - Image("menubar") - .renderingMode(.template) - } + MenuBarExtra { + MenuBarView() + .environment(sessionMonitor) + .environment(serverManager) + .environment(ngrokService) + .environment(permissionManager) + .environment(terminalLauncher) + } label: { + Image("menubar") + .renderingMode(.template) + } #endif } } @@ -121,25 +121,25 @@ final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUser NSClassFromString("XCTestCase") != nil let isRunningInPreview = processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" #if DEBUG - let isRunningInDebug = true + let isRunningInDebug = true #else - let isRunningInDebug = processInfo.environment["DYLD_INSERT_LIBRARIES"]? - .contains("libMainThreadChecker.dylib") ?? false || - processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] != nil + let isRunningInDebug = processInfo.environment["DYLD_INSERT_LIBRARIES"]? + .contains("libMainThreadChecker.dylib") ?? false || + processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] != nil #endif // Handle single instance check before doing anything else #if DEBUG // Skip single instance check in debug builds #else - if !isRunningInPreview && !isRunningInTests && !isRunningInDebug { - handleSingleInstanceCheck() - registerForDistributedNotifications() + if !isRunningInPreview && !isRunningInTests && !isRunningInDebug { + handleSingleInstanceCheck() + registerForDistributedNotifications() - // Check if app needs to be moved to Applications folder - let applicationMover = ApplicationMover() - applicationMover.checkAndOfferToMoveToApplications() - } + // Check if app needs to be moved to Applications folder + let applicationMover = ApplicationMover() + applicationMover.checkAndOfferToMoveToApplications() + } #endif // Initialize Sparkle updater manager @@ -340,11 +340,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUser NSClassFromString("XCTestCase") != nil let isRunningInPreview = processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" #if DEBUG - let isRunningInDebug = true + let isRunningInDebug = true #else - let isRunningInDebug = processInfo.environment["DYLD_INSERT_LIBRARIES"]? - .contains("libMainThreadChecker.dylib") ?? false || - processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] != nil + let isRunningInDebug = processInfo.environment["DYLD_INSERT_LIBRARIES"]? + .contains("libMainThreadChecker.dylib") ?? false || + processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] != nil #endif // Skip cleanup during tests @@ -374,13 +374,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUser #if DEBUG // Skip removing observer in debug builds #else - if !isRunningInPreview && !isRunningInTests && !isRunningInDebug { - DistributedNotificationCenter.default().removeObserver( - self, - name: Self.showSettingsNotification, - object: nil - ) - } + if !isRunningInPreview && !isRunningInTests && !isRunningInDebug { + DistributedNotificationCenter.default().removeObserver( + self, + name: Self.showSettingsNotification, + object: nil + ) + } #endif // Remove update check notification observer