prole/prole-app/Sources/WorkstationWindowController.swift
chrisfu 197c72dd7a Refactor prole-app and establish temporary release process
- Moved prole-tools-app to prole-app at the project root to make it self-contained for transition to its own repository.
- Created prole-tools-app/dist/ directory to host build artifacts.
- Generated distribution artifacts (Prole Tools.app and Prole Tools.zip) using prole-app/build.sh package.
- Checked in the generated artifacts to prole-tools-app/dist/ (bypassing .gitignore for temporary release process).
Changes Summary
•
Renamed directory prole-tools-app/ to prole-app/.
•
Populated prole-tools-app/dist/ with the latest build output from prole-app/build.sh.
•
Staged all changes, including the forced addition of ignored artifacts in prole-tools-app/dist/.
2026-01-18 15:04:23 -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()
}
}