import AppKit import RoyalVNCKit // A secondary window that hosts the Prole Workstation (VNC viewer) // Version 1: viewer-only (no interactive session wiring yet) final class WorkstationWindowController: NSWindowController { private let viewer = VNCViewerHostView() init() { let style: NSWindow.StyleMask = [.titled, .closable, .miniaturizable, .resizable] // Reasonable default for a VNC desktop view let initialRect = NSRect(x: 0, y: 0, width: 1024, height: 768) let window = NSWindow(contentRect: initialRect, styleMask: style, backing: .buffered, defer: false) super.init(window: window) window.isReleasedWhenClosed = false window.title = "Prole Workstation" window.center() window.level = .normal window.collectionBehavior = [.canJoinAllSpaces] window.appearance = NSAppearance(named: .aqua) let content = NSView() content.translatesAutoresizingMaskIntoConstraints = false window.contentView = content viewer.translatesAutoresizingMaskIntoConstraints = false content.addSubview(viewer) NSLayoutConstraint.activate([ viewer.topAnchor.constraint(equalTo: content.topAnchor), viewer.leadingAnchor.constraint(equalTo: content.leadingAnchor), viewer.trailingAnchor.constraint(equalTo: content.trailingAnchor), viewer.bottomAnchor.constraint(equalTo: content.bottomAnchor) ]) // Configure default connection (localhost:5901) viewer.connectDefault() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func show() { guard let window = window else { return } window.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: false) } func hide() { window?.orderOut(nil) } var isVisible: Bool { window?.isVisible ?? false } // Position this window to the right of a reference window with a small gap. func position(rightOf refWindow: NSWindow, gap: CGFloat = 12) { guard let this = window else { return } let refFrame = refWindow.frame var newFrame = this.frame newFrame.origin.x = refFrame.maxX + gap // Align tops if possible, else keep current y newFrame.origin.y = refFrame.origin.y + (refFrame.height - newFrame.height) this.setFrame(newFrame, display: true, animate: false) } } // Host view that embeds RoyalVNCKit viewer. final class VNCViewerHostView: NSView { private var framebufferView: VNCCAFramebufferView? private var connection: VNCConnection? // Keep a strong reference to the delegate; VNCConnection holds it weakly private var connectionDelegateStrongRef: VNCConnectionDelegate? override init(frame frameRect: NSRect) { super.init(frame: frameRect) wantsLayer = true layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor // The actual framebuffer view will be created when the connection creates the framebuffer } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func connectDefault() { connect(hostname: "localhost", port: 5901) } func connect(hostname: String, port: Int) { // Construct full settings according to current RoyalVNCKit API let settings = VNCConnection.Settings( isDebugLoggingEnabled: false, hostname: hostname, port: UInt16(port), isShared: true, isScalingEnabled: true, useDisplayLink: true, inputMode: .forwardKeyboardShortcutsIfNotInUseLocally, isClipboardRedirectionEnabled: true, colorDepth: .depth24Bit, frameEncodings: .default ) let conn = VNCConnection(settings: settings) self.connection = conn // Minimal delegate to attach framebuffer to a view class Delegate: VNCConnectionDelegate { weak var host: VNCViewerHostView? init(host: VNCViewerHostView) { self.host = host } func connection(_ connection: VNCConnection, stateDidChange connectionState: VNCConnection.ConnectionState) { // No-op for now; could update UI } func connection(_ connection: VNCConnection, credentialFor authenticationType: VNCAuthenticationType, completion: @escaping (VNCCredential?) -> Void) { // For now, no auth completion(nil) } func connection(_ connection: VNCConnection, didCreateFramebuffer framebuffer: VNCFramebuffer) { guard let host = host else { return } DispatchQueue.main.async { let fbView = VNCCAFramebufferView(frame: host.bounds, framebuffer: framebuffer, connection: connection) fbView.translatesAutoresizingMaskIntoConstraints = false host.subviews.forEach { $0.removeFromSuperview() } host.addSubview(fbView) NSLayoutConstraint.activate([ fbView.topAnchor.constraint(equalTo: host.topAnchor), fbView.leadingAnchor.constraint(equalTo: host.leadingAnchor), fbView.trailingAnchor.constraint(equalTo: host.trailingAnchor), fbView.bottomAnchor.constraint(equalTo: host.bottomAnchor) ]) host.framebufferView = fbView } } func connection(_ connection: VNCConnection, didResizeFramebuffer framebuffer: VNCFramebuffer) { // VNCCAFramebufferView queries connection/framebuffer for size; nothing required here } func connection(_ connection: VNCConnection, didUpdateFramebuffer framebuffer: VNCFramebuffer, x: UInt16, y: UInt16, width: UInt16, height: UInt16) { // View should handle drawing updates internally } func connection(_ connection: VNCConnection, didUpdateCursor cursor: VNCCursor) { // Cursor updates handled by the framebuffer view } } let delegate = Delegate(host: self) self.connectionDelegateStrongRef = delegate conn.delegate = delegate conn.connect() } }