import AppKit // DBStatusWindowController displays the output of "kubecolor cnpg status prole-db". // Uses identical style and behavior as MainWindowController. final class DBStatusWindowController: NSWindowController { struct Actions { let onMinimizeToStatusBar: () -> Void } private let outputTextView: NSTextView = { let tv = NSTextView() tv.isEditable = false tv.isSelectable = true tv.isRichText = false tv.usesFindBar = true tv.font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular) tv.textColor = .labelColor tv.backgroundColor = .textBackgroundColor tv.translatesAutoresizingMaskIntoConstraints = true tv.isVerticallyResizable = true tv.isHorizontallyResizable = false tv.minSize = NSSize(width: 0, height: 0) tv.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) tv.textContainerInset = NSSize(width: 6, height: 8) if let tc = tv.textContainer { tc.widthTracksTextView = true tc.containerSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) } let m = NSMenu(title: "Context") let copyItem = NSMenuItem(title: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c") copyItem.keyEquivalentModifierMask = [.command] copyItem.target = nil m.addItem(copyItem) m.addItem(NSMenuItem(title: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")) tv.menu = m return tv }() private let scrollView: NSScrollView = { let sv = NSScrollView() sv.hasVerticalScroller = true sv.hasHorizontalScroller = false sv.autohidesScrollers = true sv.translatesAutoresizingMaskIntoConstraints = false return sv }() private let intervalPopup: NSPopUpButton = { let p = NSPopUpButton(frame: .zero, pullsDown: false) p.translatesAutoresizingMaskIntoConstraints = false p.addItems(withTitles: ["10s", "30s", "1 min", "5 min"]) p.selectItem(withTitle: "30s") p.toolTip = "Auto-refresh interval" return p }() private var refreshTimer: Timer? private let actions: Actions init(actions: Actions) { self.actions = actions let style: NSWindow.StyleMask = [.titled, .closable, .miniaturizable, .resizable] let initialRect = NSRect(x: 0, y: 0, width: 965, height: 630) let window = NSWindow(contentRect: initialRect, styleMask: style, backing: .buffered, defer: false) super.init(window: window) window.isReleasedWhenClosed = false window.title = "Prole Database Status" window.level = .normal window.collectionBehavior = [.canJoinAllSpaces] window.appearance = NSAppearance(named: .aqua) window.delegate = self let content = NSView() content.translatesAutoresizingMaskIntoConstraints = false window.contentView = content let headerLabel: NSTextField = { let tf = NSTextField(labelWithString: "Prole — Database Status") tf.font = NSFont.systemFont(ofSize: 14, weight: .semibold) tf.translatesAutoresizingMaskIntoConstraints = false return tf }() content.addSubview(headerLabel) scrollView.borderType = .noBorder scrollView.drawsBackground = true scrollView.backgroundColor = .textBackgroundColor scrollView.documentView = outputTextView outputTextView.frame = scrollView.contentView.bounds outputTextView.autoresizingMask = [.width, .height] content.addSubview(scrollView) content.addSubview(intervalPopup) let controls = NSStackView() controls.orientation = .horizontal controls.spacing = 8 controls.alignment = .centerY controls.translatesAutoresizingMaskIntoConstraints = false content.addSubview(controls) let minimizeButton = NSButton(title: "_", target: self, action: #selector(didTapMinimize)) minimizeButton.bezelStyle = .texturedRounded minimizeButton.toolTip = "Minimize to Status Bar" controls.addArrangedSubview(minimizeButton) let guide = window.contentLayoutGuide 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([ headerLabel.topAnchor.constraint(equalTo: topAnchorRef, constant: 12), headerLabel.leadingAnchor.constraint(equalTo: leadingAnchorRef, constant: 12), headerLabel.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchorRef, constant: -12), intervalPopup.centerYAnchor.constraint(equalTo: headerLabel.centerYAnchor), intervalPopup.trailingAnchor.constraint(equalTo: trailingAnchorRef, constant: -12), scrollView.topAnchor.constraint(equalTo: headerLabel.bottomAnchor, constant: 8), scrollView.leadingAnchor.constraint(equalTo: leadingAnchorRef, constant: 8), scrollView.trailingAnchor.constraint(equalTo: trailingAnchorRef, constant: -8), scrollView.bottomAnchor.constraint(equalTo: bottomAnchorRef, constant: -44), controls.leadingAnchor.constraint(equalTo: leadingAnchorRef, constant: 8), controls.bottomAnchor.constraint(equalTo: bottomAnchorRef, constant: -8) ]) intervalPopup.target = self intervalPopup.action = #selector(didChangeInterval) runAndDisplay() resetTimer() DispatchQueue.main.async { [weak self] in self?.adjustFontToFit() } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func show() { window?.makeKeyAndOrderFront(nil) if let tv = outputTextView.window { tv.makeFirstResponder(outputTextView) } } func hide() { window?.orderOut(nil) } var isVisible: Bool { window?.isVisible ?? false } func position(relativeTo refWindow: NSWindow, offset: CGPoint = CGPoint(x: -20, y: -20)) { guard let this = window else { return } let refFrame = refWindow.frame var newFrame = this.frame newFrame.origin.x = refFrame.origin.x + offset.x newFrame.origin.y = refFrame.origin.y + offset.y this.setFrame(newFrame, display: true, animate: false) } @objc private func didTapMinimize() { actions.onMinimizeToStatusBar() } @objc private func didChangeInterval() { resetTimer() } private func selectedIntervalSeconds() -> TimeInterval { switch intervalPopup.titleOfSelectedItem { case "10s": return 10 case "30s": return 30 case "1 min": return 60 case "5 min": return 300 default: return 30 } } private func resetTimer() { refreshTimer?.invalidate() let interval = selectedIntervalSeconds() refreshTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in self?.runAndDisplay() } RunLoop.main.add(refreshTimer!, forMode: .common) } private func runAndDisplay() { DispatchQueue.main.async { [weak self] in self?.outputTextView.string = "Loading database status…" self?.outputTextView.scrollToBeginningOfDocument(nil) } DispatchQueue.global(qos: .utility).async { let res = PFScriptBridge.dbStatus() let now = ISO8601DateFormatter().string(from: Date()) var text = res.out + (res.err.isEmpty ? "" : (res.out.isEmpty ? res.err : "\n" + res.err)) if text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { text = "(no output received)" } if res.code != 0 { text = "[exit code: \(res.code)]\n" + text } let final = "# kubecolor cnpg status prole-db — status (\(now))\n\n" + text DispatchQueue.main.async { [weak self] in self?.outputTextView.string = final self?.outputTextView.scrollToBeginningOfDocument(nil) self?.adjustFontToFit() } } } } extension DBStatusWindowController: NSWindowDelegate { func windowDidResize(_ notification: Notification) { adjustFontToFit() } } private extension DBStatusWindowController { func adjustFontToFit() { guard let font = outputTextView.font, let tc = outputTextView.textContainer, let lm = outputTextView.layoutManager else { return } let availableHeight = scrollView.contentView.bounds.height guard availableHeight > 0 else { return } func contentHeight(for size: CGFloat) -> CGFloat { outputTextView.font = NSFont.monospacedSystemFont(ofSize: size, weight: .regular) lm.invalidateLayout(forCharacterRange: NSRange(location: 0, length: lm.numberOfGlyphs), actualCharacterRange: nil) lm.ensureLayout(for: tc) let used = lm.usedRect(for: tc) return used.height + outputTextView.textContainerInset.height * 2.0 } var minSize: CGFloat = 9 var maxSize: CGFloat = 12 let cap: CGFloat = 16 var best = maxSize var h = contentHeight(for: best) if h > availableHeight { var s = best while s > minSize { let next = max(minSize, s - 0.5) let nh = contentHeight(for: next) if nh <= availableHeight { best = next; break } s = next } } else { var s = best while s < cap { let next = min(cap, s + 0.5) let nh = contentHeight(for: next) if nh > availableHeight { break } best = next; s = next } } outputTextView.font = NSFont.monospacedSystemFont(ofSize: best, weight: .regular) outputTextView.scrollToBeginningOfDocument(nil) } }