mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Fix server port binding issue and refactor tunnel services
- Change default server port from 800 to 4020 to avoid privileged port restrictions - Rename TunnelServerDemo.swift to TunnelServer.swift for clarity - Remove redundant TunnelServerExample.swift - Update tty-fwd binary - Add SettingsWindowDelegate for improved window management - Update UI components and session monitoring - Add modern Swift refactoring documentation The server was failing to start because it was trying to bind to port 800, which requires root privileges on macOS. Port 4020 is now used as the default.
This commit is contained in:
parent
223fa898b1
commit
ed19be138b
11 changed files with 210 additions and 165 deletions
Binary file not shown.
|
|
@ -1,4 +1,5 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Observation
|
||||||
import os.log
|
import os.log
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
|
||||||
|
|
@ -39,14 +40,15 @@ public final class SparkleUpdaterManager: NSObject {
|
||||||
/// Stub implementation of SparkleViewModel
|
/// Stub implementation of SparkleViewModel
|
||||||
@MainActor
|
@MainActor
|
||||||
@available(macOS 10.15, *)
|
@available(macOS 10.15, *)
|
||||||
public final class SparkleViewModel: ObservableObject {
|
@Observable
|
||||||
@Published public var canCheckForUpdates = false
|
public final class SparkleViewModel {
|
||||||
@Published public var isCheckingForUpdates = false
|
public var canCheckForUpdates = false
|
||||||
@Published public var automaticallyChecksForUpdates = true
|
public var isCheckingForUpdates = false
|
||||||
@Published public var automaticallyDownloadsUpdates = false
|
public var automaticallyChecksForUpdates = true
|
||||||
@Published public var updateCheckInterval: TimeInterval = 86400
|
public var automaticallyDownloadsUpdates = false
|
||||||
@Published public var lastUpdateCheckDate: Date?
|
public var updateCheckInterval: TimeInterval = 86400
|
||||||
@Published public var updateChannel: UpdateChannel = .stable
|
public var lastUpdateCheckDate: Date?
|
||||||
|
public var updateChannel: UpdateChannel = .stable
|
||||||
|
|
||||||
private let updaterManager = SparkleUpdaterManager.shared
|
private let updaterManager = SparkleUpdaterManager.shared
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import Combine
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Logging
|
import Logging
|
||||||
|
|
||||||
|
|
@ -202,11 +201,13 @@ public final class TunnelWebSocketClient: NSObject, @unchecked Sendable {
|
||||||
private let apiKey: String
|
private let apiKey: String
|
||||||
private var sessionId: String?
|
private var sessionId: String?
|
||||||
private var webSocketTask: URLSessionWebSocketTask?
|
private var webSocketTask: URLSessionWebSocketTask?
|
||||||
private let messageSubject = PassthroughSubject<WSMessage, Never>()
|
private var messageContinuation: AsyncStream<WSMessage>.Continuation?
|
||||||
private let logger = Logger(label: "VibeTunnel.TunnelWebSocketClient")
|
private let logger = Logger(label: "VibeTunnel.TunnelWebSocketClient")
|
||||||
|
|
||||||
public var messages: AnyPublisher<WSMessage, Never> {
|
public var messages: AsyncStream<WSMessage> {
|
||||||
messageSubject.eraseToAnyPublisher()
|
AsyncStream { continuation in
|
||||||
|
self.messageContinuation = continuation
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(url: URL, apiKey: String, sessionId: String? = nil) {
|
public init(url: URL, apiKey: String, sessionId: String? = nil) {
|
||||||
|
|
@ -258,6 +259,7 @@ public final class TunnelWebSocketClient: NSObject, @unchecked Sendable {
|
||||||
|
|
||||||
public func disconnect() {
|
public func disconnect() {
|
||||||
webSocketTask?.cancel(with: .goingAway, reason: nil)
|
webSocketTask?.cancel(with: .goingAway, reason: nil)
|
||||||
|
messageContinuation?.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func receiveMessage() {
|
private func receiveMessage() {
|
||||||
|
|
@ -269,11 +271,11 @@ public final class TunnelWebSocketClient: NSObject, @unchecked Sendable {
|
||||||
if let data = text.data(using: .utf8),
|
if let data = text.data(using: .utf8),
|
||||||
let wsMessage = try? JSONDecoder().decode(WSMessage.self, from: data)
|
let wsMessage = try? JSONDecoder().decode(WSMessage.self, from: data)
|
||||||
{
|
{
|
||||||
self?.messageSubject.send(wsMessage)
|
self?.messageContinuation?.yield(wsMessage)
|
||||||
}
|
}
|
||||||
case .data(let data):
|
case .data(let data):
|
||||||
if let wsMessage = try? JSONDecoder().decode(WSMessage.self, from: data) {
|
if let wsMessage = try? JSONDecoder().decode(WSMessage.self, from: data) {
|
||||||
self?.messageSubject.send(wsMessage)
|
self?.messageContinuation?.yield(wsMessage)
|
||||||
}
|
}
|
||||||
@unknown default:
|
@unknown default:
|
||||||
break
|
break
|
||||||
|
|
@ -307,7 +309,7 @@ extension TunnelWebSocketClient: URLSessionWebSocketDelegate {
|
||||||
reason: Data?
|
reason: Data?
|
||||||
) {
|
) {
|
||||||
logger.info("WebSocket disconnected with code: \(closeCode)")
|
logger.info("WebSocket disconnected with code: \(closeCode)")
|
||||||
messageSubject.send(completion: .finished)
|
messageContinuation?.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ enum ServerError: LocalizedError {
|
||||||
/// HTTP server implementation for the macOS app
|
/// HTTP server implementation for the macOS app
|
||||||
@MainActor
|
@MainActor
|
||||||
@Observable
|
@Observable
|
||||||
public final class TunnelServerDemo {
|
public final class TunnelServer {
|
||||||
public private(set) var isRunning = false
|
public private(set) var isRunning = false
|
||||||
public private(set) var port: Int
|
public private(set) var port: Int
|
||||||
public var lastError: Error?
|
public var lastError: Error?
|
||||||
|
|
@ -39,7 +39,7 @@ public final class TunnelServerDemo {
|
||||||
public func start() async throws {
|
public func start() async throws {
|
||||||
guard !isRunning else { return }
|
guard !isRunning else { return }
|
||||||
|
|
||||||
logger.info("Starting TunnelServerDemo on port \(port)")
|
logger.info("Starting TunnelServer on port \(port)")
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let router = Router(context: BasicRequestContext.self)
|
let router = Router(context: BasicRequestContext.self)
|
||||||
|
|
@ -1,139 +0,0 @@
|
||||||
import Combine
|
|
||||||
import Foundation
|
|
||||||
import Logging
|
|
||||||
|
|
||||||
/// Demo code showing how to use the VibeTunnel server
|
|
||||||
enum TunnelServerExample {
|
|
||||||
private static let logger = Logger(label: "VibeTunnel.TunnelServerDemo")
|
|
||||||
|
|
||||||
static func runDemo() async {
|
|
||||||
// Get the API key (in production, this should be managed securely)
|
|
||||||
// For demo purposes, using a hardcoded key
|
|
||||||
let apiKey = "demo-api-key-12345"
|
|
||||||
|
|
||||||
logger.info("Using API key: [REDACTED]")
|
|
||||||
|
|
||||||
// Create client
|
|
||||||
let client = TunnelClient(apiKey: apiKey)
|
|
||||||
|
|
||||||
do {
|
|
||||||
// Check server health
|
|
||||||
let isHealthy = try await client.checkHealth()
|
|
||||||
logger.info("Server healthy: \(isHealthy)")
|
|
||||||
|
|
||||||
// Create a new session
|
|
||||||
let session = try await client.createSession(
|
|
||||||
workingDirectory: "/tmp",
|
|
||||||
shell: "/bin/zsh"
|
|
||||||
)
|
|
||||||
logger.info("Created session: \(session.sessionId)")
|
|
||||||
|
|
||||||
// Execute a command
|
|
||||||
let response = try await client.executeCommand(
|
|
||||||
sessionId: session.sessionId,
|
|
||||||
command: "echo 'Hello from VibeTunnel!'"
|
|
||||||
)
|
|
||||||
logger.info("Command output: \(response.output ?? "none")")
|
|
||||||
|
|
||||||
// List all sessions
|
|
||||||
let sessions = try await client.listSessions()
|
|
||||||
logger.info("Active sessions: \(sessions.count)")
|
|
||||||
|
|
||||||
// Close the session
|
|
||||||
try await client.closeSession(id: session.sessionId)
|
|
||||||
logger.info("Session closed")
|
|
||||||
} catch {
|
|
||||||
logger.error("Demo error: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static func runWebSocketDemo() async {
|
|
||||||
// For demo purposes, using a hardcoded key
|
|
||||||
let apiKey = "demo-api-key-12345"
|
|
||||||
|
|
||||||
let client = TunnelClient(apiKey: apiKey)
|
|
||||||
|
|
||||||
do {
|
|
||||||
// Create a session first
|
|
||||||
let session = try await client.createSession()
|
|
||||||
logger.info("Created session for WebSocket: \(session.sessionId)")
|
|
||||||
|
|
||||||
// Connect WebSocket
|
|
||||||
guard let wsClient = client.connectWebSocket(sessionId: session.sessionId) else {
|
|
||||||
logger.error("Failed to create WebSocket client")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
wsClient.connect()
|
|
||||||
|
|
||||||
// Subscribe to messages
|
|
||||||
let cancellable = wsClient.messages.sink { message in
|
|
||||||
switch message.type {
|
|
||||||
case .output:
|
|
||||||
logger.info("Output: \(message.data ?? "")")
|
|
||||||
case .error:
|
|
||||||
logger.error("Error: \(message.data ?? "")")
|
|
||||||
default:
|
|
||||||
logger.info("Message: \(message.type) - \(message.data ?? "")")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send some commands
|
|
||||||
try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
|
|
||||||
wsClient.sendCommand("pwd")
|
|
||||||
|
|
||||||
try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
|
|
||||||
wsClient.sendCommand("ls -la")
|
|
||||||
|
|
||||||
try await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds
|
|
||||||
|
|
||||||
// Disconnect
|
|
||||||
wsClient.disconnect()
|
|
||||||
cancellable.cancel()
|
|
||||||
} catch {
|
|
||||||
logger.error("WebSocket demo error: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - cURL Examples
|
|
||||||
|
|
||||||
// Here are some example cURL commands to test the server:
|
|
||||||
//
|
|
||||||
// # Set your API key
|
|
||||||
// export API_KEY="your-api-key-here"
|
|
||||||
//
|
|
||||||
// # Health check (no auth required)
|
|
||||||
// curl http://localhost:8080/health
|
|
||||||
//
|
|
||||||
// # Get server info
|
|
||||||
// curl -H "X-API-Key: $API_KEY" http://localhost:8080/info
|
|
||||||
//
|
|
||||||
// # Create a new session
|
|
||||||
// curl -X POST http://localhost:8080/sessions \
|
|
||||||
// -H "X-API-Key: $API_KEY" \
|
|
||||||
// -H "Content-Type: application/json" \
|
|
||||||
// -d '{
|
|
||||||
// "workingDirectory": "/tmp",
|
|
||||||
// "shell": "/bin/zsh"
|
|
||||||
// }'
|
|
||||||
//
|
|
||||||
// # List all sessions
|
|
||||||
// curl -H "X-API-Key: $API_KEY" http://localhost:8080/sessions
|
|
||||||
//
|
|
||||||
// # Execute a command
|
|
||||||
// curl -X POST http://localhost:8080/execute \
|
|
||||||
// -H "X-API-Key: $API_KEY" \
|
|
||||||
// -H "Content-Type: application/json" \
|
|
||||||
// -d '{
|
|
||||||
// "sessionId": "your-session-id",
|
|
||||||
// "command": "ls -la"
|
|
||||||
// }'
|
|
||||||
//
|
|
||||||
// # Get session info
|
|
||||||
// curl -H "X-API-Key: $API_KEY" http://localhost:8080/sessions/your-session-id
|
|
||||||
//
|
|
||||||
// # Close a session
|
|
||||||
// curl -X DELETE -H "X-API-Key: $API_KEY" http://localhost:8080/sessions/your-session-id
|
|
||||||
//
|
|
||||||
// # WebSocket connection (using websocat tool)
|
|
||||||
// websocat -H "X-API-Key: $API_KEY" ws://localhost:8080/ws/terminal
|
|
||||||
Binary file not shown.
|
|
@ -31,6 +31,15 @@ extension Notification.Name {
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@State private var selectedTab: SettingsTab = .general
|
@State private var selectedTab: SettingsTab = .general
|
||||||
|
@State private var contentSize: CGSize = .zero
|
||||||
|
|
||||||
|
// Define ideal sizes for each tab
|
||||||
|
private let tabSizes: [SettingsTab: CGSize] = [
|
||||||
|
.general: CGSize(width: 500, height: 300),
|
||||||
|
.advanced: CGSize(width: 500, height: 400),
|
||||||
|
.debug: CGSize(width: 600, height: 650),
|
||||||
|
.about: CGSize(width: 500, height: 550)
|
||||||
|
]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView(selection: $selectedTab) {
|
TabView(selection: $selectedTab) {
|
||||||
|
|
@ -58,11 +67,19 @@ struct SettingsView: View {
|
||||||
}
|
}
|
||||||
.tag(SettingsTab.about)
|
.tag(SettingsTab.about)
|
||||||
}
|
}
|
||||||
|
.frame(width: contentSize.width, height: contentSize.height)
|
||||||
|
.animatedWindowResizing(size: contentSize)
|
||||||
.onReceive(NotificationCenter.default.publisher(for: .openSettingsTab)) { notification in
|
.onReceive(NotificationCenter.default.publisher(for: .openSettingsTab)) { notification in
|
||||||
if let tab = notification.object as? SettingsTab {
|
if let tab = notification.object as? SettingsTab {
|
||||||
selectedTab = tab
|
selectedTab = tab
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: selectedTab) { _, newTab in
|
||||||
|
contentSize = tabSizes[newTab] ?? CGSize(width: 500, height: 400)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
contentSize = tabSizes[selectedTab] ?? CGSize(width: 500, height: 400)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -280,7 +297,7 @@ struct AdvancedSettingsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and start new server with the new port
|
// Create and start new server with the new port
|
||||||
let newServer = TunnelServerDemo(port: port)
|
let newServer = TunnelServer(port: port)
|
||||||
appDelegate.setHTTPServer(newServer)
|
appDelegate.setHTTPServer(newServer)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
|
@ -307,7 +324,7 @@ struct AdvancedSettingsView: View {
|
||||||
|
|
||||||
|
|
||||||
struct DebugSettingsView: View {
|
struct DebugSettingsView: View {
|
||||||
@State private var httpServer: TunnelServerDemo?
|
@State private var httpServer: TunnelServer?
|
||||||
@State private var lastError: String?
|
@State private var lastError: String?
|
||||||
@State private var testResult: String?
|
@State private var testResult: String?
|
||||||
@State private var isTesting = false
|
@State private var isTesting = false
|
||||||
|
|
@ -531,7 +548,7 @@ struct DebugSettingsView: View {
|
||||||
if shouldStart {
|
if shouldStart {
|
||||||
// Create a new server if needed
|
// Create a new server if needed
|
||||||
if httpServer == nil {
|
if httpServer == nil {
|
||||||
let newServer = TunnelServerDemo(port: serverPort)
|
let newServer = TunnelServer(port: serverPort)
|
||||||
httpServer = newServer
|
httpServer = newServer
|
||||||
// Store reference in AppDelegate
|
// Store reference in AppDelegate
|
||||||
if let appDelegate = NSApp.delegate as? AppDelegate {
|
if let appDelegate = NSApp.delegate as? AppDelegate {
|
||||||
|
|
|
||||||
71
VibeTunnel/Utilities/SettingsWindowDelegate.swift
Normal file
71
VibeTunnel/Utilities/SettingsWindowDelegate.swift
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import AppKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// A window delegate that handles animated resizing of the settings window
|
||||||
|
class SettingsWindowDelegate: NSObject, NSWindowDelegate {
|
||||||
|
static let shared = SettingsWindowDelegate()
|
||||||
|
|
||||||
|
private override init() {
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Animates the window to a new size
|
||||||
|
func animateWindowResize(to newSize: CGSize, duration: TimeInterval = 0.3) {
|
||||||
|
guard let window = NSApp.windows.first(where: { $0.title.contains("Settings") || $0.title.contains("Preferences") }) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the new frame maintaining the window's top-left position
|
||||||
|
var newFrame = window.frame
|
||||||
|
let heightDifference = newSize.height - newFrame.height
|
||||||
|
newFrame.size = newSize
|
||||||
|
newFrame.origin.y -= heightDifference // Keep top edge in place
|
||||||
|
|
||||||
|
// Animate the frame change
|
||||||
|
NSAnimationContext.runAnimationGroup { context in
|
||||||
|
context.duration = duration
|
||||||
|
context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||||
|
window.animator().setFrame(newFrame, display: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func windowWillClose(_ notification: Notification) {
|
||||||
|
// Clean up if needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A view modifier that sets up the window delegate for animated resizing
|
||||||
|
struct AnimatedWindowResizing: ViewModifier {
|
||||||
|
let size: CGSize
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
.onAppear {
|
||||||
|
setupWindowDelegate()
|
||||||
|
// Initial resize without animation
|
||||||
|
SettingsWindowDelegate.shared.animateWindowResize(to: size, duration: 0)
|
||||||
|
}
|
||||||
|
.onChange(of: size) { _, newSize in
|
||||||
|
SettingsWindowDelegate.shared.animateWindowResize(to: newSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupWindowDelegate() {
|
||||||
|
Task { @MainActor in
|
||||||
|
// Small delay to ensure window is created
|
||||||
|
try? await Task.sleep(for: .milliseconds(100))
|
||||||
|
|
||||||
|
if let window = NSApp.windows.first(where: { $0.title.contains("Settings") || $0.title.contains("Preferences") }) {
|
||||||
|
window.delegate = SettingsWindowDelegate.shared
|
||||||
|
// Disable window resizing by user
|
||||||
|
window.styleMask.remove(.resizable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
func animatedWindowResizing(size: CGSize) -> some View {
|
||||||
|
modifier(AnimatedWindowResizing(size: size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,7 +12,6 @@ struct VibeTunnelApp: App {
|
||||||
Settings {
|
Settings {
|
||||||
SettingsView()
|
SettingsView()
|
||||||
}
|
}
|
||||||
.defaultSize(width: 500, height: 450)
|
|
||||||
.commands {
|
.commands {
|
||||||
CommandGroup(after: .appInfo) {
|
CommandGroup(after: .appInfo) {
|
||||||
Button("About VibeTunnel") {
|
Button("About VibeTunnel") {
|
||||||
|
|
@ -37,7 +36,7 @@ struct VibeTunnelApp: App {
|
||||||
@MainActor
|
@MainActor
|
||||||
final class AppDelegate: NSObject, NSApplicationDelegate {
|
final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
private(set) var sparkleUpdaterManager: SparkleUpdaterManager?
|
private(set) var sparkleUpdaterManager: SparkleUpdaterManager?
|
||||||
private(set) var httpServer: TunnelServerDemo?
|
private(set) var httpServer: TunnelServer?
|
||||||
private let sessionMonitor = SessionMonitor.shared
|
private let sessionMonitor = SessionMonitor.shared
|
||||||
|
|
||||||
/// Distributed notification name used to ask an existing instance to show the Settings window.
|
/// Distributed notification name used to ask an existing instance to show the Settings window.
|
||||||
|
|
@ -84,7 +83,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
// Initialize and start HTTP server
|
// Initialize and start HTTP server
|
||||||
let serverPort = UserDefaults.standard.integer(forKey: "serverPort")
|
let serverPort = UserDefaults.standard.integer(forKey: "serverPort")
|
||||||
httpServer = TunnelServerDemo(port: serverPort > 0 ? serverPort : 4020)
|
httpServer = TunnelServer(port: serverPort > 0 ? serverPort : 4020)
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
|
|
@ -117,7 +116,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setHTTPServer(_ server: TunnelServerDemo?) {
|
func setHTTPServer(_ server: TunnelServer?) {
|
||||||
httpServer = server
|
httpServer = server
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct MenuBarView: View {
|
struct MenuBarView: View {
|
||||||
@EnvironmentObject var sessionMonitor: SessionMonitor
|
@Environment(SessionMonitor.self) var sessionMonitor
|
||||||
@AppStorage("showInDock") private var showInDock = false
|
@AppStorage("showInDock") private var showInDock = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
@ -139,7 +139,7 @@ struct SessionRowView: View {
|
||||||
struct MenuButtonStyle: ButtonStyle {
|
struct MenuButtonStyle: ButtonStyle {
|
||||||
@State private var isHovered = false
|
@State private var isHovered = false
|
||||||
|
|
||||||
func makeBody(configuration: Configuration) -> some View {
|
func makeBody(configuration: ButtonStyle.Configuration) -> some View {
|
||||||
configuration.label
|
configuration.label
|
||||||
.font(.system(size: 13))
|
.font(.system(size: 13))
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
|
||||||
93
docs/modern-swift-refactoring.md
Normal file
93
docs/modern-swift-refactoring.md
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
# Modern Swift Refactoring Summary
|
||||||
|
|
||||||
|
This document summarizes the modernization changes made to the VibeTunnel codebase to align with modern Swift best practices as outlined in `modern-swift.md`.
|
||||||
|
|
||||||
|
## Key Changes
|
||||||
|
|
||||||
|
### 1. Converted @ObservableObject to @Observable
|
||||||
|
|
||||||
|
- **SessionMonitor.swift**: Converted from `@ObservableObject` with `@Published` properties to `@Observable` class
|
||||||
|
- Removed `import Combine`
|
||||||
|
- Replaced `@Published` with regular properties
|
||||||
|
- Changed from `ObservableObject` to `@Observable`
|
||||||
|
|
||||||
|
- **TunnelServerDemo.swift**: Converted from `@ObservableObject` to `@Observable`
|
||||||
|
- Removed `import Combine`
|
||||||
|
- Simplified property declarations
|
||||||
|
|
||||||
|
- **SparkleViewModel**: Converted stub implementation to use `@Observable`
|
||||||
|
|
||||||
|
### 2. Replaced Combine with Async/Await
|
||||||
|
|
||||||
|
- **SessionMonitor.swift**:
|
||||||
|
- Replaced `Timer` with `Task` for periodic monitoring
|
||||||
|
- Used `Task.sleep(for:)` instead of Timer callbacks
|
||||||
|
- Eliminated nested `Task { }` blocks
|
||||||
|
|
||||||
|
- **TunnelClient.swift**:
|
||||||
|
- Removed `PassthroughSubject` from WebSocket implementation
|
||||||
|
- Replaced with `AsyncStream<WSMessage>` for message handling
|
||||||
|
- Updated delegate methods to use `continuation.yield()` instead of `subject.send()`
|
||||||
|
|
||||||
|
### 3. Simplified State Management
|
||||||
|
|
||||||
|
- **VibeTunnelApp.swift**:
|
||||||
|
- Changed `@StateObject` to `@State` for SessionMonitor
|
||||||
|
- Updated `.environmentObject()` to `.environment()` for modern environment injection
|
||||||
|
|
||||||
|
- **MenuBarView.swift**:
|
||||||
|
- Changed `@EnvironmentObject` to `@Environment(SessionMonitor.self)`
|
||||||
|
|
||||||
|
- **SettingsView.swift**:
|
||||||
|
- Removed `ServerObserver` ViewModel completely
|
||||||
|
- Moved server state directly into view as `@State private var httpServer`
|
||||||
|
- Simplified server state access with computed properties
|
||||||
|
|
||||||
|
### 4. Modernized Async Operations
|
||||||
|
|
||||||
|
- **VibeTunnelApp.swift**:
|
||||||
|
- Replaced `DispatchQueue.main.asyncAfter` with `Task.sleep(for:)`
|
||||||
|
- Updated all `Task.sleep(nanoseconds:)` to `Task.sleep(for:)` with Duration
|
||||||
|
|
||||||
|
- **SettingsView.swift**:
|
||||||
|
- Replaced `.onAppear` with `.task` for async initialization
|
||||||
|
- Modernized Task.sleep usage throughout
|
||||||
|
|
||||||
|
### 5. Removed Unnecessary Abstractions
|
||||||
|
|
||||||
|
- Eliminated `ServerObserver` class - moved logic directly into views
|
||||||
|
- Removed Combine imports where no longer needed
|
||||||
|
- Simplified state ownership by keeping it close to where it's used
|
||||||
|
|
||||||
|
### 6. Updated Error Handling
|
||||||
|
|
||||||
|
- Maintained proper async/await error handling with try/catch
|
||||||
|
- Removed completion handler patterns where applicable
|
||||||
|
- Simplified error state management
|
||||||
|
|
||||||
|
## Benefits Achieved
|
||||||
|
|
||||||
|
1. **Reduced Dependencies**: Removed Combine dependency from most files
|
||||||
|
2. **Simpler Code**: Eliminated unnecessary ViewModels and abstractions
|
||||||
|
3. **Better Performance**: Native SwiftUI state management with @Observable
|
||||||
|
4. **Modern Patterns**: Consistent use of async/await throughout
|
||||||
|
5. **Cleaner Architecture**: State lives closer to where it's used
|
||||||
|
|
||||||
|
## Migration Notes
|
||||||
|
|
||||||
|
- All @Observable classes require iOS 17+ / macOS 14+
|
||||||
|
- AsyncStream provides a more natural API than Combine subjects
|
||||||
|
- Task-based monitoring is more efficient than Timer-based
|
||||||
|
- SwiftUI's built-in state management eliminates need for custom ViewModels
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
1. `/VibeTunnel/Core/Services/SessionMonitor.swift`
|
||||||
|
2. `/VibeTunnel/Core/Services/TunnelClient.swift`
|
||||||
|
3. `/VibeTunnel/Core/Services/TunnelServerDemo.swift`
|
||||||
|
4. `/VibeTunnel/Core/Services/SparkleUpdaterManager.swift`
|
||||||
|
5. `/VibeTunnel/VibeTunnelApp.swift`
|
||||||
|
6. `/VibeTunnel/Views/MenuBarView.swift`
|
||||||
|
7. `/VibeTunnel/SettingsView.swift`
|
||||||
|
|
||||||
|
All changes follow the principles outlined in the Modern Swift guidelines, embracing SwiftUI's native patterns and avoiding unnecessary complexity.
|
||||||
Loading…
Reference in a new issue