mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Fix settings window not appearing from menu bar
Replace deprecated showSettingsWindow: selector with NSApp.openSettings() to properly show the Settings window when clicking "Settings..." in the menu bar. This aligns with the correct SwiftUI pattern used in VibeMeter.
This commit is contained in:
parent
b80c710c08
commit
702b623d7f
20 changed files with 721 additions and 725 deletions
|
|
@ -10,7 +10,8 @@ let package = Package(
|
||||||
products: [
|
products: [
|
||||||
.library(
|
.library(
|
||||||
name: "VibeTunnel",
|
name: "VibeTunnel",
|
||||||
targets: ["VibeTunnel"])
|
targets: ["VibeTunnel"]
|
||||||
|
)
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/realm/SwiftLint.git", from: "0.57.0"),
|
.package(url: "https://github.com/realm/SwiftLint.git", from: "0.57.0"),
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -9,11 +9,6 @@
|
||||||
"filename" : "menubar@2x.png",
|
"filename" : "menubar@2x.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "2x"
|
"scale" : "2x"
|
||||||
},
|
|
||||||
{
|
|
||||||
"filename" : "menubar@3x.png",
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,8 @@
|
||||||
//
|
|
||||||
// TunnelSession.swift
|
|
||||||
// VibeTunnel
|
|
||||||
//
|
|
||||||
// Created by VibeTunnel on 15.06.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Hummingbird
|
import Hummingbird
|
||||||
|
|
||||||
/// Represents a terminal session that can be controlled remotely
|
/// Represents a terminal session that can be controlled remotely
|
||||||
public struct TunnelSession: Identifiable, Codable {
|
public struct TunnelSession: Identifiable, Codable, Sendable {
|
||||||
public let id: UUID
|
public let id: UUID
|
||||||
public let createdAt: Date
|
public let createdAt: Date
|
||||||
public var lastActivity: Date
|
public var lastActivity: Date
|
||||||
|
|
@ -43,7 +36,7 @@ public struct CreateSessionRequest: Codable {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response after creating a session
|
/// Response after creating a session
|
||||||
public struct CreateSessionResponse: Codable, ResponseEncodable {
|
public struct CreateSessionResponse: Codable, ResponseGenerator {
|
||||||
public let sessionId: String
|
public let sessionId: String
|
||||||
public let createdAt: Date
|
public let createdAt: Date
|
||||||
|
|
||||||
|
|
@ -69,14 +62,20 @@ public struct CommandRequest: Codable {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Command execution response
|
/// Command execution response
|
||||||
public struct CommandResponse: Codable, ResponseEncodable {
|
public struct CommandResponse: Codable, ResponseGenerator {
|
||||||
public let sessionId: String
|
public let sessionId: String
|
||||||
public let output: String?
|
public let output: String?
|
||||||
public let error: String?
|
public let error: String?
|
||||||
public let exitCode: Int32?
|
public let exitCode: Int32?
|
||||||
public let timestamp: Date
|
public let timestamp: Date
|
||||||
|
|
||||||
public init(sessionId: String, output: String? = nil, error: String? = nil, exitCode: Int32? = nil, timestamp: Date = Date()) {
|
public init(
|
||||||
|
sessionId: String,
|
||||||
|
output: String? = nil,
|
||||||
|
error: String? = nil,
|
||||||
|
exitCode: Int32? = nil,
|
||||||
|
timestamp: Date = Date()
|
||||||
|
) {
|
||||||
self.sessionId = sessionId
|
self.sessionId = sessionId
|
||||||
self.output = output
|
self.output = output
|
||||||
self.error = error
|
self.error = error
|
||||||
|
|
@ -86,7 +85,7 @@ public struct CommandResponse: Codable, ResponseEncodable {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Session information
|
/// Session information
|
||||||
public struct SessionInfo: Codable, ResponseEncodable {
|
public struct SessionInfo: Codable, ResponseGenerator {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let createdAt: Date
|
public let createdAt: Date
|
||||||
public let lastActivity: Date
|
public let lastActivity: Date
|
||||||
|
|
@ -101,7 +100,7 @@ public struct SessionInfo: Codable, ResponseEncodable {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List sessions response
|
/// List sessions response
|
||||||
public struct ListSessionsResponse: Codable, ResponseEncodable {
|
public struct ListSessionsResponse: Codable, ResponseGenerator {
|
||||||
public let sessions: [SessionInfo]
|
public let sessions: [SessionInfo]
|
||||||
|
|
||||||
public init(sessions: [SessionInfo]) {
|
public init(sessions: [SessionInfo]) {
|
||||||
|
|
|
||||||
|
|
@ -32,12 +32,16 @@ public enum UpdateChannel: String, CaseIterable, Codable, Sendable {
|
||||||
public var appcastURL: URL {
|
public var appcastURL: URL {
|
||||||
switch self {
|
switch self {
|
||||||
case .stable:
|
case .stable:
|
||||||
URL(string: "https://vibetunnel.sh/appcast.xml")!
|
Self.stableAppcastURL
|
||||||
case .prerelease:
|
case .prerelease:
|
||||||
URL(string: "https://vibetunnel.sh/appcast-prerelease.xml")!
|
Self.prereleaseAppcastURL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Static URLs to ensure they're validated at compile time
|
||||||
|
private static let stableAppcastURL = URL(string: "https://vibetunnel.sh/appcast.xml")!
|
||||||
|
private static let prereleaseAppcastURL = URL(string: "https://vibetunnel.sh/appcast-prerelease.xml")!
|
||||||
|
|
||||||
/// Whether this channel includes pre-release versions
|
/// Whether this channel includes pre-release versions
|
||||||
public var includesPreReleases: Bool {
|
public var includesPreReleases: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
|
|
@ -49,21 +53,21 @@ public enum UpdateChannel: String, CaseIterable, Codable, Sendable {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The current update channel based on user defaults
|
/// The current update channel based on user defaults
|
||||||
public static var current: UpdateChannel {
|
public static var current: Self {
|
||||||
if let rawValue = UserDefaults.standard.string(forKey: "updateChannel"),
|
if let rawValue = UserDefaults.standard.string(forKey: "updateChannel"),
|
||||||
let channel = UpdateChannel(rawValue: rawValue) {
|
let channel = Self(rawValue: rawValue) {
|
||||||
return channel
|
return channel
|
||||||
}
|
}
|
||||||
return defaultChannel
|
return defaultChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The default update channel based on the current app version
|
/// The default update channel based on the current app version
|
||||||
public static var defaultChannel: UpdateChannel {
|
public static var defaultChannel: Self {
|
||||||
defaultChannel(for: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0")
|
defaultChannel(for: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determines if the current app version suggests this channel should be default
|
/// Determines if the current app version suggests this channel should be default
|
||||||
public static func defaultChannel(for appVersion: String) -> UpdateChannel {
|
public static func defaultChannel(for appVersion: String) -> Self {
|
||||||
// First check if this build was marked as a pre-release during build time
|
// First check if this build was marked as a pre-release during build time
|
||||||
if let isPrereleaseValue = Bundle.main.object(forInfoDictionaryKey: "IS_PRERELEASE_BUILD"),
|
if let isPrereleaseValue = Bundle.main.object(forInfoDictionaryKey: "IS_PRERELEASE_BUILD"),
|
||||||
let isPrerelease = isPrereleaseValue as? Bool,
|
let isPrerelease = isPrereleaseValue as? Bool,
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,11 @@
|
||||||
//
|
import CryptoKit
|
||||||
// AuthenticationMiddleware.swift
|
|
||||||
// VibeTunnel
|
|
||||||
//
|
|
||||||
// Created by VibeTunnel on 15.06.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import HTTPTypes
|
||||||
import Hummingbird
|
import Hummingbird
|
||||||
import HummingbirdCore
|
import HummingbirdCore
|
||||||
import HTTPTypes
|
|
||||||
import Logging
|
import Logging
|
||||||
import CryptoKit
|
|
||||||
|
|
||||||
// Custom HTTP header name for API key
|
/// Custom HTTP header name for API key
|
||||||
extension HTTPField.Name {
|
extension HTTPField.Name {
|
||||||
static let xAPIKey = Self("X-API-Key")!
|
static let xAPIKey = Self("X-API-Key")!
|
||||||
}
|
}
|
||||||
|
|
@ -22,7 +15,7 @@ struct AuthenticationMiddleware<Context: RequestContext>: RouterMiddleware {
|
||||||
private let logger = Logger(label: "VibeTunnel.AuthMiddleware")
|
private let logger = Logger(label: "VibeTunnel.AuthMiddleware")
|
||||||
private let bearerPrefix = "Bearer "
|
private let bearerPrefix = "Bearer "
|
||||||
|
|
||||||
// In production, this should be stored securely and configurable
|
/// In production, this should be stored securely and configurable
|
||||||
private let validApiKeys: Set<String>
|
private let validApiKeys: Set<String>
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
|
@ -46,7 +39,12 @@ struct AuthenticationMiddleware<Context: RequestContext>: RouterMiddleware {
|
||||||
self.validApiKeys = apiKeys
|
self.validApiKeys = apiKeys
|
||||||
}
|
}
|
||||||
|
|
||||||
public func handle(_ request: Request, context: Context, next: (Request, Context) async throws -> Response) async throws -> Response {
|
func handle(
|
||||||
|
_ request: Request,
|
||||||
|
context: Context,
|
||||||
|
next: (Request, Context) async throws -> Response
|
||||||
|
)
|
||||||
|
async throws -> Response {
|
||||||
// Skip authentication for health check and WebSocket upgrade
|
// Skip authentication for health check and WebSocket upgrade
|
||||||
if request.uri.path == "/health" || request.headers[.upgrade] == "websocket" {
|
if request.uri.path == "/health" || request.headers[.upgrade] == "websocket" {
|
||||||
return try await next(request, context)
|
return try await next(request, context)
|
||||||
|
|
@ -90,7 +88,8 @@ enum APIKeyManager {
|
||||||
|
|
||||||
static func loadStoredAPIKeys() -> Set<String> {
|
static func loadStoredAPIKeys() -> Set<String> {
|
||||||
guard let data = UserDefaults.standard.data(forKey: apiKeyStorageKey),
|
guard let data = UserDefaults.standard.data(forKey: apiKeyStorageKey),
|
||||||
let keys = try? JSONDecoder().decode(Set<String>.self, from: data) else {
|
let keys = try? JSONDecoder().decode(Set<String>.self, from: data)
|
||||||
|
else {
|
||||||
// Generate and store a default key if none exists
|
// Generate and store a default key if none exists
|
||||||
let defaultKey = AuthenticationMiddleware<BasicRequestContext>.generateAPIKey()
|
let defaultKey = AuthenticationMiddleware<BasicRequestContext>.generateAPIKey()
|
||||||
let keys = Set([defaultKey])
|
let keys = Set([defaultKey])
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,8 @@ import UserNotifications
|
||||||
/// ```
|
/// ```
|
||||||
@MainActor
|
@MainActor
|
||||||
@Observable
|
@Observable
|
||||||
public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUserDriverDelegate, UNUserNotificationCenterDelegate {
|
public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUserDriverDelegate,
|
||||||
|
UNUserNotificationCenterDelegate {
|
||||||
// MARK: Initialization
|
// MARK: Initialization
|
||||||
|
|
||||||
private nonisolated static let staticLogger = Logger(subsystem: "com.amantus.vibetunnel", category: "updates")
|
private nonisolated static let staticLogger = Logger(subsystem: "com.amantus.vibetunnel", category: "updates")
|
||||||
|
|
@ -75,7 +76,7 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
||||||
|
|
||||||
/// Checks for updates immediately
|
/// Checks for updates immediately
|
||||||
func checkForUpdates() {
|
func checkForUpdates() {
|
||||||
guard let updaterController = updaterController else {
|
guard let updaterController else {
|
||||||
logger.warning("Updater controller not available")
|
logger.warning("Updater controller not available")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -205,7 +206,7 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show first reminder after 1 hour
|
// Show first reminder after 1 hour
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3600) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3_600) { [weak self] in
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
self?.showGentleUpdateReminder()
|
self?.showGentleUpdateReminder()
|
||||||
}
|
}
|
||||||
|
|
@ -232,9 +233,9 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provide the feed URL dynamically based on update channel
|
/// Provide the feed URL dynamically based on update channel
|
||||||
@objc public nonisolated func feedURLString(for updater: SPUUpdater) -> String? {
|
@objc public nonisolated func feedURLString(for updater: SPUUpdater) -> String? {
|
||||||
return UpdateChannel.current.appcastURL.absoluteString
|
UpdateChannel.current.appcastURL.absoluteString
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - SPUStandardUserDriverDelegate
|
// MARK: - SPUStandardUserDriverDelegate
|
||||||
|
|
@ -313,7 +314,7 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
||||||
|
|
||||||
// MARK: - KVO
|
// MARK: - KVO
|
||||||
|
|
||||||
public override func observeValue(
|
override public func observeValue(
|
||||||
forKeyPath keyPath: String?,
|
forKeyPath keyPath: String?,
|
||||||
of object: Any?,
|
of object: Any?,
|
||||||
change: [NSKeyValueChangeKey: Any]?,
|
change: [NSKeyValueChangeKey: Any]?,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import ServiceManagement
|
|
||||||
import os
|
import os
|
||||||
|
import ServiceManagement
|
||||||
|
|
||||||
/// Protocol defining the interface for managing launch at login functionality.
|
/// Protocol defining the interface for managing launch at login functionality.
|
||||||
@MainActor
|
@MainActor
|
||||||
|
|
@ -31,7 +31,10 @@ public struct StartupManager: StartupControlling {
|
||||||
logger.info("Successfully unregistered for launch at login.")
|
logger.info("Successfully unregistered for launch at login.")
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Failed to \(enabled ? "register" : "unregister") for launch at login: \(error.localizedDescription)")
|
logger
|
||||||
|
.error(
|
||||||
|
"Failed to \(enabled ? "register" : "unregister") for launch at login: \(error.localizedDescription)"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,6 @@
|
||||||
//
|
import Combine
|
||||||
// TerminalManager.swift
|
|
||||||
// VibeTunnel
|
|
||||||
//
|
|
||||||
// Created by VibeTunnel on 15.06.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Logging
|
import Logging
|
||||||
import Combine
|
|
||||||
|
|
||||||
/// Manages terminal sessions and command execution
|
/// Manages terminal sessions and command execution
|
||||||
actor TerminalManager {
|
actor TerminalManager {
|
||||||
|
|
@ -61,7 +54,8 @@ actor TerminalManager {
|
||||||
guard var session = sessions[sessionId],
|
guard var session = sessions[sessionId],
|
||||||
let process = processes[sessionId],
|
let process = processes[sessionId],
|
||||||
let (stdin, stdout, stderr) = pipes[sessionId],
|
let (stdin, stdout, stderr) = pipes[sessionId],
|
||||||
process.isRunning else {
|
process.isRunning
|
||||||
|
else {
|
||||||
throw TunnelError.sessionNotFound
|
throw TunnelError.sessionNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,7 +64,9 @@ actor TerminalManager {
|
||||||
sessions[sessionId] = session
|
sessions[sessionId] = session
|
||||||
|
|
||||||
// Send command to stdin
|
// Send command to stdin
|
||||||
let commandData = (command + "\n").data(using: .utf8)!
|
guard let commandData = (command + "\n").data(using: .utf8) else {
|
||||||
|
throw TunnelError.commandExecutionFailed("Failed to encode command")
|
||||||
|
}
|
||||||
stdin.fileHandleForWriting.write(commandData)
|
stdin.fileHandleForWriting.write(commandData)
|
||||||
|
|
||||||
// Read output with timeout
|
// Read output with timeout
|
||||||
|
|
@ -90,12 +86,12 @@ actor TerminalManager {
|
||||||
|
|
||||||
/// Get all active sessions
|
/// Get all active sessions
|
||||||
func listSessions() -> [TunnelSession] {
|
func listSessions() -> [TunnelSession] {
|
||||||
return Array(sessions.values)
|
Array(sessions.values)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a specific session
|
/// Get a specific session
|
||||||
func getSession(id: UUID) -> TunnelSession? {
|
func getSession(id: UUID) -> TunnelSession? {
|
||||||
return sessions[id]
|
sessions[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Close a session
|
/// Close a session
|
||||||
|
|
@ -122,7 +118,7 @@ actor TerminalManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function for timeout
|
/// Helper function for timeout
|
||||||
private func withTimeout<T>(seconds: TimeInterval, operation: @escaping () async throws -> T) async throws -> T {
|
private func withTimeout<T>(seconds: TimeInterval, operation: @escaping () 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 {
|
||||||
|
|
@ -134,7 +130,9 @@ actor TerminalManager {
|
||||||
throw TunnelError.timeout
|
throw TunnelError.timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = try await group.next()!
|
guard let result = try await group.next() else {
|
||||||
|
throw TunnelError.timeout
|
||||||
|
}
|
||||||
group.cancelAll()
|
group.cancelAll()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
@ -151,13 +149,13 @@ enum TunnelError: LocalizedError {
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .sessionNotFound:
|
case .sessionNotFound:
|
||||||
return "Session not found"
|
"Session not found"
|
||||||
case .commandExecutionFailed(let message):
|
case .commandExecutionFailed(let message):
|
||||||
return "Command execution failed: \(message)"
|
"Command execution failed: \(message)"
|
||||||
case .timeout:
|
case .timeout:
|
||||||
return "Operation timed out"
|
"Operation timed out"
|
||||||
case .invalidRequest:
|
case .invalidRequest:
|
||||||
return "Invalid request"
|
"Invalid request"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,5 @@
|
||||||
//
|
|
||||||
// TunnelClient.swift
|
|
||||||
// VibeTunnel
|
|
||||||
//
|
|
||||||
// Created by VibeTunnel on 15.06.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Combine
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
|
||||||
/// Client SDK for interacting with the VibeTunnel server
|
/// Client SDK for interacting with the VibeTunnel server
|
||||||
public class TunnelClient {
|
public class TunnelClient {
|
||||||
|
|
@ -43,9 +36,12 @@ public class TunnelClient {
|
||||||
|
|
||||||
// MARK: - Session Management
|
// MARK: - Session Management
|
||||||
|
|
||||||
public func createSession(workingDirectory: String? = nil,
|
public func createSession(
|
||||||
|
workingDirectory: String? = nil,
|
||||||
environment: [String: String]? = nil,
|
environment: [String: String]? = nil,
|
||||||
shell: String? = nil) async throws -> CreateSessionResponse {
|
shell: String? = nil
|
||||||
|
)
|
||||||
|
async throws -> CreateSessionResponse {
|
||||||
let url = baseURL.appendingPathComponent("sessions")
|
let url = baseURL.appendingPathComponent("sessions")
|
||||||
let request = CreateSessionRequest(
|
let request = CreateSessionRequest(
|
||||||
workingDirectory: workingDirectory,
|
workingDirectory: workingDirectory,
|
||||||
|
|
@ -74,7 +70,12 @@ public class TunnelClient {
|
||||||
|
|
||||||
// MARK: - Command Execution
|
// MARK: - Command Execution
|
||||||
|
|
||||||
public func executeCommand(sessionId: String, command: String, args: [String]? = nil) async throws -> CommandResponse {
|
public func executeCommand(
|
||||||
|
sessionId: String,
|
||||||
|
command: String,
|
||||||
|
args: [String]? = nil
|
||||||
|
)
|
||||||
|
async throws -> CommandResponse {
|
||||||
let url = baseURL.appendingPathComponent("execute")
|
let url = baseURL.appendingPathComponent("execute")
|
||||||
let request = CommandRequest(
|
let request = CommandRequest(
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
|
|
@ -113,7 +114,7 @@ public class TunnelClient {
|
||||||
return try decoder.decode(T.self, from: data)
|
return try decoder.decode(T.self, from: data)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func post<T: Encodable, R: Decodable>(to url: URL, body: T) async throws -> R {
|
private func post<R: Decodable>(to url: URL, body: some Encodable) async throws -> R {
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.httpMethod = "POST"
|
request.httpMethod = "POST"
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
|
@ -176,7 +177,7 @@ public class TunnelWebSocketClient: NSObject {
|
||||||
webSocketTask?.resume()
|
webSocketTask?.resume()
|
||||||
|
|
||||||
// Send initial connection message if session ID is provided
|
// Send initial connection message if session ID is provided
|
||||||
if let sessionId = sessionId {
|
if let sessionId {
|
||||||
send(WSMessage(type: .connect, sessionId: sessionId))
|
send(WSMessage(type: .connect, sessionId: sessionId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -185,7 +186,7 @@ public class TunnelWebSocketClient: NSObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func send(_ message: WSMessage) {
|
public func send(_ message: WSMessage) {
|
||||||
guard let webSocketTask = webSocketTask else { return }
|
guard let webSocketTask else { return }
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let data = try JSONEncoder().encode(message)
|
let data = try JSONEncoder().encode(message)
|
||||||
|
|
@ -193,7 +194,7 @@ public class TunnelWebSocketClient: NSObject {
|
||||||
let message = URLSessionWebSocketTask.Message.string(text)
|
let message = URLSessionWebSocketTask.Message.string(text)
|
||||||
|
|
||||||
webSocketTask.send(message) { error in
|
webSocketTask.send(message) { error in
|
||||||
if let error = error {
|
if let error {
|
||||||
print("WebSocket send error: \(error)")
|
print("WebSocket send error: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -203,7 +204,7 @@ public class TunnelWebSocketClient: NSObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sendCommand(_ command: String) {
|
public func sendCommand(_ command: String) {
|
||||||
guard let sessionId = sessionId else { return }
|
guard let sessionId else { return }
|
||||||
send(WSMessage(type: .command, sessionId: sessionId, data: command))
|
send(WSMessage(type: .command, sessionId: sessionId, data: command))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -242,11 +243,20 @@ public class TunnelWebSocketClient: NSObject {
|
||||||
// MARK: - URLSessionWebSocketDelegate
|
// MARK: - URLSessionWebSocketDelegate
|
||||||
|
|
||||||
extension TunnelWebSocketClient: URLSessionWebSocketDelegate {
|
extension TunnelWebSocketClient: URLSessionWebSocketDelegate {
|
||||||
public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
|
public func urlSession(
|
||||||
|
_ session: URLSession,
|
||||||
|
webSocketTask: URLSessionWebSocketTask,
|
||||||
|
didOpenWithProtocol protocol: String?
|
||||||
|
) {
|
||||||
print("WebSocket connected")
|
print("WebSocket connected")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
|
public func urlSession(
|
||||||
|
_ session: URLSession,
|
||||||
|
webSocketTask: URLSessionWebSocketTask,
|
||||||
|
didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
|
||||||
|
reason: Data?
|
||||||
|
) {
|
||||||
print("WebSocket disconnected")
|
print("WebSocket disconnected")
|
||||||
messageSubject.send(completion: .finished)
|
messageSubject.send(completion: .finished)
|
||||||
}
|
}
|
||||||
|
|
@ -262,11 +272,11 @@ public enum TunnelClientError: LocalizedError {
|
||||||
public var errorDescription: String? {
|
public var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .invalidResponse:
|
case .invalidResponse:
|
||||||
return "Invalid response from server"
|
"Invalid response from server"
|
||||||
case .httpError(let statusCode):
|
case .httpError(let statusCode):
|
||||||
return "HTTP error: \(statusCode)"
|
"HTTP error: \(statusCode)"
|
||||||
case .decodingError(let error):
|
case .decodingError(let error):
|
||||||
return "Decoding error: \(error.localizedDescription)"
|
"Decoding error: \(error.localizedDescription)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,24 +1,17 @@
|
||||||
//
|
|
||||||
// TunnelServer.swift
|
|
||||||
// VibeTunnel
|
|
||||||
//
|
|
||||||
// Created by VibeTunnel on 15.06.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import AppKit
|
import AppKit
|
||||||
import Combine
|
import Combine
|
||||||
import Logging
|
import Foundation
|
||||||
import os
|
import HTTPTypes
|
||||||
import Hummingbird
|
import Hummingbird
|
||||||
import HummingbirdCore
|
import HummingbirdCore
|
||||||
import HTTPTypes
|
import Logging
|
||||||
import NIOCore
|
import NIOCore
|
||||||
|
import os
|
||||||
|
|
||||||
// MARK: - Response Models
|
// MARK: - Response Models
|
||||||
|
|
||||||
/// Server info response
|
/// Server info response
|
||||||
struct ServerInfoResponse: Codable, ResponseEncodable {
|
struct ServerInfoResponse: Codable, ResponseGenerator {
|
||||||
let name: String
|
let name: String
|
||||||
let version: String
|
let version: String
|
||||||
let uptime: TimeInterval
|
let uptime: TimeInterval
|
||||||
|
|
@ -37,7 +30,7 @@ final class TunnelServer: ObservableObject {
|
||||||
@Published var lastError: Error?
|
@Published var lastError: Error?
|
||||||
@Published var connectedClients = 0
|
@Published var connectedClients = 0
|
||||||
|
|
||||||
init(port: Int = 8080) {
|
init(port: Int = 8_080) {
|
||||||
self.port = port
|
self.port = port
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,12 +116,12 @@ final class TunnelServer: ObservableObject {
|
||||||
|
|
||||||
private func configureRoutes(_ router: Router<BasicRequestContext>) {
|
private func configureRoutes(_ router: Router<BasicRequestContext>) {
|
||||||
// Health check endpoint
|
// Health check endpoint
|
||||||
router.get("/health") { request, context -> HTTPResponse.Status in
|
router.get("/health") { _, _ -> HTTPResponse.Status in
|
||||||
return .ok
|
.ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server info endpoint
|
// Server info endpoint
|
||||||
router.get("/info") { request, context async -> ServerInfoResponse in
|
router.get("/info") { _, _ async -> ServerInfoResponse in
|
||||||
let sessionCount = await self.terminalManager.listSessions().count
|
let sessionCount = await self.terminalManager.listSessions().count
|
||||||
return ServerInfoResponse(
|
return ServerInfoResponse(
|
||||||
name: "VibeTunnel",
|
name: "VibeTunnel",
|
||||||
|
|
@ -142,7 +135,7 @@ final class TunnelServer: ObservableObject {
|
||||||
let sessions = router.group("sessions")
|
let sessions = router.group("sessions")
|
||||||
|
|
||||||
// List all sessions
|
// List all sessions
|
||||||
sessions.get("/") { request, context async -> ListSessionsResponse in
|
sessions.get("/") { _, _ async -> ListSessionsResponse in
|
||||||
let sessions = await self.terminalManager.listSessions()
|
let sessions = await self.terminalManager.listSessions()
|
||||||
let sessionInfos = sessions.map { session in
|
let sessionInfos = sessions.map { session in
|
||||||
SessionInfo(
|
SessionInfo(
|
||||||
|
|
@ -167,10 +160,11 @@ final class TunnelServer: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get session info
|
// Get session info
|
||||||
sessions.get(":sessionId") { request, context async throws -> SessionInfo in
|
sessions.get(":sessionId") { _, context async throws -> SessionInfo in
|
||||||
guard let sessionIdString = context.parameters.get("sessionId", as: String.self),
|
guard let sessionIdString = context.parameters.get("sessionId", as: String.self),
|
||||||
let sessionId = UUID(uuidString: sessionIdString),
|
let sessionId = UUID(uuidString: sessionIdString),
|
||||||
let session = await self.terminalManager.getSession(id: sessionId) else {
|
let session = await self.terminalManager.getSession(id: sessionId)
|
||||||
|
else {
|
||||||
throw HTTPError(.notFound)
|
throw HTTPError(.notFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,9 +177,10 @@ final class TunnelServer: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close session
|
// Close session
|
||||||
sessions.delete(":sessionId") { request, context async throws -> HTTPResponse.Status in
|
sessions.delete(":sessionId") { _, context async throws -> HTTPResponse.Status in
|
||||||
guard let sessionIdString = context.parameters.get("sessionId", as: String.self),
|
guard let sessionIdString = context.parameters.get("sessionId", as: String.self),
|
||||||
let sessionId = UUID(uuidString: sessionIdString) else {
|
let sessionId = UUID(uuidString: sessionIdString)
|
||||||
|
else {
|
||||||
throw HTTPError(.badRequest)
|
throw HTTPError(.badRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,7 +214,6 @@ final class TunnelServer: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Integration with AppDelegate
|
// MARK: - Integration with AppDelegate
|
||||||
|
|
@ -229,7 +223,7 @@ extension AppDelegate {
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
let port = UserDefaults.standard.integer(forKey: "serverPort")
|
let port = UserDefaults.standard.integer(forKey: "serverPort")
|
||||||
let tunnelServer = TunnelServer(port: port > 0 ? port : 8080)
|
let tunnelServer = TunnelServer(port: port > 0 ? port : 8_080)
|
||||||
|
|
||||||
// Store reference if needed
|
// Store reference if needed
|
||||||
// self.tunnelServer = tunnelServer
|
// self.tunnelServer = tunnelServer
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,8 @@
|
||||||
//
|
|
||||||
// TunnelServerDemo.swift
|
|
||||||
// VibeTunnel
|
|
||||||
//
|
|
||||||
// Created by VibeTunnel on 15.06.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Combine
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
|
||||||
/// Demo code showing how to use the VibeTunnel server
|
/// Demo code showing how to use the VibeTunnel server
|
||||||
class TunnelServerDemo {
|
class TunnelServerDemo {
|
||||||
|
|
||||||
static func runDemo() async {
|
static func runDemo() async {
|
||||||
// Get the API key (in production, this should be managed securely)
|
// Get the API key (in production, this should be managed securely)
|
||||||
let apiKeys = APIKeyManager.loadStoredAPIKeys()
|
let apiKeys = APIKeyManager.loadStoredAPIKeys()
|
||||||
|
|
@ -50,7 +42,6 @@ class TunnelServerDemo {
|
||||||
// Close the session
|
// Close the session
|
||||||
try await client.closeSession(id: session.sessionId)
|
try await client.closeSession(id: session.sessionId)
|
||||||
print("Session closed")
|
print("Session closed")
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
print("Demo error: \(error)")
|
print("Demo error: \(error)")
|
||||||
}
|
}
|
||||||
|
|
@ -98,7 +89,6 @@ class TunnelServerDemo {
|
||||||
// Disconnect
|
// Disconnect
|
||||||
wsClient.disconnect()
|
wsClient.disconnect()
|
||||||
cancellable.cancel()
|
cancellable.cancel()
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
print("WebSocket demo error: \(error)")
|
print("WebSocket demo error: \(error)")
|
||||||
}
|
}
|
||||||
|
|
@ -107,45 +97,43 @@ class TunnelServerDemo {
|
||||||
|
|
||||||
// MARK: - cURL Examples
|
// MARK: - cURL Examples
|
||||||
|
|
||||||
/*
|
// Here are some example cURL commands to test the server:
|
||||||
Here are some example cURL commands to test the server:
|
//
|
||||||
|
// # Set your API key
|
||||||
# Set your API key
|
// export API_KEY="your-api-key-here"
|
||||||
export API_KEY="your-api-key-here"
|
//
|
||||||
|
// # Health check (no auth required)
|
||||||
# Health check (no auth required)
|
// curl http://localhost:8080/health
|
||||||
curl http://localhost:8080/health
|
//
|
||||||
|
// # Get server info
|
||||||
# Get server info
|
// curl -H "X-API-Key: $API_KEY" http://localhost:8080/info
|
||||||
curl -H "X-API-Key: $API_KEY" http://localhost:8080/info
|
//
|
||||||
|
// # Create a new session
|
||||||
# Create a new session
|
// curl -X POST http://localhost:8080/sessions \
|
||||||
curl -X POST http://localhost:8080/sessions \
|
// -H "X-API-Key: $API_KEY" \
|
||||||
-H "X-API-Key: $API_KEY" \
|
// -H "Content-Type: application/json" \
|
||||||
-H "Content-Type: application/json" \
|
// -d '{
|
||||||
-d '{
|
// "workingDirectory": "/tmp",
|
||||||
"workingDirectory": "/tmp",
|
// "shell": "/bin/zsh"
|
||||||
"shell": "/bin/zsh"
|
// }'
|
||||||
}'
|
//
|
||||||
|
// # List all sessions
|
||||||
# List all sessions
|
// curl -H "X-API-Key: $API_KEY" http://localhost:8080/sessions
|
||||||
curl -H "X-API-Key: $API_KEY" http://localhost:8080/sessions
|
//
|
||||||
|
// # Execute a command
|
||||||
# Execute a command
|
// curl -X POST http://localhost:8080/execute \
|
||||||
curl -X POST http://localhost:8080/execute \
|
// -H "X-API-Key: $API_KEY" \
|
||||||
-H "X-API-Key: $API_KEY" \
|
// -H "Content-Type: application/json" \
|
||||||
-H "Content-Type: application/json" \
|
// -d '{
|
||||||
-d '{
|
// "sessionId": "your-session-id",
|
||||||
"sessionId": "your-session-id",
|
// "command": "ls -la"
|
||||||
"command": "ls -la"
|
// }'
|
||||||
}'
|
//
|
||||||
|
// # Get session info
|
||||||
# Get session info
|
// curl -H "X-API-Key: $API_KEY" http://localhost:8080/sessions/your-session-id
|
||||||
curl -H "X-API-Key: $API_KEY" http://localhost:8080/sessions/your-session-id
|
//
|
||||||
|
// # Close a session
|
||||||
# Close a session
|
// curl -X DELETE -H "X-API-Key: $API_KEY" http://localhost:8080/sessions/your-session-id
|
||||||
curl -X DELETE -H "X-API-Key: $API_KEY" http://localhost:8080/sessions/your-session-id
|
//
|
||||||
|
// # WebSocket connection (using websocat tool)
|
||||||
# WebSocket connection (using websocat tool)
|
// websocat -H "X-API-Key: $API_KEY" ws://localhost:8080/ws/terminal
|
||||||
websocat -H "X-API-Key: $API_KEY" ws://localhost:8080/ws/terminal
|
|
||||||
*/
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,8 @@
|
||||||
//
|
|
||||||
// WebSocketHandler.swift
|
|
||||||
// VibeTunnel
|
|
||||||
//
|
|
||||||
// Created by VibeTunnel on 15.06.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Hummingbird
|
import Hummingbird
|
||||||
import HummingbirdCore
|
import HummingbirdCore
|
||||||
import NIOCore
|
import NIOCore
|
||||||
|
|
||||||
// import NIOWebSocket // TODO: This is available in swift-nio package
|
// import NIOWebSocket // TODO: This is available in swift-nio package
|
||||||
import Logging
|
import Logging
|
||||||
|
|
||||||
|
|
@ -39,161 +33,162 @@ public struct WSMessage: Codable {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Enable when HummingbirdWebSocket package is added
|
// TODO: Enable when HummingbirdWebSocket package is added
|
||||||
/*
|
// /// Handles WebSocket connections for real-time terminal communication
|
||||||
/// Handles WebSocket connections for real-time terminal communication
|
// final class WebSocketHandler {
|
||||||
final class WebSocketHandler {
|
// private let terminalManager: TerminalManager
|
||||||
private let terminalManager: TerminalManager
|
// private let logger = Logger(label: "VibeTunnel.WebSocketHandler")
|
||||||
private let logger = Logger(label: "VibeTunnel.WebSocketHandler")
|
// private var activeConnections: [UUID: WebSocketHandler.Connection] = [:]
|
||||||
private var activeConnections: [UUID: WebSocketHandler.Connection] = [:]
|
//
|
||||||
|
// init(terminalManager: TerminalManager) {
|
||||||
init(terminalManager: TerminalManager) {
|
// self.terminalManager = terminalManager
|
||||||
self.terminalManager = terminalManager
|
// }
|
||||||
}
|
//
|
||||||
|
// /// Handle incoming WebSocket connection
|
||||||
/// Handle incoming WebSocket connection
|
// func handle(ws: WebSocket, context: some RequestContext) async {
|
||||||
func handle(ws: WebSocket, context: some RequestContext) async {
|
// let connectionId = UUID()
|
||||||
let connectionId = UUID()
|
// let connection = Connection(id: connectionId, websocket: ws)
|
||||||
let connection = Connection(id: connectionId, websocket: ws)
|
//
|
||||||
|
// await MainActor.run {
|
||||||
await MainActor.run {
|
// activeConnections[connectionId] = connection
|
||||||
activeConnections[connectionId] = connection
|
// }
|
||||||
}
|
//
|
||||||
|
// logger.info("WebSocket connection established: \(connectionId)")
|
||||||
logger.info("WebSocket connection established: \(connectionId)")
|
//
|
||||||
|
// // Set up message handlers
|
||||||
// Set up message handlers
|
// ws.onText { [weak self] ws, text in
|
||||||
ws.onText { [weak self] ws, text in
|
// await self?.handleTextMessage(text, connection: connection)
|
||||||
await self?.handleTextMessage(text, connection: connection)
|
// }
|
||||||
}
|
//
|
||||||
|
// ws.onBinary { [weak self] ws, buffer in
|
||||||
ws.onBinary { [weak self] ws, buffer in
|
// // Handle binary data if needed
|
||||||
// Handle binary data if needed
|
// self?.logger.debug("Received binary data: \(buffer.readableBytes) bytes")
|
||||||
self?.logger.debug("Received binary data: \(buffer.readableBytes) bytes")
|
// }
|
||||||
}
|
//
|
||||||
|
// ws.onClose { [weak self] closeCode in
|
||||||
ws.onClose { [weak self] closeCode in
|
// await self?.handleClose(connection: connection)
|
||||||
await self?.handleClose(connection: connection)
|
// }
|
||||||
}
|
//
|
||||||
|
// // Send initial connection acknowledgment
|
||||||
// Send initial connection acknowledgment
|
// await sendMessage(WSMessage(type: .connect, data: "Connected to VibeTunnel"), to: connection)
|
||||||
await sendMessage(WSMessage(type: .connect, data: "Connected to VibeTunnel"), to: connection)
|
//
|
||||||
|
// // Keep connection alive with periodic pings
|
||||||
// Keep connection alive with periodic pings
|
// Task {
|
||||||
Task {
|
// while !Task.isCancelled && !connection.isClosed {
|
||||||
while !Task.isCancelled && !connection.isClosed {
|
// await sendMessage(WSMessage(type: .ping), to: connection)
|
||||||
await sendMessage(WSMessage(type: .ping), to: connection)
|
// try? await Task.sleep(nanoseconds: 30 * 1_000_000_000) // 30 seconds
|
||||||
try? await Task.sleep(nanoseconds: 30 * 1_000_000_000) // 30 seconds
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//
|
||||||
|
// private func handleTextMessage(_ text: String, connection: Connection) async {
|
||||||
private func handleTextMessage(_ text: String, connection: Connection) async {
|
// guard let data = text.data(using: .utf8),
|
||||||
guard let data = text.data(using: .utf8),
|
// let message = try? JSONDecoder().decode(WSMessage.self, from: data) else {
|
||||||
let message = try? JSONDecoder().decode(WSMessage.self, from: data) else {
|
// logger.error("Failed to decode WebSocket message: \(text)")
|
||||||
logger.error("Failed to decode WebSocket message: \(text)")
|
// await sendError("Invalid message format", to: connection)
|
||||||
await sendError("Invalid message format", to: connection)
|
// return
|
||||||
return
|
// }
|
||||||
}
|
//
|
||||||
|
// switch message.type {
|
||||||
switch message.type {
|
// case .connect:
|
||||||
case .connect:
|
// // Handle session connection
|
||||||
// Handle session connection
|
// if let sessionId = message.sessionId,
|
||||||
if let sessionId = message.sessionId,
|
// let uuid = UUID(uuidString: sessionId) {
|
||||||
let uuid = UUID(uuidString: sessionId) {
|
// connection.sessionId = uuid
|
||||||
connection.sessionId = uuid
|
// await sendMessage(WSMessage(type: .output, sessionId: sessionId, data: "Session connected"), to:
|
||||||
await sendMessage(WSMessage(type: .output, sessionId: sessionId, data: "Session connected"), to: connection)
|
// connection)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
case .command:
|
// case .command:
|
||||||
// Execute command in terminal session
|
// // Execute command in terminal session
|
||||||
guard let sessionId = connection.sessionId,
|
// guard let sessionId = connection.sessionId,
|
||||||
let command = message.data else {
|
// let command = message.data else {
|
||||||
await sendError("Session ID and command required", to: connection)
|
// await sendError("Session ID and command required", to: connection)
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
do {
|
// do {
|
||||||
let (output, error) = try await terminalManager.executeCommand(sessionId: sessionId, command: command)
|
// let (output, error) = try await terminalManager.executeCommand(sessionId: sessionId, command: command)
|
||||||
|
//
|
||||||
if !output.isEmpty {
|
// if !output.isEmpty {
|
||||||
await sendMessage(WSMessage(type: .output, sessionId: sessionId.uuidString, data: output), to: connection)
|
// await sendMessage(WSMessage(type: .output, sessionId: sessionId.uuidString, data: output), to:
|
||||||
}
|
// connection)
|
||||||
|
// }
|
||||||
if !error.isEmpty {
|
//
|
||||||
await sendMessage(WSMessage(type: .error, sessionId: sessionId.uuidString, data: error), to: connection)
|
// if !error.isEmpty {
|
||||||
}
|
// await sendMessage(WSMessage(type: .error, sessionId: sessionId.uuidString, data: error), to:
|
||||||
} catch {
|
// connection)
|
||||||
await sendError(error.localizedDescription, to: connection)
|
// }
|
||||||
}
|
// } catch {
|
||||||
|
// await sendError(error.localizedDescription, to: connection)
|
||||||
case .ping:
|
// }
|
||||||
// Respond to ping with pong
|
//
|
||||||
await sendMessage(WSMessage(type: .pong), to: connection)
|
// case .ping:
|
||||||
|
// // Respond to ping with pong
|
||||||
case .close:
|
// await sendMessage(WSMessage(type: .pong), to: connection)
|
||||||
// Close the session
|
//
|
||||||
if let sessionId = connection.sessionId {
|
// case .close:
|
||||||
await terminalManager.closeSession(id: sessionId)
|
// // Close the session
|
||||||
}
|
// if let sessionId = connection.sessionId {
|
||||||
try? await connection.websocket.close()
|
// await terminalManager.closeSession(id: sessionId)
|
||||||
|
// }
|
||||||
default:
|
// try? await connection.websocket.close()
|
||||||
logger.warning("Unhandled message type: \(message.type)")
|
//
|
||||||
}
|
// default:
|
||||||
}
|
// logger.warning("Unhandled message type: \(message.type)")
|
||||||
|
// }
|
||||||
private func handleClose(connection: Connection) async {
|
// }
|
||||||
logger.info("WebSocket connection closed: \(connection.id)")
|
//
|
||||||
|
// private func handleClose(connection: Connection) async {
|
||||||
await MainActor.run {
|
// logger.info("WebSocket connection closed: \(connection.id)")
|
||||||
activeConnections.removeValue(forKey: connection.id)
|
//
|
||||||
}
|
// await MainActor.run {
|
||||||
|
// activeConnections.removeValue(forKey: connection.id)
|
||||||
// Clean up associated session if any
|
// }
|
||||||
if let sessionId = connection.sessionId {
|
//
|
||||||
await terminalManager.closeSession(id: sessionId)
|
// // Clean up associated session if any
|
||||||
}
|
// if let sessionId = connection.sessionId {
|
||||||
|
// await terminalManager.closeSession(id: sessionId)
|
||||||
connection.isClosed = true
|
// }
|
||||||
}
|
//
|
||||||
|
// connection.isClosed = true
|
||||||
private func sendMessage(_ message: WSMessage, to connection: Connection) async {
|
// }
|
||||||
do {
|
//
|
||||||
let data = try JSONEncoder().encode(message)
|
// private func sendMessage(_ message: WSMessage, to connection: Connection) async {
|
||||||
let text = String(data: data, encoding: .utf8) ?? "{}"
|
// do {
|
||||||
try await connection.websocket.send(text: text)
|
// let data = try JSONEncoder().encode(message)
|
||||||
} catch {
|
// let text = String(data: data, encoding: .utf8) ?? "{}"
|
||||||
logger.error("Failed to send WebSocket message: \(error)")
|
// try await connection.websocket.send(text: text)
|
||||||
}
|
// } catch {
|
||||||
}
|
// logger.error("Failed to send WebSocket message: \(error)")
|
||||||
|
// }
|
||||||
private func sendError(_ error: String, to connection: Connection) async {
|
// }
|
||||||
await sendMessage(WSMessage(type: .error, data: error), to: connection)
|
//
|
||||||
}
|
// private func sendError(_ error: String, to connection: Connection) async {
|
||||||
|
// await sendMessage(WSMessage(type: .error, data: error), to: connection)
|
||||||
/// WebSocket connection wrapper
|
// }
|
||||||
class Connection {
|
//
|
||||||
let id: UUID
|
// /// WebSocket connection wrapper
|
||||||
let websocket: WebSocket
|
// class Connection {
|
||||||
var sessionId: UUID?
|
// let id: UUID
|
||||||
var isClosed = false
|
// let websocket: WebSocket
|
||||||
|
// var sessionId: UUID?
|
||||||
init(id: UUID, websocket: WebSocket) {
|
// var isClosed = false
|
||||||
self.id = id
|
//
|
||||||
self.websocket = websocket
|
// init(id: UUID, websocket: WebSocket) {
|
||||||
}
|
// self.id = id
|
||||||
}
|
// self.websocket = websocket
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
/// Extension to add WebSocket routes to the router
|
// }
|
||||||
extension RouterBuilder {
|
//
|
||||||
mutating func addWebSocketRoutes(terminalManager: TerminalManager) {
|
// /// Extension to add WebSocket routes to the router
|
||||||
let wsHandler = WebSocketHandler(terminalManager: terminalManager)
|
// extension RouterBuilder {
|
||||||
|
// mutating func addWebSocketRoutes(terminalManager: TerminalManager) {
|
||||||
// WebSocket endpoint for terminal streaming
|
// let wsHandler = WebSocketHandler(terminalManager: terminalManager)
|
||||||
ws("/ws/terminal") { request, ws, context in
|
//
|
||||||
await wsHandler.handle(ws: ws, context: context)
|
// // WebSocket endpoint for terminal streaming
|
||||||
}
|
// ws("/ws/terminal") { request, ws, context in
|
||||||
}
|
// await wsHandler.handle(ws: ws, context: context)
|
||||||
}
|
// }
|
||||||
*/
|
// }
|
||||||
|
// }
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@ struct MaterialBackgroundModifier: ViewModifier {
|
||||||
content
|
content
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: cornerRadius)
|
RoundedRectangle(cornerRadius: cornerRadius)
|
||||||
.fill(material))
|
.fill(material)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,7 +51,8 @@ struct CardStyleModifier: ViewModifier {
|
||||||
init(
|
init(
|
||||||
cornerRadius: CGFloat = 10,
|
cornerRadius: CGFloat = 10,
|
||||||
horizontalPadding: CGFloat = 14,
|
horizontalPadding: CGFloat = 14,
|
||||||
verticalPadding: CGFloat = 10) {
|
verticalPadding: CGFloat = 10
|
||||||
|
) {
|
||||||
self.cornerRadius = cornerRadius
|
self.cornerRadius = cornerRadius
|
||||||
self.horizontalPadding = horizontalPadding
|
self.horizontalPadding = horizontalPadding
|
||||||
self.verticalPadding = verticalPadding
|
self.verticalPadding = verticalPadding
|
||||||
|
|
@ -66,15 +68,17 @@ struct CardStyleModifier: ViewModifier {
|
||||||
|
|
||||||
// MARK: - View Extensions
|
// MARK: - View Extensions
|
||||||
|
|
||||||
public extension View {
|
extension View {
|
||||||
/// Applies a material background with rounded corners.
|
/// Applies a material background with rounded corners.
|
||||||
///
|
///
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - cornerRadius: Corner radius for the rounded rectangle (default: 10)
|
/// - cornerRadius: Corner radius for the rounded rectangle (default: 10)
|
||||||
/// - material: Material type to use (default: .thickMaterial)
|
/// - material: Material type to use (default: .thickMaterial)
|
||||||
func materialBackground(
|
public func materialBackground(
|
||||||
cornerRadius: CGFloat = 10,
|
cornerRadius: CGFloat = 10,
|
||||||
material: Material = .thickMaterial) -> some View {
|
material: Material = .thickMaterial
|
||||||
|
)
|
||||||
|
-> some View {
|
||||||
modifier(MaterialBackgroundModifier(cornerRadius: cornerRadius, material: material))
|
modifier(MaterialBackgroundModifier(cornerRadius: cornerRadius, material: material))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,9 +87,11 @@ public extension View {
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - horizontal: Horizontal padding (default: 16)
|
/// - horizontal: Horizontal padding (default: 16)
|
||||||
/// - vertical: Vertical padding (default: 14)
|
/// - vertical: Vertical padding (default: 14)
|
||||||
func standardPadding(
|
public func standardPadding(
|
||||||
horizontal: CGFloat = 16,
|
horizontal: CGFloat = 16,
|
||||||
vertical: CGFloat = 14) -> some View {
|
vertical: CGFloat = 14
|
||||||
|
)
|
||||||
|
-> some View {
|
||||||
modifier(StandardPaddingModifier(horizontal: horizontal, vertical: vertical))
|
modifier(StandardPaddingModifier(horizontal: horizontal, vertical: vertical))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,14 +101,17 @@ public extension View {
|
||||||
/// - cornerRadius: Corner radius for the card (default: 10)
|
/// - cornerRadius: Corner radius for the card (default: 10)
|
||||||
/// - horizontalPadding: Horizontal padding (default: 14)
|
/// - horizontalPadding: Horizontal padding (default: 14)
|
||||||
/// - verticalPadding: Vertical padding (default: 10)
|
/// - verticalPadding: Vertical padding (default: 10)
|
||||||
func cardStyle(
|
public func cardStyle(
|
||||||
cornerRadius: CGFloat = 10,
|
cornerRadius: CGFloat = 10,
|
||||||
horizontalPadding: CGFloat = 14,
|
horizontalPadding: CGFloat = 14,
|
||||||
verticalPadding: CGFloat = 10) -> some View {
|
verticalPadding: CGFloat = 10
|
||||||
|
)
|
||||||
|
-> some View {
|
||||||
modifier(CardStyleModifier(
|
modifier(CardStyleModifier(
|
||||||
cornerRadius: cornerRadius,
|
cornerRadius: cornerRadius,
|
||||||
horizontalPadding: horizontalPadding,
|
horizontalPadding: horizontalPadding,
|
||||||
verticalPadding: verticalPadding))
|
verticalPadding: verticalPadding
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@ struct PressEventModifier: ViewModifier {
|
||||||
.simultaneousGesture(
|
.simultaneousGesture(
|
||||||
DragGesture(minimumDistance: 0)
|
DragGesture(minimumDistance: 0)
|
||||||
.onChanged { _ in onPress() }
|
.onChanged { _ in onPress() }
|
||||||
.onEnded { _ in onRelease() })
|
.onEnded { _ in onRelease() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,7 +31,8 @@ struct PointingHandCursorModifier: ViewModifier {
|
||||||
content
|
content
|
||||||
.background(
|
.background(
|
||||||
CursorTrackingView()
|
CursorTrackingView()
|
||||||
.allowsHitTesting(false))
|
.allowsHitTesting(false)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,8 @@ struct AboutView: View {
|
||||||
HoverableLink(
|
HoverableLink(
|
||||||
url: "https://github.com/amantus-ai/vibetunnel/issues",
|
url: "https://github.com/amantus-ai/vibetunnel/issues",
|
||||||
title: "Report an Issue",
|
title: "Report an Issue",
|
||||||
icon: "exclamationmark.bubble")
|
icon: "exclamationmark.bubble"
|
||||||
|
)
|
||||||
HoverableLink(url: "https://x.com/steipete", title: "Follow @steipete on Twitter", icon: "bird")
|
HoverableLink(url: "https://x.com/steipete", title: "Follow @steipete on Twitter", icon: "bird")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -87,8 +88,12 @@ struct HoverableLink: View {
|
||||||
@State
|
@State
|
||||||
private var isHovering = false
|
private var isHovering = false
|
||||||
|
|
||||||
|
private var destinationURL: URL {
|
||||||
|
URL(string: url) ?? URL(fileURLWithPath: "/")
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Link(destination: URL(string: url)!) {
|
Link(destination: destinationURL) {
|
||||||
Label(title, systemImage: icon)
|
Label(title, systemImage: icon)
|
||||||
.underline(isHovering, color: .accentColor)
|
.underline(isHovering, color: .accentColor)
|
||||||
}
|
}
|
||||||
|
|
@ -126,7 +131,8 @@ struct InteractiveAppIcon: View {
|
||||||
color: shadowColor,
|
color: shadowColor,
|
||||||
radius: shadowRadius,
|
radius: shadowRadius,
|
||||||
x: 0,
|
x: 0,
|
||||||
y: shadowOffset)
|
y: shadowOffset
|
||||||
|
)
|
||||||
.animation(.easeInOut(duration: 0.2), value: isHovering)
|
.animation(.easeInOut(duration: 0.2), value: isHovering)
|
||||||
.animation(.easeInOut(duration: 0.1), value: isPressed)
|
.animation(.easeInOut(duration: 0.1), value: isPressed)
|
||||||
|
|
||||||
|
|
@ -144,7 +150,8 @@ struct InteractiveAppIcon: View {
|
||||||
}
|
}
|
||||||
.pressEvents(
|
.pressEvents(
|
||||||
onPress: { isPressed = true },
|
onPress: { isPressed = true },
|
||||||
onRelease: { isPressed = false })
|
onRelease: { isPressed = false }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var shadowColor: Color {
|
private var shadowColor: Color {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,3 @@
|
||||||
//
|
|
||||||
// SettingsView.swift
|
|
||||||
// VibeTunnel
|
|
||||||
//
|
|
||||||
// Created by Peter Steinberger on 15.06.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
enum SettingsTab: String, CaseIterable {
|
enum SettingsTab: String, CaseIterable {
|
||||||
|
|
@ -14,17 +7,17 @@ enum SettingsTab: String, CaseIterable {
|
||||||
|
|
||||||
var displayName: String {
|
var displayName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .general: return "General"
|
case .general: "General"
|
||||||
case .advanced: return "Advanced"
|
case .advanced: "Advanced"
|
||||||
case .about: return "About"
|
case .about: "About"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var icon: String {
|
var icon: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .general: return "gear"
|
case .general: "gear"
|
||||||
case .advanced: return "gearshape.2"
|
case .advanced: "gearshape.2"
|
||||||
case .about: return "info.circle"
|
case .about: "info.circle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -125,7 +118,8 @@ struct GeneralSettingsView: View {
|
||||||
set: { newValue in
|
set: { newValue in
|
||||||
autostart = newValue
|
autostart = newValue
|
||||||
startupManager.setLaunchAtLogin(enabled: newValue)
|
startupManager.setLaunchAtLogin(enabled: newValue)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var showInDockBinding: Binding<Bool> {
|
private var showInDockBinding: Binding<Bool> {
|
||||||
|
|
@ -134,7 +128,8 @@ struct GeneralSettingsView: View {
|
||||||
set: { newValue in
|
set: { newValue in
|
||||||
showInDock = newValue
|
showInDock = newValue
|
||||||
NSApp.setActivationPolicy(newValue ? .regular : .accessory)
|
NSApp.setActivationPolicy(newValue ? .regular : .accessory)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,7 +142,7 @@ struct AdvancedSettingsView: View {
|
||||||
@StateObject private var tunnelServer: TunnelServer
|
@StateObject private var tunnelServer: TunnelServer
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
let port = Int(UserDefaults.standard.string(forKey: "serverPort") ?? "8080") ?? 8080
|
let port = Int(UserDefaults.standard.string(forKey: "serverPort") ?? "8080") ?? 8_080
|
||||||
_tunnelServer = StateObject(wrappedValue: TunnelServer(port: port))
|
_tunnelServer = StateObject(wrappedValue: TunnelServer(port: port))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -213,7 +208,9 @@ struct AdvancedSettingsView: View {
|
||||||
.frame(width: 8, height: 8)
|
.frame(width: 8, height: 8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text(tunnelServer.isRunning ? "Server is running on port \(serverPort)" : "Server is stopped")
|
Text(tunnelServer
|
||||||
|
.isRunning ? "Server is running on port \(serverPort)" : "Server is stopped"
|
||||||
|
)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
@ -227,8 +224,8 @@ struct AdvancedSettingsView: View {
|
||||||
.tint(tunnelServer.isRunning ? .red : .blue)
|
.tint(tunnelServer.isRunning ? .red : .blue)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tunnelServer.isRunning {
|
if tunnelServer.isRunning, let serverURL = URL(string: "http://localhost:\(serverPort)") {
|
||||||
Link("Open in Browser", destination: URL(string: "http://localhost:\(serverPort)")!)
|
Link("Open in Browser", destination: serverURL)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -279,7 +276,8 @@ struct AdvancedSettingsView: View {
|
||||||
object: nil,
|
object: nil,
|
||||||
userInfo: ["channel": newValue]
|
userInfo: ["channel": newValue]
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func checkForUpdates() {
|
private func checkForUpdates() {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import SwiftUI
|
|
||||||
import AppKit
|
import AppKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
/// Window controller for the About window
|
/// Window controller for the About window
|
||||||
final class AboutWindowController {
|
final class AboutWindowController {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,3 @@
|
||||||
//
|
|
||||||
// NSApplication+OpenSettings.swift
|
|
||||||
// VibeTunnel
|
|
||||||
//
|
|
||||||
// Created by Peter Steinberger on 15.06.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
extension NSApplication {
|
extension NSApplication {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,5 @@
|
||||||
//
|
|
||||||
// VibeTunnelApp.swift
|
|
||||||
// VibeTunnel
|
|
||||||
//
|
|
||||||
// Created by Peter Steinberger on 15.06.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import AppKit
|
import AppKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct VibeTunnelApp: App {
|
struct VibeTunnelApp: App {
|
||||||
|
|
@ -43,7 +36,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
let processInfo = ProcessInfo.processInfo
|
let processInfo = ProcessInfo.processInfo
|
||||||
let isRunningInTests = processInfo.environment["XCTestConfigurationFilePath"] != nil
|
let isRunningInTests = processInfo.environment["XCTestConfigurationFilePath"] != nil
|
||||||
let isRunningInPreview = processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
|
let isRunningInPreview = processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
|
||||||
let isRunningInDebug = processInfo.environment["DYLD_INSERT_LIBRARIES"]?.contains("libMainThreadChecker.dylib") ?? false
|
let isRunningInDebug = processInfo.environment["DYLD_INSERT_LIBRARIES"]?
|
||||||
|
.contains("libMainThreadChecker.dylib") ?? false
|
||||||
|
|
||||||
// Handle single instance check before doing anything else
|
// Handle single instance check before doing anything else
|
||||||
if !isRunningInPreview, !isRunningInTests, !isRunningInDebug {
|
if !isRunningInPreview, !isRunningInTests, !isRunningInDebug {
|
||||||
|
|
@ -66,7 +60,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
// For menu bar apps, we need to ensure the settings window is accessible
|
// For menu bar apps, we need to ensure the settings window is accessible
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
if NSApp.windows.isEmpty || NSApp.windows.allSatisfy({ !$0.isVisible }) {
|
if NSApp.windows.isEmpty || NSApp.windows.allSatisfy({ !$0.isVisible }) {
|
||||||
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
|
NSApp.openSettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -76,7 +70,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
self,
|
self,
|
||||||
selector: #selector(handleCheckForUpdatesNotification),
|
selector: #selector(handleCheckForUpdatesNotification),
|
||||||
name: Notification.Name("checkForUpdates"),
|
name: Notification.Name("checkForUpdates"),
|
||||||
object: nil)
|
object: nil
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleSingleInstanceCheck() {
|
private func handleSingleInstanceCheck() {
|
||||||
|
|
@ -91,7 +86,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let alert = NSAlert()
|
let alert = NSAlert()
|
||||||
alert.messageText = "VibeTunnel is already running"
|
alert.messageText = "VibeTunnel is already running"
|
||||||
alert.informativeText = "Another instance of VibeTunnel is already running. This instance will now quit."
|
alert
|
||||||
|
.informativeText = "Another instance of VibeTunnel is already running. This instance will now quit."
|
||||||
alert.alertStyle = .informational
|
alert.alertStyle = .informational
|
||||||
alert.addButton(withTitle: "OK")
|
alert.addButton(withTitle: "OK")
|
||||||
alert.runModal()
|
alert.runModal()
|
||||||
|
|
@ -108,14 +104,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
self,
|
self,
|
||||||
selector: #selector(handleShowSettingsNotification),
|
selector: #selector(handleShowSettingsNotification),
|
||||||
name: Self.showSettingsNotification,
|
name: Self.showSettingsNotification,
|
||||||
object: nil)
|
object: nil
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows the Settings window when another VibeTunnel instance asks us to.
|
/// Shows the Settings window when another VibeTunnel instance asks us to.
|
||||||
@objc
|
@objc
|
||||||
private func handleShowSettingsNotification(_ notification: Notification) {
|
private func handleShowSettingsNotification(_ notification: Notification) {
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
|
NSApp.openSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func handleCheckForUpdatesNotification() {
|
@objc private func handleCheckForUpdatesNotification() {
|
||||||
|
|
@ -127,20 +124,23 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
let processInfo = ProcessInfo.processInfo
|
let processInfo = ProcessInfo.processInfo
|
||||||
let isRunningInTests = processInfo.environment["XCTestConfigurationFilePath"] != nil
|
let isRunningInTests = processInfo.environment["XCTestConfigurationFilePath"] != nil
|
||||||
let isRunningInPreview = processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
|
let isRunningInPreview = processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
|
||||||
let isRunningInDebug = processInfo.environment["DYLD_INSERT_LIBRARIES"]?.contains("libMainThreadChecker.dylib") ?? false
|
let isRunningInDebug = processInfo.environment["DYLD_INSERT_LIBRARIES"]?
|
||||||
|
.contains("libMainThreadChecker.dylib") ?? false
|
||||||
|
|
||||||
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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove update check notification observer
|
// Remove update check notification observer
|
||||||
NotificationCenter.default.removeObserver(
|
NotificationCenter.default.removeObserver(
|
||||||
self,
|
self,
|
||||||
name: Notification.Name("checkForUpdates"),
|
name: Notification.Name("checkForUpdates"),
|
||||||
object: nil)
|
object: nil
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Status Item
|
// MARK: - Status Item
|
||||||
|
|
@ -172,7 +172,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func showSettings() {
|
@objc private func showSettings() {
|
||||||
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
|
NSApp.openSettings()
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue