fix: CI and linting issues across all platforms

- Fix code signing in Mac and iOS test workflows
- Fix all SwiftFormat and SwiftLint issues
- Fix ESLint issues in web code
- Remove force casts and unwrapping in Swift code
- Update build scripts to use correct file paths
This commit is contained in:
Peter Steinberger 2025-06-23 19:40:53 +02:00
parent d72b009696
commit baaaa5a033
61 changed files with 685 additions and 609 deletions

View file

@ -206,6 +206,9 @@ jobs:
-scheme VibeTunnel-Mac \ -scheme VibeTunnel-Mac \
-configuration Debug \ -configuration Debug \
-destination "platform=macOS" \ -destination "platform=macOS" \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
| xcbeautify || { | xcbeautify || {
echo "::error::Tests failed" echo "::error::Tests failed"
exit 1 exit 1

View file

@ -28,7 +28,7 @@ struct VibeTunnelApp: App {
} }
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
.macCatalystWindowStyle(getStoredWindowStyle()) .macCatalystWindowStyle(getStoredWindowStyle())
#endif #endif
} }
} }
@ -45,7 +45,8 @@ struct VibeTunnelApp: App {
if url.host == "session", if url.host == "session",
let sessionId = url.pathComponents.last, let sessionId = url.pathComponents.last,
!sessionId.isEmpty { !sessionId.isEmpty
{
navigationManager.navigateToSession(sessionId) navigationManager.navigateToSession(sessionId)
} }
} }
@ -75,7 +76,8 @@ class ConnectionManager {
private func loadSavedConnection() { private func loadSavedConnection() {
if let data = UserDefaults.standard.data(forKey: "savedServerConfig"), 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 self.serverConfig = config
} }
} }

View file

@ -6,12 +6,12 @@ enum AppConfig {
/// Change this to control verbosity of logs /// Change this to control verbosity of logs
static func configureLogging() { static func configureLogging() {
#if DEBUG #if DEBUG
// In debug builds, default to info level to reduce noise // In debug builds, default to info level to reduce noise
// Change to .verbose only when debugging binary protocol issues // Change to .verbose only when debugging binary protocol issues
Logger.globalLevel = .info Logger.globalLevel = .info
#else #else
// In release builds, only show warnings and errors // In release builds, only show warnings and errors
Logger.globalLevel = .warning Logger.globalLevel = .warning
#endif #endif
} }
} }

View file

@ -181,7 +181,8 @@ class CastRecorder {
let eventArray: [Any] = [event.time, event.type, event.data] let eventArray: [Any] = [event.time, event.type, event.data]
if let jsonData = try? JSONSerialization.data(withJSONObject: eventArray), 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" castContent += jsonString + "\n"
} }
} }

View file

@ -37,16 +37,16 @@ struct ServerProfile: Identifiable, Codable, Equatable {
/// Create a ServerConfig from this profile /// Create a ServerConfig from this profile
func toServerConfig(password: String? = nil) -> ServerConfig? { func toServerConfig(password: String? = nil) -> ServerConfig? {
guard let urlComponents = URLComponents(string: url), guard let urlComponents = URLComponents(string: url),
let host = urlComponents.host else { let host = urlComponents.host
else {
return nil return nil
} }
// Determine default port based on scheme // Determine default port based on scheme
let defaultPort: Int let defaultPort: Int = if let scheme = urlComponents.scheme?.lowercased() {
if let scheme = urlComponents.scheme?.lowercased() { scheme == "https" ? 443 : 80
defaultPort = scheme == "https" ? 443 : 80
} else { } else {
defaultPort = 80 80
} }
let port = urlComponents.port ?? defaultPort let port = urlComponents.port ?? defaultPort
@ -68,7 +68,8 @@ extension ServerProfile {
/// Load all saved profiles from UserDefaults /// Load all saved profiles from UserDefaults
static func loadAll() -> [ServerProfile] { static func loadAll() -> [ServerProfile] {
guard let data = UserDefaults.standard.data(forKey: storageKey), 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 []
} }
return profiles return profiles
@ -117,7 +118,8 @@ extension ServerProfile {
static func suggestedName(for url: String) -> String { static func suggestedName(for url: String) -> String {
if let urlComponents = URLComponents(string: url), if let urlComponents = URLComponents(string: url),
let host = urlComponents.host { let host = urlComponents.host
{
// Remove common suffixes // Remove common suffixes
let cleanHost = host let cleanHost = host
.replacingOccurrences(of: ".local", with: "") .replacingOccurrences(of: ".local", with: "")

View file

@ -45,7 +45,8 @@ enum TerminalEvent {
let exitString = array[0] as? String, let exitString = array[0] as? String,
exitString == "exit", exitString == "exit",
let exitCode = array[1] as? Int, let exitCode = array[1] as? Int,
let sessionId = array[2] as? String { let sessionId = array[2] as? String
{
self = .exit(code: exitCode, sessionId: sessionId) self = .exit(code: exitCode, sessionId: sessionId)
return return
} }

View file

@ -27,7 +27,8 @@ enum TerminalRenderer: String, CaseIterable, Codable {
static var selected: Self { static var selected: Self {
get { get {
if let rawValue = UserDefaults.standard.string(forKey: "selectedTerminalRenderer"), if let rawValue = UserDefaults.standard.string(forKey: "selectedTerminalRenderer"),
let renderer = Self(rawValue: rawValue) { let renderer = Self(rawValue: rawValue)
{
return renderer return renderer
} }
return .swiftTerm // Default return .swiftTerm // Default

View file

@ -384,7 +384,8 @@ class APIClient: APIClientProtocol {
// This is the header // This is the header
if let version = json["version"] as? Int, if let version = json["version"] as? Int,
let width = json["width"] as? Int, let width = json["width"] as? Int,
let height = json["height"] as? Int { let height = json["height"] as? Int
{
header = AsciinemaHeader( header = AsciinemaHeader(
version: version, version: version,
width: width, width: width,
@ -401,7 +402,8 @@ class APIClient: APIClientProtocol {
if json.count >= 3, if json.count >= 3,
let timestamp = json[0] as? Double, let timestamp = json[0] as? Double,
let typeStr = json[1] as? String, let typeStr = json[1] as? String,
let eventData = json[2] as? String { let eventData = json[2] as? String
{
let eventType: AsciinemaEvent.EventType let eventType: AsciinemaEvent.EventType
switch typeStr { switch typeStr {
case "o": eventType = .output case "o": eventType = .output
@ -479,7 +481,8 @@ class APIClient: APIClientProtocol {
showHidden: Bool = false, showHidden: Bool = false,
gitFilter: String = "all" gitFilter: String = "all"
) )
async throws -> DirectoryListing { async throws -> DirectoryListing
{
guard let baseURL else { guard let baseURL else {
throw APIError.noServerConfigured throw APIError.noServerConfigured
} }

View file

@ -114,7 +114,8 @@ class BufferWebSocketClient: NSObject {
// Add authentication header if needed // Add authentication header if needed
if let config = UserDefaults.standard.data(forKey: "savedServerConfig"), 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),
let authHeader = serverConfig.authorizationHeader { let authHeader = serverConfig.authorizationHeader
{
headers["Authorization"] = authHeader headers["Authorization"] = authHeader
} }
@ -211,7 +212,8 @@ class BufferWebSocketClient: NSObject {
// Decode terminal event // Decode terminal event
if let event = decodeTerminalEvent(from: messageData), if let event = decodeTerminalEvent(from: messageData),
let handler = subscriptions[sessionId] { let handler = subscriptions[sessionId]
{
logger.verbose("Dispatching event to handler") logger.verbose("Dispatching event to handler")
handler(event) handler(event)
} else { } else {
@ -598,14 +600,16 @@ class BufferWebSocketClient: NSObject {
(0xFF00...0xFF60).contains(value) || // Fullwidth Forms (0xFF00...0xFF60).contains(value) || // Fullwidth Forms
(0xFFE0...0xFFE6).contains(value) || // Fullwidth Forms (0xFFE0...0xFFE6).contains(value) || // Fullwidth Forms
(0x20000...0x2FFFD).contains(value) || // CJK Extension B-F (0x20000...0x2FFFD).contains(value) || // CJK Extension B-F
(0x30000...0x3FFFD).contains(value) { // CJK Extension G (0x30000...0x3FFFD).contains(value)
{ // CJK Extension G
return 2 return 2
} }
// Zero-width characters // Zero-width characters
if (0x200B...0x200F).contains(value) || // Zero-width spaces if (0x200B...0x200F).contains(value) || // Zero-width spaces
(0xFE00...0xFE0F).contains(value) || // Variation selectors (0xFE00...0xFE0F).contains(value) || // Variation selectors
scalar.properties.isJoinControl { scalar.properties.isJoinControl
{
return 0 return 0
} }

View file

@ -80,7 +80,8 @@ enum KeychainService {
} }
guard let data = result as? Data, 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 throw KeychainError.unexpectedData
} }

View file

@ -75,16 +75,17 @@ final class LivePreviewManager {
// Schedule delayed update if not already scheduled // Schedule delayed update if not already scheduled
if self.updateTimers[sessionId] == nil { if self.updateTimers[sessionId] == nil {
let timer = Timer.scheduledTimer(withTimeInterval: self.updateInterval, repeats: false) { _ in let timer = Timer
Task { @MainActor in .scheduledTimer(withTimeInterval: self.updateInterval, repeats: false) { _ in
if let pending = pendingSnapshot { Task { @MainActor in
subscription.latestSnapshot = pending if let pending = pendingSnapshot {
subscription.lastUpdate = Date() subscription.latestSnapshot = pending
pendingSnapshot = nil subscription.lastUpdate = Date()
pendingSnapshot = nil
}
self.updateTimers.removeValue(forKey: sessionId)
} }
self.updateTimers.removeValue(forKey: sessionId)
} }
}
self.updateTimers[sessionId] = timer self.updateTimers[sessionId] = timer
} }
} }
@ -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 { private struct LivePreviewSubscriptionKey: EnvironmentKey {
static let defaultValue: LivePreviewSubscription? = nil static let defaultValue: LivePreviewSubscription? = nil
} }

View file

@ -84,7 +84,8 @@ class QuickLookManager: NSObject, ObservableObject {
for file in files { for file in files {
if let creationDate = try? file.resourceValues(forKeys: [.creationDateKey]).creationDate, if let creationDate = try? file.resourceValues(forKeys: [.creationDateKey]).creationDate,
creationDate < oneHourAgo { creationDate < oneHourAgo
{
try? FileManager.default.removeItem(at: file) try? FileManager.default.removeItem(at: file)
} }
} }

View file

@ -111,7 +111,13 @@ class ReconnectionManager {
extension ReconnectionManager { extension ReconnectionManager {
/// Calculate the next retry delay using exponential backoff /// 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)) let exponentialDelay = baseDelay * pow(2.0, Double(attempt - 1))
return min(exponentialDelay, maxDelay) return min(exponentialDelay, maxDelay)
} }

View file

@ -117,13 +117,15 @@ final class SSEClient: NSObject, @unchecked Sendable {
// Check for exit event // Check for exit event
if let firstElement = array[0] as? String, firstElement == "exit", if let firstElement = array[0] as? String, firstElement == "exit",
let exitCode = array[1] as? Int, 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)) delegate?.sseClient(self, didReceiveEvent: .exit(exitCode: exitCode, sessionId: sessionId))
} }
// Regular terminal output // Regular terminal output
else if let timestamp = array[0] as? Double, else if let timestamp = array[0] as? Double,
let type = array[1] as? String, let type = array[1] as? String,
let outputData = array[2] as? String { let outputData = array[2] as? String
{
delegate?.sseClient( delegate?.sseClient(
self, self,
didReceiveEvent: .terminalOutput(timestamp: timestamp, type: type, data: outputData) didReceiveEvent: .terminalOutput(timestamp: timestamp, type: type, data: outputData)

View file

@ -29,7 +29,9 @@ extension View {
func errorAlert( func errorAlert(
error: Binding<Error?>, error: Binding<Error?>,
onDismiss: (() -> Void)? = nil onDismiss: (() -> Void)? = nil
) -> some View { )
-> some View
{
modifier(ErrorAlertModifier(error: error, onDismiss: onDismiss)) modifier(ErrorAlertModifier(error: error, onDismiss: onDismiss))
} }
} }
@ -70,22 +72,22 @@ extension APIError: RecoverableError {
var recoverySuggestion: String? { var recoverySuggestion: String? {
switch self { switch self {
case .noServerConfigured: case .noServerConfigured:
return "Please configure a server connection in Settings." "Please configure a server connection in Settings."
case .networkError: case .networkError:
return "Check your internet connection and try again." "Check your internet connection and try again."
case .serverError(let code, _): case .serverError(let code, _):
switch code { switch code {
case 401: case 401:
return "Check your authentication credentials in Settings." "Check your authentication credentials in Settings."
case 500...599: case 500...599:
return "The server is experiencing issues. Please try again later." "The server is experiencing issues. Please try again later."
default: default:
return nil nil
} }
case .resizeDisabledByServer: case .resizeDisabledByServer:
return "Terminal resizing is not supported by this server." "Terminal resizing is not supported by this server."
default: default:
return nil nil
} }
} }
} }
@ -151,7 +153,9 @@ extension Task where Failure == Error {
priority: TaskPriority? = nil, priority: TaskPriority? = nil,
errorHandler: @escaping @Sendable (Error) -> Void, errorHandler: @escaping @Sendable (Error) -> Void,
operation: @escaping @Sendable () async throws -> T operation: @escaping @Sendable () async throws -> T
) -> Task<T, Error> { )
-> Task<T, Error>
{
Task<T, Error>(priority: priority) { Task<T, Error>(priority: priority) {
do { do {
return try await operation() return try await operation()

View file

@ -24,9 +24,9 @@ struct Logger {
// Global log level - only messages at this level or higher will be printed // Global log level - only messages at this level or higher will be printed
#if DEBUG #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 #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 #endif
init(category: String) { init(category: String) {

View file

@ -1,13 +1,13 @@
import SwiftUI import SwiftUI
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
import UIKit
import Dynamic import Dynamic
import UIKit
// MARK: - Window Style // MARK: - Window Style
enum MacWindowStyle { enum MacWindowStyle {
case standard // Normal title bar with traffic lights case standard // Normal title bar with traffic lights
case inline // Hidden title bar with repositioned traffic lights case inline // Hidden title bar with repositioned traffic lights
} }
// MARK: - UIWindow Extension // MARK: - UIWindow Extension
@ -61,8 +61,9 @@ class MacCatalystWindowManager: ObservableObject {
} }
private func applyWindowStyle(_ style: MacWindowStyle) { private func applyWindowStyle(_ style: MacWindowStyle) {
guard let window = window, guard let window,
let nsWindow = window.nsWindow else { let nsWindow = window.nsWindow
else {
logger.warning("Unable to access NSWindow") logger.warning("Unable to access NSWindow")
return return
} }
@ -83,7 +84,12 @@ class MacCatalystWindowManager: ObservableObject {
// Show title bar // Show title bar
nsWindow.titlebarAppearsTransparent = false nsWindow.titlebarAppearsTransparent = false
nsWindow.titleVisibility = Dynamic.NSWindowTitleVisibility.visible 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 // Reset traffic light positions
resetTrafficLightPositions(nsWindow) resetTrafficLightPositions(nsWindow)
@ -104,8 +110,12 @@ class MacCatalystWindowManager: ObservableObject {
nsWindow.backgroundColor = Dynamic.NSColor.clearColor nsWindow.backgroundColor = Dynamic.NSColor.clearColor
// Keep the titled style mask to preserve traffic lights // Keep the titled style mask to preserve traffic lights
let currentMask = nsWindow.styleMask.asObject! as! UInt guard let currentMask = nsWindow.styleMask.asObject as? UInt,
nsWindow.styleMask = currentMask | Dynamic.NSWindowStyleMask.titled.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 // Reposition traffic lights
repositionTrafficLights(nsWindow, window: window) repositionTrafficLights(nsWindow, window: window)
@ -164,8 +174,13 @@ class MacCatalystWindowManager: ObservableObject {
// Add new tracking area at the button's current position // Add new tracking area at the button's current position
let trackingRect = button.bounds let trackingRect = button.bounds
let options = Dynamic.NSTrackingAreaOptions.mouseEnteredAndExited.asObject! as! UInt | guard let mouseEnteredAndExited = Dynamic.NSTrackingAreaOptions.mouseEnteredAndExited.asObject as? UInt,
Dynamic.NSTrackingAreaOptions.activeAlways.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() let trackingArea = Dynamic.NSTrackingArea.alloc()
.initWithRect(trackingRect, options: options, owner: button, userInfo: nil) .initWithRect(trackingRect, options: options, owner: button, userInfo: nil)
@ -189,7 +204,7 @@ class MacCatalystWindowManager: ObservableObject {
object: nil, object: nil,
queue: .main queue: .main
) { [weak self] notification in ) { [weak self] notification in
guard let self = self, guard let self,
self.windowStyle == .inline, self.windowStyle == .inline,
let window = self.window, let window = self.window,
let notificationWindow = notification.object as? NSObject, let notificationWindow = notification.object as? NSObject,
@ -208,7 +223,7 @@ class MacCatalystWindowManager: ObservableObject {
object: window, object: window,
queue: .main queue: .main
) { [weak self] _ in ) { [weak self] _ in
guard let self = self, guard let self,
self.windowStyle == .inline else { return } self.windowStyle == .inline else { return }
// Reapply inline style when window becomes key // Reapply inline style when window becomes key
@ -223,12 +238,13 @@ class MacCatalystWindowManager: ObservableObject {
object: nil, object: nil,
queue: .main queue: .main
) { [weak self] _ in ) { [weak self] _ in
guard let self = self, guard let self,
self.windowStyle == .inline else { return } self.windowStyle == .inline else { return }
// Reposition if needed // Reposition if needed
if let window = self.window, if let window = self.window,
let nsWindow = window.nsWindow { let nsWindow = window.nsWindow
{
self.repositionTrafficLights(Dynamic(nsWindow), window: window) self.repositionTrafficLights(Dynamic(nsWindow), window: window)
} }
} }
@ -259,7 +275,8 @@ struct MacCatalystWindowStyle: ViewModifier {
private func setupWindow() { private func setupWindow() {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first else { let window = windowScene.windows.first
else {
return return
} }

View file

@ -17,13 +17,13 @@ class ServerProfilesViewModel {
profiles = ServerProfile.loadAll().sorted { profile1, profile2 in profiles = ServerProfile.loadAll().sorted { profile1, profile2 in
// Sort by last connected (most recent first), then by name // Sort by last connected (most recent first), then by name
if let date1 = profile1.lastConnected, let date2 = profile2.lastConnected { if let date1 = profile1.lastConnected, let date2 = profile2.lastConnected {
return date1 > date2 date1 > date2
} else if profile1.lastConnected != nil { } else if profile1.lastConnected != nil {
return true true
} else if profile2.lastConnected != nil { } else if profile2.lastConnected != nil {
return false false
} else { } else {
return profile1.name < profile2.name profile1.name < profile2.name
} }
} }
} }
@ -32,7 +32,7 @@ class ServerProfilesViewModel {
ServerProfile.save(profile) ServerProfile.save(profile)
// Save password to keychain if provided // Save password to keychain if provided
if let password = password, !password.isEmpty { if let password, !password.isEmpty {
try KeychainService.savePassword(password, for: profile.id) try KeychainService.savePassword(password, for: profile.id)
} }
@ -45,7 +45,7 @@ class ServerProfilesViewModel {
ServerProfile.save(updatedProfile) ServerProfile.save(updatedProfile)
// Update password if provided // Update password if provided
if let password = password { if let password {
if password.isEmpty { if password.isEmpty {
// Delete password if empty // Delete password if empty
try KeychainService.deletePassword(for: profile.id) try KeychainService.deletePassword(for: profile.id)
@ -140,7 +140,8 @@ extension ServerProfilesViewModel {
// Validate URL // Validate URL
guard let url = URL(string: cleanURL), guard let url = URL(string: cleanURL),
let _ = url.host else { let _ = url.host
else {
return nil return nil
} }

View file

@ -121,7 +121,8 @@ class ConnectionViewModel {
func loadLastConnection() { func loadLastConnection() {
if let config = UserDefaults.standard.data(forKey: "savedServerConfig"), 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.host = serverConfig.host
self.port = String(serverConfig.port) self.port = String(serverConfig.port)
self.name = serverConfig.name ?? "" self.name = serverConfig.name ?? ""
@ -162,7 +163,8 @@ class ConnectionViewModel {
let (_, response) = try await URLSession.shared.data(for: request) let (_, response) = try await URLSession.shared.data(for: request)
if let httpResponse = response as? HTTPURLResponse, if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 { httpResponse.statusCode == 200
{
onSuccess(config) onSuccess(config)
} else { } else {
errorMessage = "Failed to connect to server" errorMessage = "Failed to connect to server"

View file

@ -21,44 +21,44 @@ struct EnhancedConnectionView: View {
NavigationStack { NavigationStack {
ZStack { ZStack {
ScrollView { ScrollView {
VStack(spacing: Theme.Spacing.extraLarge) { VStack(spacing: Theme.Spacing.extraLarge) {
// Logo and Title // Logo and Title
headerView headerView
.padding(.top, { .padding(.top, {
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
return windowManager.windowStyle == .inline ? 60 : 40 return windowManager.windowStyle == .inline ? 60 : 40
#else #else
return 40 return 40
#endif #endif
}()) }())
// Quick Connect Section // Quick Connect Section
if !profilesViewModel.profiles.isEmpty && !showingNewServerForm { if !profilesViewModel.profiles.isEmpty && !showingNewServerForm {
quickConnectSection quickConnectSection
.opacity(contentOpacity) .opacity(contentOpacity)
.onAppear { .onAppear {
withAnimation(Theme.Animation.smooth.delay(0.3)) { withAnimation(Theme.Animation.smooth.delay(0.3)) {
contentOpacity = 1.0 contentOpacity = 1.0
}
} }
} }
}
// New Connection Form // New Connection Form
if showingNewServerForm || profilesViewModel.profiles.isEmpty { if showingNewServerForm || profilesViewModel.profiles.isEmpty {
newConnectionSection newConnectionSection
.opacity(contentOpacity) .opacity(contentOpacity)
.onAppear { .onAppear {
withAnimation(Theme.Animation.smooth.delay(0.3)) { withAnimation(Theme.Animation.smooth.delay(0.3)) {
contentOpacity = 1.0 contentOpacity = 1.0
}
} }
} }
}
Spacer(minLength: 50) Spacer(minLength: 50)
}
.padding()
} }
.padding() .scrollBounceBehavior(.basedOnSize)
}
.scrollBounceBehavior(.basedOnSize)
} }
.toolbar(.hidden, for: .navigationBar) .toolbar(.hidden, for: .navigationBar)
.background(Theme.Colors.terminalBackground.ignoresSafeArea()) .background(Theme.Colors.terminalBackground.ignoresSafeArea())
@ -418,7 +418,7 @@ struct ServerProfileEditView: View {
onDelete() onDelete()
dismiss() dismiss()
} }
Button("Cancel", role: .cancel) { } Button("Cancel", role: .cancel) {}
} message: { } message: {
Text("Are you sure you want to delete \"\(profile.name)\"? This action cannot be undone.") Text("Are you sure you want to delete \"\(profile.name)\"? This action cannot be undone.")
} }
@ -426,7 +426,8 @@ struct ServerProfileEditView: View {
.task { .task {
// Load existing password from keychain // Load existing password from keychain
if profile.requiresAuth, if profile.requiresAuth,
let existingPassword = try? KeychainService.getPassword(for: profile.id) { let existingPassword = try? KeychainService.getPassword(for: profile.id)
{
password = existingPassword password = existingPassword
} }
} }

View file

@ -143,13 +143,13 @@ struct ServerConfigForm: View {
} }
}) })
.foregroundColor(isConnecting || !networkMonitor.isConnected ? Theme.Colors.terminalForeground : Theme .foregroundColor(isConnecting || !networkMonitor.isConnected ? Theme.Colors.terminalForeground : Theme
.Colors.primaryAccent .Colors.primaryAccent
) )
.padding(.vertical, Theme.Spacing.medium) .padding(.vertical, Theme.Spacing.medium)
.background( .background(
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium) RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
.fill(isConnecting || !networkMonitor.isConnected ? Theme.Colors.cardBackground : Theme.Colors .fill(isConnecting || !networkMonitor.isConnected ? Theme.Colors.cardBackground : Theme.Colors
.terminalBackground .terminalBackground
) )
) )
.overlay( .overlay(
@ -218,7 +218,8 @@ struct ServerConfigForm: View {
private func loadRecentServers() { private func loadRecentServers() {
// Load recent servers from UserDefaults // Load recent servers from UserDefaults
if let data = UserDefaults.standard.data(forKey: "recentServers"), 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 recentServers = servers
} }
} }

View file

@ -88,7 +88,8 @@ struct FilePreviewView: View {
case .image: case .image:
if let content = preview.content, if let content = preview.content,
let data = Data(base64Encoded: content), let data = Data(base64Encoded: content),
let uiImage = UIImage(data: data) { let uiImage = UIImage(data: data)
{
Image(uiImage: uiImage) Image(uiImage: uiImage)
.resizable() .resizable()
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)

View file

@ -109,14 +109,14 @@ struct FileBrowserView: View {
.font(.custom("SF Mono", size: 12)) .font(.custom("SF Mono", size: 12))
} }
.foregroundColor(viewModel.gitFilter == .changed ? Theme.Colors.successAccent : Theme.Colors .foregroundColor(viewModel.gitFilter == .changed ? Theme.Colors.successAccent : Theme.Colors
.terminalGray .terminalGray
) )
.padding(.horizontal, 12) .padding(.horizontal, 12)
.padding(.vertical, 6) .padding(.vertical, 6)
.background( .background(
RoundedRectangle(cornerRadius: 6) RoundedRectangle(cornerRadius: 6)
.fill(viewModel.gitFilter == .changed ? Theme.Colors.successAccent.opacity(0.2) : Theme.Colors .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( .background(
RoundedRectangle(cornerRadius: 6) RoundedRectangle(cornerRadius: 6)
.fill(viewModel.showHidden ? Theme.Colors.terminalAccent.opacity(0.2) : Theme.Colors .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) Text(name)
.font(.custom("SF Mono", size: 14)) .font(.custom("SF Mono", size: 14))
.foregroundColor(isParent ? Theme.Colors .foregroundColor(isParent ? Theme.Colors
.terminalAccent : (isDirectory ? Theme.Colors.terminalWhite : Theme.Colors.terminalGray) .terminalAccent : (isDirectory ? Theme.Colors.terminalWhite : Theme.Colors.terminalGray)
) )
.lineLimit(1) .lineLimit(1)
.truncationMode(.middle) .truncationMode(.middle)

View file

@ -25,7 +25,8 @@ struct SessionCardView: View {
// Convert absolute paths back to ~ notation for display // Convert absolute paths back to ~ notation for display
let homePrefix = "/Users/" let homePrefix = "/Users/"
if session.workingDir.hasPrefix(homePrefix), 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...]) let restOfPath = String(session.workingDir[userEndIndex...])
return "~\(restOfPath)" return "~\(restOfPath)"
} }
@ -61,7 +62,7 @@ struct SessionCardView: View {
Image(systemName: session.isRunning ? "xmark.circle" : "trash.circle") Image(systemName: session.isRunning ? "xmark.circle" : "trash.circle")
.font(.system(size: 18)) .font(.system(size: 18))
.foregroundColor(session.isRunning ? Theme.Colors.errorAccent : Theme.Colors .foregroundColor(session.isRunning ? Theme.Colors.errorAccent : Theme.Colors
.terminalForeground.opacity(0.6) .terminalForeground.opacity(0.6)
) )
} }
}) })
@ -106,13 +107,13 @@ struct SessionCardView: View {
HStack(spacing: 4) { HStack(spacing: 4) {
Circle() Circle()
.fill(session.isRunning ? Theme.Colors.successAccent : Theme.Colors.terminalForeground .fill(session.isRunning ? Theme.Colors.successAccent : Theme.Colors.terminalForeground
.opacity(0.3) .opacity(0.3)
) )
.frame(width: 6, height: 6) .frame(width: 6, height: 6)
Text(session.isRunning ? "running" : "exited") Text(session.isRunning ? "running" : "exited")
.font(Theme.Typography.terminalSystem(size: 10)) .font(Theme.Typography.terminalSystem(size: 10))
.foregroundColor(session.isRunning ? Theme.Colors.successAccent : Theme.Colors .foregroundColor(session.isRunning ? Theme.Colors.successAccent : Theme.Colors
.terminalForeground.opacity(0.5) .terminalForeground.opacity(0.5)
) )
// Live preview indicator // Live preview indicator

View file

@ -117,9 +117,9 @@ struct SessionCreateView: View {
if presentedError != nil { if presentedError != nil {
ErrorBanner( ErrorBanner(
message: presentedError?.error.localizedDescription ?? "An error occurred" message: presentedError?.error.localizedDescription ?? "An error occurred"
) { ) {
presentedError = nil presentedError = nil
} }
.overlay( .overlay(
RoundedRectangle(cornerRadius: Theme.CornerRadius.small) RoundedRectangle(cornerRadius: Theme.CornerRadius.small)
.stroke(Theme.Colors.errorAccent.opacity(0.3), lineWidth: 1) .stroke(Theme.Colors.errorAccent.opacity(0.3), lineWidth: 1)
@ -156,14 +156,14 @@ struct SessionCreateView: View {
.font(Theme.Typography.terminalSystem(size: 13)) .font(Theme.Typography.terminalSystem(size: 13))
} }
.foregroundColor(workingDirectory == dir ? Theme.Colors .foregroundColor(workingDirectory == dir ? Theme.Colors
.terminalBackground : Theme.Colors.terminalForeground .terminalBackground : Theme.Colors.terminalForeground
) )
.padding(.horizontal, 12) .padding(.horizontal, 12)
.padding(.vertical, 8) .padding(.vertical, 8)
.background( .background(
RoundedRectangle(cornerRadius: Theme.CornerRadius.small) RoundedRectangle(cornerRadius: Theme.CornerRadius.small)
.fill(workingDirectory == dir ? Theme.Colors .fill(workingDirectory == dir ? Theme.Colors
.primaryAccent : Theme.Colors.cardBorder.opacity(0.1) .primaryAccent : Theme.Colors.cardBorder.opacity(0.1)
) )
) )
.overlay( .overlay(
@ -209,16 +209,16 @@ struct SessionCreateView: View {
Spacer() Spacer()
} }
.foregroundColor(command == item.command ? Theme.Colors .foregroundColor(command == item.command ? Theme.Colors
.terminalBackground : Theme.Colors .terminalBackground : Theme.Colors
.terminalForeground .terminalForeground
) )
.padding(.horizontal, Theme.Spacing.medium) .padding(.horizontal, Theme.Spacing.medium)
.padding(.vertical, 14) .padding(.vertical, 14)
.background( .background(
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium) RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
.fill(command == item.command ? Theme.Colors.primaryAccent : Theme .fill(command == item.command ? Theme.Colors.primaryAccent : Theme
.Colors .Colors
.cardBackground .cardBackground
) )
) )
.overlay( .overlay(
@ -283,7 +283,7 @@ struct SessionCreateView: View {
Text("Create") Text("Create")
.font(.system(size: 17, weight: .semibold)) .font(.system(size: 17, weight: .semibold))
.foregroundColor(command.isEmpty ? Theme.Colors.primaryAccent.opacity(0.5) : Theme .foregroundColor(command.isEmpty ? Theme.Colors.primaryAccent.opacity(0.5) : Theme
.Colors.primaryAccent .Colors.primaryAccent
) )
} }
}) })

View file

@ -200,7 +200,8 @@ struct SessionListView: View {
.onChange(of: navigationManager.shouldNavigateToSession) { _, shouldNavigate in .onChange(of: navigationManager.shouldNavigateToSession) { _, shouldNavigate in
if shouldNavigate, if shouldNavigate,
let sessionId = navigationManager.selectedSessionId, let sessionId = navigationManager.selectedSessionId,
let session = viewModel.sessions.first(where: { $0.id == sessionId }) { let session = viewModel.sessions.first(where: { $0.id == sessionId })
{
selectedSession = session selectedSession = session
navigationManager.clearNavigation() navigationManager.clearNavigation()
} }

View file

@ -40,7 +40,7 @@ struct SettingsView: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, Theme.Spacing.medium) .padding(.vertical, Theme.Spacing.medium)
.foregroundColor(selectedTab == tab ? Theme.Colors.primaryAccent : Theme.Colors .foregroundColor(selectedTab == tab ? Theme.Colors.primaryAccent : Theme.Colors
.terminalForeground.opacity(0.5) .terminalForeground.opacity(0.5)
) )
.background( .background(
selectedTab == tab ? Theme.Colors.primaryAccent.opacity(0.1) : Color.clear selectedTab == tab ? Theme.Colors.primaryAccent.opacity(0.1) : Color.clear
@ -294,10 +294,11 @@ struct AdvancedSettingsView: View {
} }
Text(macWindowStyle == .inline ? Text(macWindowStyle == .inline ?
"Traffic light buttons appear inline with content" : "Traffic light buttons appear inline with content" :
"Standard macOS title bar with traffic lights") "Standard macOS title bar with traffic lights"
.font(Theme.Typography.terminalSystem(size: 12)) )
.foregroundColor(Theme.Colors.terminalForeground.opacity(0.6)) .font(Theme.Typography.terminalSystem(size: 12))
.foregroundColor(Theme.Colors.terminalForeground.opacity(0.6))
} }
.padding() .padding()
.background(Theme.Colors.cardBackground) .background(Theme.Colors.cardBackground)
@ -434,7 +435,7 @@ struct LinkRow: View {
var body: some View { var body: some View {
Button(action: { Button(action: {
if let url = url { if let url {
UIApplication.shared.open(url) UIApplication.shared.open(url)
} }
}) { }) {

View file

@ -296,7 +296,8 @@ struct SystemLogsView: View {
// Present it // Present it
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first, let window = windowScene.windows.first,
let rootVC = window.rootViewController { let rootVC = window.rootViewController
{
rootVC.present(activityVC, animated: true) rootVC.present(activityVC, animated: true)
} }
} }

View file

@ -233,7 +233,8 @@ struct CtrlKeyButton: View {
Button(action: { Button(action: {
// Calculate control character (Ctrl+A = 1, Ctrl+B = 2, etc.) // Calculate control character (Ctrl+A = 1, Ctrl+B = 2, etc.)
if let scalar = char.unicodeScalars.first, if let scalar = char.unicodeScalars.first,
let ctrlScalar = UnicodeScalar(scalar.value - 64) { let ctrlScalar = UnicodeScalar(scalar.value - 64)
{
let ctrlChar = Character(ctrlScalar) let ctrlChar = Character(ctrlScalar)
onPress(String(ctrlChar)) onPress(String(ctrlChar))
} }

View file

@ -7,7 +7,7 @@ struct CtrlKeyGrid: View {
@Binding var isPresented: Bool @Binding var isPresented: Bool
let onKeyPress: (String) -> Void let onKeyPress: (String) -> Void
// Common Ctrl combinations organized by category /// Common Ctrl combinations organized by category
let navigationKeys = [ let navigationKeys = [
("A", "Beginning of line"), ("A", "Beginning of line"),
("E", "End of line"), ("E", "End of line"),
@ -70,7 +70,7 @@ struct CtrlKeyGrid: View {
CtrlGridKeyButton( CtrlGridKeyButton(
key: key, key: key,
description: description description: description
) { sendCtrlKey(key) } ) { sendCtrlKey(key) }
} }
} }
.padding() .padding()
@ -106,11 +106,11 @@ struct CtrlKeyGrid: View {
private var currentKeys: [(String, String)] { private var currentKeys: [(String, String)] {
switch selectedCategory { switch selectedCategory {
case 0: return navigationKeys case 0: navigationKeys
case 1: return editingKeys case 1: editingKeys
case 2: return processKeys case 2: processKeys
case 3: return searchKeys case 3: searchKeys
default: return navigationKeys default: navigationKeys
} }
} }

View file

@ -81,14 +81,14 @@ struct FontSizeSheet: View {
Text("\(Int(size))") Text("\(Int(size))")
.font(.system(size: 14, weight: .medium)) .font(.system(size: 14, weight: .medium))
.foregroundColor(fontSize == size ? Theme.Colors.terminalBackground : Theme.Colors .foregroundColor(fontSize == size ? Theme.Colors.terminalBackground : Theme.Colors
.terminalForeground .terminalForeground
) )
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, Theme.Spacing.small) .padding(.vertical, Theme.Spacing.small)
.background( .background(
RoundedRectangle(cornerRadius: Theme.CornerRadius.small) RoundedRectangle(cornerRadius: Theme.CornerRadius.small)
.fill(fontSize == size ? Theme.Colors.primaryAccent : Theme.Colors .fill(fontSize == size ? Theme.Colors.primaryAccent : Theme.Colors
.cardBorder.opacity(0.3) .cardBorder.opacity(0.3)
) )
) )
.overlay( .overlay(

View file

@ -12,7 +12,9 @@ struct QuickFontSizeButtons: View {
Button(action: decreaseFontSize) { Button(action: decreaseFontSize) {
Image(systemName: "minus") Image(systemName: "minus")
.font(.system(size: 14, weight: .medium)) .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) .frame(width: 30, height: 30)
.background(Theme.Colors.cardBackground) .background(Theme.Colors.cardBackground)
.overlay( .overlay(
@ -41,7 +43,9 @@ struct QuickFontSizeButtons: View {
Button(action: increaseFontSize) { Button(action: increaseFontSize) {
Image(systemName: "plus") Image(systemName: "plus")
.font(.system(size: 14, weight: .medium)) .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) .frame(width: 30, height: 30)
.background(Theme.Colors.cardBackground) .background(Theme.Colors.cardBackground)
.overlay( .overlay(

View file

@ -68,7 +68,7 @@ struct TerminalBufferPreview: View {
} }
// Check if RGB color (has alpha channel flag) // Check if RGB color (has alpha channel flag)
if (fg & 0xFF000000) != 0 { if (fg & 0xFF00_0000) != 0 {
// RGB color // RGB color
let red = Double((fg >> 16) & 0xFF) / 255.0 let red = Double((fg >> 16) & 0xFF) / 255.0
let green = Double((fg >> 8) & 0xFF) / 255.0 let green = Double((fg >> 8) & 0xFF) / 255.0
@ -86,7 +86,7 @@ struct TerminalBufferPreview: View {
} }
// Check if RGB color (has alpha channel flag) // Check if RGB color (has alpha channel flag)
if (bg & 0xFF000000) != 0 { if (bg & 0xFF00_0000) != 0 {
// RGB color // RGB color
let red = Double((bg >> 16) & 0xFF) / 255.0 let red = Double((bg >> 16) & 0xFF) / 255.0
let green = Double((bg >> 8) & 0xFF) / 255.0 let green = Double((bg >> 8) & 0xFF) / 255.0

View file

@ -383,7 +383,8 @@ struct TerminalHostingView: UIViewRepresentable {
from oldSnapshot: BufferSnapshot, from oldSnapshot: BufferSnapshot,
to newSnapshot: BufferSnapshot to newSnapshot: BufferSnapshot
) )
-> String { -> String
{
var output = "" var output = ""
var currentFg: Int? var currentFg: Int?
var currentBg: Int? var currentBg: Int?

View file

@ -74,8 +74,8 @@ struct TerminalThemeSheet: View {
.background( .background(
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium) RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
.fill(selectedTheme.id == theme.id .fill(selectedTheme.id == theme.id
? Theme.Colors.primaryAccent.opacity(0.1) ? Theme.Colors.primaryAccent.opacity(0.1)
: Theme.Colors.cardBorder.opacity(0.1) : Theme.Colors.cardBorder.opacity(0.1)
) )
) )
.overlay( .overlay(

View file

@ -112,7 +112,9 @@ struct TerminalView: View {
} }
) )
.task { .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 { if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
withAnimation(Theme.Animation.standard) { withAnimation(Theme.Animation.standard) {
keyboardHeight = keyboardFrame.height keyboardHeight = keyboardFrame.height
@ -524,10 +526,10 @@ struct TerminalView: View {
.overlay( .overlay(
ScrollToBottomButton( ScrollToBottomButton(
isVisible: showScrollToBottom isVisible: showScrollToBottom
) { ) {
viewModel.scrollToBottom() viewModel.scrollToBottom()
showScrollToBottom = false showScrollToBottom = false
} }
.padding(.bottom, Theme.Spacing.large) .padding(.bottom, Theme.Spacing.large)
.padding(.leading, Theme.Spacing.large), .padding(.leading, Theme.Spacing.large),
alignment: .bottomLeading alignment: .bottomLeading
@ -698,7 +700,7 @@ class TerminalViewModel {
logger.info("Terminal initialized: \(width)x\(height)") logger.info("Terminal initialized: \(width)x\(height)")
terminalCols = width terminalCols = width
terminalRows = height terminalRows = height
// The terminal will be resized when created // The terminal will be resized when created
case .output(_, let data): case .output(_, let data):
// Feed output data directly to the terminal // Feed output data directly to the terminal
@ -723,7 +725,8 @@ class TerminalViewModel {
let parts = dimensions.split(separator: "x") let parts = dimensions.split(separator: "x")
if parts.count == 2, if parts.count == 2,
let cols = Int(parts[0]), let cols = Int(parts[0]),
let rows = Int(parts[1]) { let rows = Int(parts[1])
{
// Update terminal dimensions // Update terminal dimensions
terminalCols = cols terminalCols = cols
terminalRows = rows terminalRows = rows

View file

@ -142,8 +142,8 @@ struct TerminalWidthSheet: View {
.background( .background(
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium) RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
.fill(selectedWidth == preset.columns .fill(selectedWidth == preset.columns
? Theme.Colors.primaryAccent.opacity(0.1) ? Theme.Colors.primaryAccent.opacity(0.1)
: Theme.Colors.cardBorder.opacity(0.1) : Theme.Colors.cardBorder.opacity(0.1)
) )
) )
.overlay( .overlay(

View file

@ -44,8 +44,8 @@ struct WidthSelectorPopover: View {
let customWidths = TerminalWidthManager.shared.customWidths let customWidths = TerminalWidthManager.shared.customWidths
if !customWidths.isEmpty { if !customWidths.isEmpty {
Section(header: Text("Recent Custom Widths") Section(header: Text("Recent Custom Widths")
.font(.caption) .font(.caption)
.foregroundColor(Theme.Colors.terminalForeground.opacity(0.7)) .foregroundColor(Theme.Colors.terminalForeground.opacity(0.7))
) { ) {
ForEach(customWidths, id: \.self) { width in ForEach(customWidths, id: \.self) { width in
WidthPresetRow( WidthPresetRow(

View file

@ -251,7 +251,8 @@ struct XtermWebView: UIViewRepresentable {
case "terminalResize": case "terminalResize":
if let dict = message.body as? [String: Any], if let dict = message.body as? [String: Any],
let cols = dict["cols"] as? Int, let cols = dict["cols"] as? Int,
let rows = dict["rows"] as? Int { let rows = dict["rows"] as? Int
{
parent.onResize(cols, rows) parent.onResize(cols, rows)
} }

View file

@ -47,6 +47,9 @@ if command -v xcpretty &> /dev/null; then
-scheme VibeTunnel-iOS \ -scheme VibeTunnel-iOS \
-destination "platform=iOS Simulator,id=$SIMULATOR_ID" \ -destination "platform=iOS Simulator,id=$SIMULATOR_ID" \
-resultBundlePath TestResults.xcresult \ -resultBundlePath TestResults.xcresult \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
2>&1 | xcpretty || { 2>&1 | xcpretty || {
EXIT_CODE=$? EXIT_CODE=$?
echo "Tests failed with exit code: $EXIT_CODE" echo "Tests failed with exit code: $EXIT_CODE"
@ -68,6 +71,9 @@ else
-scheme VibeTunnel-iOS \ -scheme VibeTunnel-iOS \
-destination "platform=iOS Simulator,id=$SIMULATOR_ID" \ -destination "platform=iOS Simulator,id=$SIMULATOR_ID" \
-resultBundlePath TestResults.xcresult \ -resultBundlePath TestResults.xcresult \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
|| { || {
EXIT_CODE=$? EXIT_CODE=$?
echo "Tests failed with exit code: $EXIT_CODE" echo "Tests failed with exit code: $EXIT_CODE"

View file

@ -53,7 +53,7 @@ extension View {
systemPermissionManager: SystemPermissionManager? = nil, systemPermissionManager: SystemPermissionManager? = nil,
terminalLauncher: TerminalLauncher? = nil terminalLauncher: TerminalLauncher? = nil
) )
-> some View -> some View
{ {
self self
.environment(\.serverManager, serverManager ?? ServerManager.shared) .environment(\.serverManager, serverManager ?? ServerManager.shared)

View file

@ -56,7 +56,8 @@ final class DockIconManager: NSObject, @unchecked Sendable {
// Log window details for debugging // Log window details for debugging
// for window in visibleWindows { // 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 // Show dock if user wants it shown OR if any windows are open

View file

@ -41,7 +41,7 @@ public enum UpdateChannel: String, CaseIterable, Codable, Sendable {
/// Static URLs to ensure they're validated at compile time /// Static URLs to ensure they're validated at compile time
private static let stableAppcastURL: URL = { private static let stableAppcastURL: URL = {
guard let 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 { else {
fatalError("Invalid stable appcast URL - this should never happen with a hardcoded URL") 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 = { private static let prereleaseAppcastURL: URL = {
guard let url = guard let url =
URL( URL(
string: "https://stats.store/api/v1/appcast/appcast-prerelease.xml" string: "https://stats.store/api/v1/appcast/appcast-prerelease.xml"
) )
else { else {
fatalError("Invalid prerelease appcast URL - this should never happen with a hardcoded URL") fatalError("Invalid prerelease appcast URL - this should never happen with a hardcoded URL")
} }

View file

@ -480,7 +480,7 @@ final class BunServer {
seconds: TimeInterval, seconds: TimeInterval,
operation: @escaping @Sendable () async -> T operation: @escaping @Sendable () async -> T
) )
async -> T? async -> T?
{ {
await withTaskGroup(of: T?.self) { group in await withTaskGroup(of: T?.self) { group in
group.addTask { group.addTask {

View file

@ -20,33 +20,33 @@ final class DashboardKeychain {
/// Get the dashboard password from keychain /// Get the dashboard password from keychain
func getPassword() -> String? { func getPassword() -> String? {
#if DEBUG #if DEBUG
// In debug builds, skip keychain access to avoid authorization dialogs // In debug builds, skip keychain access to avoid authorization dialogs
logger logger
.info( .info(
"Debug mode: Skipping keychain password retrieval. Password will only persist during current app session." "Debug mode: Skipping keychain password retrieval. Password will only persist during current app session."
) )
return nil return nil
#else #else
let query: [String: Any] = [ let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword, kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service, kSecAttrService as String: service,
kSecAttrAccount as String: account, kSecAttrAccount as String: account,
kSecReturnData as String: true kSecReturnData as String: true
] ]
var result: AnyObject? var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result) let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess, guard status == errSecSuccess,
let data = result as? Data, let data = result as? Data,
let password = String(data: data, encoding: .utf8) let password = String(data: data, encoding: .utf8)
else { else {
logger.debug("No password found in keychain") logger.debug("No password found in keychain")
return nil return nil
} }
logger.debug("Password retrieved from keychain") logger.debug("Password retrieved from keychain")
return password return password
#endif #endif
} }
@ -103,12 +103,12 @@ final class DashboardKeychain {
logger.info("Password \(success ? "saved to" : "failed to save to") keychain") logger.info("Password \(success ? "saved to" : "failed to save to") keychain")
#if DEBUG #if DEBUG
if success { if success {
logger logger
.info( .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." "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 #endif
return success return success

View file

@ -291,7 +291,7 @@ final class NgrokService: NgrokTunnelProtocol {
seconds: TimeInterval, seconds: TimeInterval,
operation: @Sendable @escaping () async throws -> T operation: @Sendable @escaping () async throws -> T
) )
async throws -> T async throws -> T
{ {
try await withThrowingTaskGroup(of: T.self) { group in try await withThrowingTaskGroup(of: T.self) { group in
group.addTask { group.addTask {

View file

@ -62,7 +62,7 @@ class ServerManager {
get { get {
let mode = DashboardAccessMode(rawValue: UserDefaults.standard.string(forKey: "dashboardAccessMode") ?? "" let mode = DashboardAccessMode(rawValue: UserDefaults.standard.string(forKey: "dashboardAccessMode") ?? ""
) ?? ) ??
.localhost .localhost
return mode.bindAddress return mode.bindAddress
} }
set { set {

View file

@ -52,38 +52,38 @@ public final class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate {
// Initialize Sparkle with standard configuration // Initialize Sparkle with standard configuration
#if DEBUG #if DEBUG
// In debug mode, start the updater for testing // In debug mode, start the updater for testing
updaterController = SPUStandardUpdaterController( updaterController = SPUStandardUpdaterController(
startingUpdater: true, startingUpdater: true,
updaterDelegate: self, updaterDelegate: self,
userDriverDelegate: userDriverDelegate userDriverDelegate: userDriverDelegate
) )
#else #else
updaterController = SPUStandardUpdaterController( updaterController = SPUStandardUpdaterController(
startingUpdater: true, startingUpdater: true,
updaterDelegate: self, updaterDelegate: self,
userDriverDelegate: userDriverDelegate userDriverDelegate: userDriverDelegate
) )
#endif #endif
// Configure automatic updates // Configure automatic updates
if let updater = updaterController?.updater { if let updater = updaterController?.updater {
#if DEBUG #if DEBUG
// Enable automatic checks in debug too // Enable automatic checks in debug too
updater.automaticallyChecksForUpdates = true updater.automaticallyChecksForUpdates = true
updater.automaticallyDownloadsUpdates = false updater.automaticallyDownloadsUpdates = false
logger.info("Sparkle updater initialized in DEBUG mode - automatic updates enabled for testing") logger.info("Sparkle updater initialized in DEBUG mode - automatic updates enabled for testing")
#else #else
// Enable automatic checking for updates // Enable automatic checking for updates
updater.automaticallyChecksForUpdates = true updater.automaticallyChecksForUpdates = true
// Enable automatic downloading of updates // Enable automatic downloading of updates
updater.automaticallyDownloadsUpdates = true updater.automaticallyDownloadsUpdates = true
// Set update check interval to 24 hours // Set update check interval to 24 hours
updater.updateCheckInterval = 86_400 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 #endif
// Start the updater for both debug and release builds // Start the updater for both debug and release builds

View file

@ -39,7 +39,7 @@ final class SparkleUserDriverDelegate: NSObject, @preconcurrency SPUStandardUser
_ update: SUAppcastItem, _ update: SUAppcastItem,
andInImmediateFocus immediateFocus: Bool andInImmediateFocus immediateFocus: Bool
) )
-> Bool -> Bool
{ {
logger.info("Should handle showing update: \(update.displayVersionString), immediate: \(immediateFocus)") logger.info("Should handle showing update: \(update.displayVersionString), immediate: \(immediateFocus)")
@ -211,7 +211,7 @@ final class SparkleUserDriverDelegate: NSObject, @preconcurrency SPUStandardUser
case "LATER_ACTION": case "LATER_ACTION":
logger.info("User tapped 'Remind Me Later' in notification") logger.info("User tapped 'Remind Me Later' in notification")
// The next reminder is already scheduled // The next reminder is already scheduled
default: default:
break break

View file

@ -134,7 +134,7 @@ actor TerminalManager {
seconds: TimeInterval, seconds: TimeInterval,
operation: @escaping @Sendable () async throws -> T operation: @escaping @Sendable () async throws -> T
) )
async throws -> T async throws -> T
{ {
try await withThrowingTaskGroup(of: T.self) { group in try await withThrowingTaskGroup(of: T.self) { group in
group.addTask { group.addTask {

View file

@ -150,7 +150,7 @@ final class WindowTracker {
tabReference: String?, tabReference: String?,
tabID: String? tabID: String?
) )
-> WindowInfo? -> WindowInfo?
{ {
let allWindows = Self.getAllTerminalWindows() let allWindows = Self.getAllTerminalWindows()

View file

@ -32,7 +32,7 @@ extension View {
error: Binding<Error?>, error: Binding<Error?>,
onDismiss: (() -> Void)? = nil onDismiss: (() -> Void)? = nil
) )
-> some View -> some View
{ {
modifier(ErrorAlertModifier(error: error, title: title, onDismiss: onDismiss)) modifier(ErrorAlertModifier(error: error, title: title, onDismiss: onDismiss))
} }
@ -49,7 +49,7 @@ extension Task where Failure == Error {
errorBinding: Binding<Error?>, errorBinding: Binding<Error?>,
operation: @escaping () async throws -> T operation: @escaping () async throws -> T
) )
-> Task<T, Error> -> Task<T, Error>
{ {
Task<T, Error>(priority: priority) { Task<T, Error>(priority: priority) {
do { do {

View file

@ -12,7 +12,7 @@ extension View {
horizontal: CGFloat = 16, horizontal: CGFloat = 16,
vertical: CGFloat = 14 vertical: CGFloat = 14
) )
-> some View -> some View
{ {
self self
.padding(.horizontal, horizontal) .padding(.horizontal, horizontal)

View file

@ -54,7 +54,7 @@ struct MenuBarView: View {
// Show Tutorial // Show Tutorial
Button(action: { Button(action: {
#if !SWIFT_PACKAGE #if !SWIFT_PACKAGE
AppDelegate.showWelcomeScreen() AppDelegate.showWelcomeScreen()
#endif #endif
}, label: { }, label: {
HStack { HStack {

View file

@ -327,7 +327,7 @@ private struct DeveloperToolsSection: View {
Spacer() Spacer()
Button("Show Welcome") { Button("Show Welcome") {
#if !SWIFT_PACKAGE #if !SWIFT_PACKAGE
AppDelegate.showWelcomeScreen() AppDelegate.showWelcomeScreen()
#endif #endif
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)

View file

@ -171,7 +171,7 @@ struct GlowingAppIcon: View {
enableInteraction: true, enableInteraction: true,
glowIntensity: 0.3 glowIntensity: 0.3
) { ) {
print("Icon clicked!") // Icon clicked - action handled here
} }
} }
.padding() .padding()

View file

@ -124,7 +124,7 @@ struct ProtectDashboardPageView: View {
// When password is set for the first time, automatically switch to network mode // When password is set for the first time, automatically switch to network mode
let currentMode = DashboardAccessMode(rawValue: UserDefaults.standard let currentMode = DashboardAccessMode(rawValue: UserDefaults.standard
.string(forKey: "dashboardAccessMode") ?? "" .string(forKey: "dashboardAccessMode") ?? ""
) ?? .localhost ) ?? .localhost
if currentMode == .localhost { if currentMode == .localhost {
UserDefaults.standard.set(DashboardAccessMode.network.rawValue, forKey: "dashboardAccessMode") UserDefaults.standard.set(DashboardAccessMode.network.rawValue, forKey: "dashboardAccessMode")

View file

@ -172,8 +172,8 @@ final class ApplicationMover {
logger.debug("ApplicationMover: hdiutil returned \(data.count) bytes") logger.debug("ApplicationMover: hdiutil returned \(data.count) bytes")
guard let plist = try PropertyListSerialization guard let plist = try PropertyListSerialization
.propertyList(from: data, options: [], format: nil) as? [String: Any], .propertyList(from: data, options: [], format: nil) as? [String: Any],
let images = plist["images"] as? [[String: Any]] let images = plist["images"] as? [[String: Any]]
else { else {
logger.debug("ApplicationMover: No disk images found in hdiutil output") logger.debug("ApplicationMover: No disk images found in hdiutil output")
return nil return nil

View file

@ -383,7 +383,7 @@ final class TerminalLauncher {
var runningTerminals: [Terminal] = [] var runningTerminals: [Terminal] = []
for terminal in Terminal.allCases for terminal in Terminal.allCases
where runningApps.contains(where: { $0.bundleIdentifier == terminal.bundleIdentifier }) where runningApps.contains(where: { $0.bundleIdentifier == terminal.bundleIdentifier })
{ {
runningTerminals.append(terminal) runningTerminals.append(terminal)
logger.debug("Detected running terminal: \(terminal.rawValue)") logger.debug("Detected running terminal: \(terminal.rawValue)")
@ -416,7 +416,7 @@ final class TerminalLauncher {
_ config: TerminalLaunchConfig, _ config: TerminalLaunchConfig,
sessionId: String? = nil sessionId: String? = nil
) )
throws -> TerminalLaunchResult throws -> TerminalLaunchResult
{ {
logger.debug("Launch config - command: \(config.command)") logger.debug("Launch config - command: \(config.command)")
logger.debug("Launch config - fullCommand: \(config.fullCommand)") logger.debug("Launch config - fullCommand: \(config.fullCommand)")
@ -519,7 +519,7 @@ final class TerminalLauncher {
if process.terminationStatus != 0 { if process.terminationStatus != 0 {
throw TerminalLauncherError throw TerminalLauncherError
.processLaunchFailed("Process exited with status \(process.terminationStatus)") .processLaunchFailed("Process exited with status \(process.terminationStatus)")
} }
} catch { } catch {
logger.error("Failed to launch terminal: \(error.localizedDescription)") logger.error("Failed to launch terminal: \(error.localizedDescription)")
@ -676,7 +676,7 @@ final class TerminalLauncher {
sessionId: String, sessionId: String,
vibetunnelPath: String? = nil vibetunnelPath: String? = nil
) )
throws throws
{ {
// Expand tilde in working directory path // Expand tilde in working directory path
let expandedWorkingDir = (workingDirectory as NSString).expandingTildeInPath let expandedWorkingDir = (workingDirectory as NSString).expandingTildeInPath
@ -801,7 +801,7 @@ final class TerminalLauncher {
workingDir: String, workingDir: String,
sessionId: String? = nil sessionId: String? = nil
) )
-> String -> String
{ {
// Bun executable has fwd command built-in // Bun executable has fwd command built-in
logger.info("Using Bun executable for session creation") logger.info("Using Bun executable for session creation")

View file

@ -21,81 +21,81 @@ struct VibeTunnelApp: App {
var body: some Scene { var body: some Scene {
#if os(macOS) #if os(macOS)
// Hidden WindowGroup to make Settings work in MenuBarExtra-only apps // Hidden WindowGroup to make Settings work in MenuBarExtra-only apps
// This is a workaround for FB10184971 // This is a workaround for FB10184971
WindowGroup("HiddenWindow") { WindowGroup("HiddenWindow") {
HiddenWindowView() HiddenWindowView()
} }
.windowResizability(.contentSize) .windowResizability(.contentSize)
.defaultSize(width: 1, height: 1) .defaultSize(width: 1, height: 1)
.windowStyle(.hiddenTitleBar) .windowStyle(.hiddenTitleBar)
// Welcome Window // Welcome Window
WindowGroup("Welcome", id: "welcome") { WindowGroup("Welcome", id: "welcome") {
WelcomeView() 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(sessionMonitor)
.environment(serverManager) .environment(serverManager)
.environment(ngrokService) .environment(ngrokService)
.environment(permissionManager) .environment(permissionManager)
.environment(terminalLauncher) .environment(terminalLauncher)
} else {
Text("Session not found")
.frame(width: 400, height: 300)
} }
.windowResizability(.contentSize) }
.defaultSize(width: 580, height: 480) .windowResizability(.contentSize)
.windowStyle(.hiddenTitleBar)
// Session Detail Window Settings {
WindowGroup("Session Details", id: "session-detail", for: String.self) { $sessionId in SettingsView()
if let sessionId, .environment(sessionMonitor)
let session = sessionMonitor.sessions[sessionId] .environment(serverManager)
{ .environment(ngrokService)
SessionDetailView(session: session) .environment(permissionManager)
.environment(sessionMonitor) .environment(terminalLauncher)
.environment(serverManager) }
.environment(ngrokService) .commands {
.environment(permissionManager) CommandGroup(after: .appInfo) {
.environment(terminalLauncher) Button("About VibeTunnel") {
} else { SettingsOpener.openSettings()
Text("Session not found") // Navigate to About tab after settings opens
.frame(width: 400, height: 300) Task {
} try? await Task.sleep(for: .milliseconds(100))
} NotificationCenter.default.post(
.windowResizability(.contentSize) 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 { MenuBarExtra {
MenuBarView() MenuBarView()
.environment(sessionMonitor) .environment(sessionMonitor)
.environment(serverManager) .environment(serverManager)
.environment(ngrokService) .environment(ngrokService)
.environment(permissionManager) .environment(permissionManager)
.environment(terminalLauncher) .environment(terminalLauncher)
} label: { } label: {
Image("menubar") Image("menubar")
.renderingMode(.template) .renderingMode(.template)
} }
#endif #endif
} }
} }
@ -121,25 +121,25 @@ final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUser
NSClassFromString("XCTestCase") != nil NSClassFromString("XCTestCase") != nil
let isRunningInPreview = processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" let isRunningInPreview = processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
#if DEBUG #if DEBUG
let isRunningInDebug = true let isRunningInDebug = true
#else #else
let isRunningInDebug = processInfo.environment["DYLD_INSERT_LIBRARIES"]? let isRunningInDebug = processInfo.environment["DYLD_INSERT_LIBRARIES"]?
.contains("libMainThreadChecker.dylib") ?? false || .contains("libMainThreadChecker.dylib") ?? false ||
processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] != nil processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] != nil
#endif #endif
// Handle single instance check before doing anything else // Handle single instance check before doing anything else
#if DEBUG #if DEBUG
// Skip single instance check in debug builds // Skip single instance check in debug builds
#else #else
if !isRunningInPreview && !isRunningInTests && !isRunningInDebug { if !isRunningInPreview && !isRunningInTests && !isRunningInDebug {
handleSingleInstanceCheck() handleSingleInstanceCheck()
registerForDistributedNotifications() registerForDistributedNotifications()
// Check if app needs to be moved to Applications folder // Check if app needs to be moved to Applications folder
let applicationMover = ApplicationMover() let applicationMover = ApplicationMover()
applicationMover.checkAndOfferToMoveToApplications() applicationMover.checkAndOfferToMoveToApplications()
} }
#endif #endif
// Initialize Sparkle updater manager // Initialize Sparkle updater manager
@ -340,11 +340,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUser
NSClassFromString("XCTestCase") != nil NSClassFromString("XCTestCase") != nil
let isRunningInPreview = processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" let isRunningInPreview = processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
#if DEBUG #if DEBUG
let isRunningInDebug = true let isRunningInDebug = true
#else #else
let isRunningInDebug = processInfo.environment["DYLD_INSERT_LIBRARIES"]? let isRunningInDebug = processInfo.environment["DYLD_INSERT_LIBRARIES"]?
.contains("libMainThreadChecker.dylib") ?? false || .contains("libMainThreadChecker.dylib") ?? false ||
processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] != nil processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] != nil
#endif #endif
// Skip cleanup during tests // Skip cleanup during tests
@ -374,13 +374,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUser
#if DEBUG #if DEBUG
// Skip removing observer in debug builds // Skip removing observer in debug builds
#else #else
if !isRunningInPreview && !isRunningInTests && !isRunningInDebug { if !isRunningInPreview && !isRunningInTests && !isRunningInDebug {
DistributedNotificationCenter.default().removeObserver( DistributedNotificationCenter.default().removeObserver(
self, self,
name: Self.showSettingsNotification, name: Self.showSettingsNotification,
object: nil object: nil
) )
} }
#endif #endif
// Remove update check notification observer // Remove update check notification observer