import AppKit // MainWindowController builds the regular window you can move/resize. // It embeds an AppStatusView (3 simple rows, no scrolling) and a tiny // control bar in the bottom‑left with: // - ⟳ Refresh (forces a status check) // - _ Minimize to Status Bar (switches to overlay mode) final class MainWindowController: NSWindowController { struct Actions { let onRefresh: () -> Void let onMinimizeToStatusBar: () -> Void } private let statusView = StatusView() private let detailsLabel: NSTextField = { let tf = NSTextField(labelWithString: "") tf.font = NSFont.monospacedSystemFont(ofSize: NSFont.systemFontSize(for: .small), weight: .regular) tf.textColor = .secondaryLabelColor tf.alignment = .left tf.lineBreakMode = .byWordWrapping tf.cell?.wraps = true tf.translatesAutoresizingMaskIntoConstraints = false return tf }() private var pfManager: PortForwardManager? private let actions: Actions init(serviceChecker: ServiceChecker, actions: Actions, pfManager: PortForwardManager? = nil) { // Initialize stored properties before calling super.init self.actions = actions let style: NSWindow.StyleMask = [.titled, .closable, .miniaturizable, .resizable] let initialRect = NSRect(x: 0, y: 0, width: 720, height: 420) let window = NSWindow(contentRect: initialRect, styleMask: style, backing: .buffered, defer: false) super.init(window: window) window.isReleasedWhenClosed = false window.title = "Prole Status — ⌘⌥⇧P to toggle" window.level = .normal window.collectionBehavior = [.canJoinAllSpaces] window.appearance = NSAppearance(named: .aqua) // A plain NSView acts as the content container let content = NSView() content.translatesAutoresizingMaskIntoConstraints = false window.contentView = content // Header label to avoid title bar overlap and give more context let headerLabel: NSTextField = { let tf = NSTextField(labelWithString: "Prole — System Status") tf.font = NSFont.systemFont(ofSize: 14, weight: .semibold) tf.lineBreakMode = .byWordWrapping tf.cell?.wraps = true tf.translatesAutoresizingMaskIntoConstraints = false return tf }() statusView.translatesAutoresizingMaskIntoConstraints = false content.addSubview(headerLabel) content.addSubview(statusView) content.addSubview(detailsLabel) // Bottom-left control bar with Refresh and Minimize buttons let controls = NSStackView() controls.orientation = .horizontal controls.spacing = 8 controls.alignment = .centerY controls.translatesAutoresizingMaskIntoConstraints = false content.addSubview(controls) let refreshButton = NSButton(title: "⟳", target: nil, action: nil) refreshButton.bezelStyle = .texturedRounded refreshButton.toolTip = "Refresh Now" refreshButton.target = self refreshButton.action = #selector(didTapRefresh) let minimizeButton = NSButton(title: "_", target: nil, action: nil) minimizeButton.bezelStyle = .texturedRounded minimizeButton.toolTip = "Minimize to Status Bar" minimizeButton.target = self minimizeButton.action = #selector(didTapMinimize) controls.addArrangedSubview(refreshButton) controls.addArrangedSubview(minimizeButton) // NSWindow.contentLayoutGuide is typed as Any? on AppKit; cast safely to NSLayoutGuide. // Fallback to the content view's anchors if unavailable. let layoutGuide = window.contentLayoutGuide as Any? let guide = (layoutGuide as? NSLayoutGuide) let topAnchorRef = guide?.topAnchor ?? content.topAnchor let leadingAnchorRef = guide?.leadingAnchor ?? content.leadingAnchor let trailingAnchorRef = guide?.trailingAnchor ?? content.trailingAnchor let bottomAnchorRef = guide?.bottomAnchor ?? content.bottomAnchor NSLayoutConstraint.activate([ // Header at the top inside the content layout guide (avoids title bar) headerLabel.topAnchor.constraint(equalTo: topAnchorRef, constant: 12), headerLabel.leadingAnchor.constraint(equalTo: leadingAnchorRef, constant: 12), headerLabel.trailingAnchor.constraint(equalTo: trailingAnchorRef, constant: -12), // StatusView beneath the header, full width statusView.topAnchor.constraint(equalTo: headerLabel.bottomAnchor, constant: 8), statusView.leadingAnchor.constraint(equalTo: leadingAnchorRef, constant: 8), statusView.trailingAnchor.constraint(equalTo: trailingAnchorRef, constant: -8), // Details label below statusView detailsLabel.topAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 12), detailsLabel.leadingAnchor.constraint(equalTo: leadingAnchorRef, constant: 8), detailsLabel.trailingAnchor.constraint(equalTo: trailingAnchorRef, constant: -8), // Controls pinned bottom-left inside safe area controls.leadingAnchor.constraint(equalTo: leadingAnchorRef, constant: 8), controls.bottomAnchor.constraint(equalTo: bottomAnchorRef, constant: -8) ]) statusView.bindTo(serviceChecker: serviceChecker) self.pfManager = pfManager statusView.setPortForwardManager(pfManager) if let m = pfManager { NotificationCenter.default.addObserver(self, selector: #selector(updatePFDetails), name: PortForwardManager.statusDidChangeNotification, object: m) } updatePFDetails() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func show() { guard let window = window else { return } MainWindowController.positionWindowAtLeftEdge(window) window.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: false) } func hide() { window?.orderOut(nil) } var isVisible: Bool { window?.isVisible ?? false } // MARK: - Positioning helpers static func positionWindowAtLeftEdge(_ window: NSWindow, margin: CGFloat = 12) { guard let screen = window.screen ?? NSScreen.main else { return } let vis = screen.visibleFrame var frame = window.frame // Place near left edge and below the menu bar, keep current size frame.origin.x = vis.origin.x + margin // Align top to visible frame top with small margin frame.origin.y = vis.maxY - frame.height - margin // Ensure not offscreen vertically if frame.origin.y < vis.origin.y + margin { frame.origin.y = vis.origin.y + margin } window.setFrame(frame, display: true, animate: false) } // MARK: - Actions @objc private func didTapRefresh() { actions.onRefresh() } @objc private func didTapMinimize() { actions.onMinimizeToStatusBar() } // MARK: - Port Forward Details @objc private func updatePFDetails() { guard let m = pfManager else { detailsLabel.stringValue = ""; return } if m.details.isEmpty { if Config.shared.pfEnabled { detailsLabel.stringValue = "Port Forwards: enabled but no commands loaded (check prole.properties path)" } else { detailsLabel.stringValue = "Port Forwards: none configured" } return } var lines: [String] = ["Port Forwards:"] let df = DateFormatter(); df.dateStyle = .short; df.timeStyle = .medium for d in m.details { let pid = d.pid != nil ? String(d.pid!) : "-" let status = d.running ? "running" : "stopped" var extra = "" if !d.running { if let code = d.lastExitCode, let when = d.lastExitAt { extra = " (exit=\(code) at \(df.string(from: when)))" } else if let code = d.lastExitCode { extra = " (exit=\(code))" } } lines.append(" • PID \(pid) • \(status)\(extra) — \(d.command)") } detailsLabel.stringValue = lines.joined(separator: "\n") } }