prole/prole-app/Sources/WorkstationWindowController.swift
chrisfu 64455fa886 ### Milestone: Remove IRCKit, migrate to swift-nio IRC, enable RoyalVNC via SwiftPM, and fix runtime embedding
- Replaced all IRCKit (0.16) usage with NozeIO `swift-nio-irc-client` (SwiftPM).
- Enabled IRC window with new client: connects and joins `#prole` (plain TCP for now; SSL checkbox displays a notice).
- Enabled RoyalVNC and removed all optional/flag-based handling. Integrated RoyalVNC via SwiftPM (`royalvnc` on `main`).
- Updated VNC UI to current RoyalVNC API: `VNCConnection` + `VNCCAFramebufferView` with a strong delegate reference.
- Simplified build to use SwiftPM for both IRC and RoyalVNC.
- Fixed runtime embedding and loader issues:
  - Copy SwiftPM-built `.dylib` products (e.g., `libRoyalVNCKit.dylib`) into `Contents/Frameworks` and codesign them.
  - Added `@executable_path/../Frameworks` to app rpaths with `install_name_tool`.
- Removed manual RoyalVNCKit `.xcframework` building/embedding and any IRCKit traces.

#### Key changes
- `prole-app/build.sh`:
  - Build app via SwiftPM; removed IRCKit and manual RoyalVNC build logic.
  - Embed SwiftPM `.dylib` outputs into `Contents/Frameworks` and set app rpath.
  - Cleaned usage text; dependencies now handled by SwiftPM.
- `prole-app/Package.swift`:
  - Add `swift-nio-irc-client` (NozeIO) dependency.
  - Add RoyalVNC via SwiftPM: `https://github.com/royalapplications/royalvnc` on `main`.
- `prole-app/Sources/IRCWindowController.swift`:
  - Migrate to NozeIO `IRC` package API; re-enable Connect; join `#prole`.
  - Import AppKit; provide convenience init for transcript view.
- `prole-app/Sources/WorkstationWindowController.swift`:
  - Import `RoyalVNCKit`; use `VNCConnection` + `VNCCAFramebufferView`.
  - Implement `VNCConnectionDelegate` and keep a strong reference to the delegate.
- `prole-app/debug.sh`:
  - Removed IRCKit logs section; kept RoyalVNC logs earlier; then removed manual RoyalVNC altogether.

This build now launches successfully (no dyld errors), opens the Workstation window (RoyalVNC), and the IRC window connects via the new client.

### Suggested follow-ups
- If TLS is required for IRC, add `NIOSSL` integration and wire SSL checkbox to TLS connection.
- Optionally remove leftover Vendor references if any local cache remains.
2025-12-05 23:37:54 -08:00

151 lines
6.2 KiB
Swift

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()
}
}