refactor(app, scripts): remove deprecated init-port-forwards.sh; enhance UI and db management

- Deleted `init-port-forwards.sh`, removing deprecated placeholder script.
- Introduced `dbStatus` command in `PFScriptBridge.swift` for querying database status with `kubecolor`.
- Improved `StatusView` UI: refined button interactivity and font standardization.
- Added `DBStatusWindowController` to `AppDelegate` for better database status visibility and control.
- Updated `OverlayWindow`: adjusted window levels and interaction behavior for smoother UX.
This commit is contained in:
chrisfu 2025-12-22 21:27:00 -08:00
parent 325d180e9e
commit 6fa949cabc
6 changed files with 119 additions and 65 deletions

View File

@ -1,17 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# DEPRECATED: this script has been renamed to etc/init_port_forwards.sh
# Keep as a thin wrapper for backward compatibility.
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
NEW_SCRIPT="$SCRIPT_DIR/init_port_forwards.sh"
echo "[init-port-forwards.sh] WARNING: This script is deprecated. Please use etc/init_port_forwards.sh instead." >&2
if [ -x "$NEW_SCRIPT" ]; then
exec "$NEW_SCRIPT" "$@"
else
echo "[init-port-forwards.sh] ERROR: $NEW_SCRIPT not found or not executable." >&2
exit 127
fi

View File

@ -14,6 +14,7 @@ import AppKit
final class AppDelegate: NSObject, NSApplicationDelegate {
private var overlayWindowController: OverlayWindowController!
private var mainWindowController: MainWindowController!
private var dbStatusWindowController: DBStatusWindowController!
private var workstationWindowController: WorkstationWindowController!
private var ircWindowController: IRCWindowController!
private var hotKeyManager: HotKeyManager!
@ -64,6 +65,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
), pfManager: nil)
dlog("MainWindowController created")
// Database status window — positioned slightly down and left of main window
dbStatusWindowController = DBStatusWindowController(actions: .init(
onMinimizeToStatusBar: { [weak self] in self?.switchToStatusBarMode() }
))
dlog("DBStatusWindowController created")
// Workstation window (VNC viewer) — shown to the right of Prole Status when not in status bar mode
workstationWindowController = WorkstationWindowController()
dlog("WorkstationWindowController created")
@ -152,6 +159,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
private func switchToStatusBarMode() {
mode = .statusBar
mainWindowController.hide()
dbStatusWindowController.hide()
workstationWindowController.hide()
ircWindowController.hide()
overlayWindowController.showOverlayNow()
@ -167,6 +175,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
mode = .appWindow
overlayWindowController.hideOverlay()
mainWindowController.show()
// Position database status window slightly down and to the left of the main window
if let ref = mainWindowController.window { dbStatusWindowController.position(relativeTo: ref) }
dbStatusWindowController.show()
// Position workstation window to the right of the main window and show it
if let ref = mainWindowController.window { workstationWindowController.position(rightOf: ref) }
workstationWindowController.show()
@ -194,14 +205,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
private func toggleAppWindowVisibility() {
if mainWindowController.isVisible {
mainWindowController.hide()
dbStatusWindowController.hide()
workstationWindowController.hide()
ircWindowController.hide()
if mode == .appWindow { mode = .statusBar }
} else {
mainWindowController.show()
if let ref = mainWindowController.window { workstationWindowController.position(rightOf: ref) }
if let ref = mainWindowController.window {
dbStatusWindowController.position(relativeTo: ref)
workstationWindowController.position(rightOf: ref)
ircWindowController.position(below: ref)
}
dbStatusWindowController.show()
workstationWindowController.show()
if let ref = mainWindowController.window { ircWindowController.position(below: ref) }
ircWindowController.show()
mode = .appWindow
overlayWindowController.hideOverlay()
@ -297,6 +313,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
// Invoked by overlay maximize button
@objc private func showMainWindowRequested() {
dlog("AppDelegate: showMainWindowRequested notification received")
switchToAppWindowMode()
// Maximize (zoom) the window to ensure it's fully visible/readable
mainWindowController.window?.zoom(nil)

View File

@ -1,12 +1,7 @@
import Foundation
enum Debug {
static let isEnabled: Bool = {
if let v = ProcessInfo.processInfo.environment["PROLESTATUS_DEBUG"] {
return v == "1" || v.lowercased() == "true" || v.lowercased() == "yes"
}
return false
}()
static let isEnabled: Bool = true
static func log(_ message: @autoclosure () -> String) {
guard isEnabled else { return }

View File

@ -9,8 +9,8 @@ final class OverlayWindowController: NSWindowController {
let window = OverlayWindow(contentRect: .zero, styleMask: style, backing: .buffered, defer: false)
super.init(window: window)
window.isReleasedWhenClosed = false
window.level = .floating // below status bar level, above normal windows
window.collectionBehavior = [.canJoinAllSpaces, .stationary]
window.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()) + 1)
window.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle]
window.hasShadow = false
window.backgroundColor = .clear
window.isOpaque = false
@ -53,8 +53,7 @@ final class OverlayWindowController: NSWindowController {
let rect = NSRect(x: frame.minX, y: y, width: frame.width, height: overlayHeight)
window?.setFrame(rect, display: true)
window?.orderFrontRegardless()
// Allow mouse for our inline controls (refresh/maximize). The view will pass-through elsewhere.
window?.ignoresMouseEvents = false
window?.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()) + 1)
dlog("Overlay window forced visible at top of visible frame")
}
@ -86,19 +85,12 @@ final class OverlayWindowController: NSWindowController {
dlog("Computed overlay frame: x=\(rect.origin.x), y=\(rect.origin.y), w=\(rect.size.width), h=\(rect.size.height) | menuBarHeight=\(menuBarHeight), statusBarThickness=\(statusThickness), overlayHeight=\(overlayHeight)")
window?.setFrame(rect, display: true)
window?.orderFrontRegardless()
// Enable mouse so the inline buttons work; StatusView forwards other clicks through.
window?.ignoresMouseEvents = false
window?.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()) + 1)
dlog("Overlay window ordered front")
}
func showContextMenu() {
guard let window = window, window.isVisible else { return }
// Temporarily enable mouse interactions so the context menu can be used
let wasIgnoring = window.ignoresMouseEvents
if wasIgnoring {
dlog("Enabling mouse events for context menu interaction")
window.ignoresMouseEvents = false
}
let menu = NSMenu(title: "Prole Tools")
menu.addItem(withTitle: statusView.aggregateStatusSummary(), action: nil, keyEquivalent: "")
menu.addItem(.separator())
@ -109,11 +101,6 @@ final class OverlayWindowController: NSWindowController {
let point = NSPoint(x: window.frame.midX, y: window.frame.minY)
menu.popUp(positioning: nil, at: point, in: nil)
// Restore click-through after the menu is dismissed
if wasIgnoring {
dlog("Restoring click-through after context menu")
window.ignoresMouseEvents = true
}
}
@objc private func refreshNow() {
@ -131,17 +118,49 @@ final class OverlayWindowController: NSWindowController {
}
final class OverlayWindow: NSWindow {
override var canBecomeKey: Bool { false }
override var canBecomeMain: Bool { false }
override var canBecomeKey: Bool { return false }
override var canBecomeMain: Bool { return false }
// Allow interaction with the window even if it's not key
override var acceptsMouseMovedEvents: Bool {
get { return true }
set { }
}
override func mouseDown(with event: NSEvent) {
dlog("OverlayWindow: mouseDown at \(event.locationInWindow)")
super.mouseDown(with: event)
}
override func sendEvent(_ event: NSEvent) {
if event.type == .leftMouseDown {
dlog("OverlayWindow: sendEvent .leftMouseDown at \(event.locationInWindow)")
// Fallback: Manually check if the click is in the maximize button area
if let contentView = self.contentView,
let statusView = contentView.subviews.first(where: { $0 is StatusView }) as? StatusView {
let pointInStatusView = statusView.convert(event.locationInWindow, from: nil)
if let hitView = statusView.hitTest(pointInStatusView), hitView.toolTip == "Show Main Window" {
dlog("OverlayWindow: Manual hit detected for maximize button via sendEvent")
NotificationCenter.default.post(name: NSNotification.Name("ProleStatus.showMainWindow"), object: nil)
return // Intercepted
}
}
}
super.sendEvent(event)
}
override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) {
super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag)
isOpaque = false
backgroundColor = .clear
// Default to fully click-through. We temporarily disable this only while showing the context menu.
ignoresMouseEvents = true
// Default to NOT click-through so we can intercept clicks on the maximize button.
// TransparentContainerView.hitTest handles passing through clicks for non-button areas.
ignoresMouseEvents = false
acceptsMouseMovedEvents = true
dlog("OverlayWindow initialized (borderless, transparent, accepts mouse moved events)")
isMovableByWindowBackground = false
// Ensure the window level is high and it doesn't ignore mouse events
level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()) + 1)
dlog("OverlayWindow initialized (borderless, transparent, level: \(level.rawValue))")
}
}
@ -149,13 +168,8 @@ final class OverlayWindow: NSWindow {
// while still allowing mouse-moved events for subviews that add tracking areas.
final class TransparentContainerView: NSView {
override func hitTest(_ point: NSPoint) -> NSView? {
// Allow interaction only with interactive subviews (e.g., buttons inside StatusView).
// Otherwise, pass clicks through.
if let hit = super.hitTest(point) {
if hit is NSButton { return hit }
// If a subview wants events but isn't a button, also allow it
if hit.acceptsFirstResponder { return hit }
}
return nil
let view = super.hitTest(point)
dlog("TransparentContainerView: hitTest at \(point) -> \(view?.description ?? "nil")")
return view
}
}

View File

@ -15,6 +15,39 @@ enum PFScriptBridge {
static func status() -> (code: Int32, out: String, err: String) { runScript(arg: "status") }
static func dbStatus() -> (code: Int32, out: String, err: String) {
runRawCommand(command: "kubecolor cnpg status prole-db", description: "kubecolor cnpg status prole-db")
}
private static func runRawCommand(command: String, description: String) -> (code: Int32, out: String, err: String) {
lastInvocation = description
let p = Process()
p.executableURL = URL(fileURLWithPath: "/bin/bash")
p.arguments = ["-lc", command]
// Use home as default CWD
let env = ProcessInfo.processInfo.environment
let home = env["HOME"] ?? NSHomeDirectory()
let proleHomeEnv = env["PROLE_HOME"]
p.currentDirectoryURL = URL(fileURLWithPath: proleHomeEnv ?? home)
Logger.shared.info("invoking raw: \(description)")
let outPipe = Pipe(); let errPipe = Pipe()
p.standardOutput = outPipe
p.standardError = errPipe
do { try p.run() } catch {
let errStr = String(describing: error)
Logger.shared.error("spawn error: \(errStr)")
return (-1, "", errStr)
}
p.waitUntilExit()
let out = String(data: outPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
let err = String(data: errPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
let code = p.terminationStatus
return (code, out, err)
}
private static func runScript(arg: String) -> (code: Int32, out: String, err: String) {
let fm = FileManager.default
let env = ProcessInfo.processInfo.environment

View File

@ -8,7 +8,7 @@ final class StatusView: NSView {
let tf = NSTextField(labelWithString: "0000-00-00 00:00:00 +00:00")
tf.textColor = .secondaryLabelColor
tf.alignment = .left
tf.font = NSFont.monospacedDigitSystemFont(ofSize: NSFont.systemFontSize(for: .small), weight: .regular)
tf.font = NSFont.monospacedDigitSystemFont(ofSize: 10, weight: .regular)
tf.setContentHuggingPriority(.required, for: .horizontal)
tf.setContentCompressionResistancePriority(.required, for: .horizontal)
return tf
@ -17,7 +17,10 @@ final class StatusView: NSView {
private let maximizeButton: NSButton = {
let b = NSButton(title: "□", target: nil, action: nil)
b.bezelStyle = .texturedRounded
b.setButtonType(.momentaryPushIn)
b.toolTip = "Show Main Window"
b.isEnabled = true
b.refusesFirstResponder = true // Don't take focus away from other apps
b.setContentHuggingPriority(.required, for: .horizontal)
b.setContentCompressionResistancePriority(.required, for: .horizontal)
return b
@ -68,18 +71,26 @@ final class StatusView: NSView {
// Configure marquee width to be ~64 monospace characters
configureMarqueeWidth()
// Ensure button responds to mouse down immediately for better responsiveness in overlays
maximizeButton.sendAction(on: [.leftMouseDown, .leftMouseUp])
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func hitTest(_ point: NSPoint) -> NSView? {
// Allow interaction with our inline controls and status lights (for tooltips),
// pass other clicks through to avoid blocking the menu bar.
if let v = super.hitTest(point) {
if v is NSButton { return v }
if v is TrafficLight { return v }
if let tl = v.toolTip, !tl.isEmpty { return v }
// Allow interaction ONLY with the maximize button.
// Other subviews (like traffic lights) are no longer returned here to ensure they are click-through.
// Only the small area of the box click should be active.
// Convert point to maximizeButton's coordinate system
let pointInButton = convert(point, to: maximizeButton)
let hit = maximizeButton.bounds.contains(pointInButton)
dlog("StatusView: hitTest at \(point) (inButton: \(pointInButton)) -> \(hit ? "maximizeButton" : "nil") | buttonFrame=\(maximizeButton.frame) window=\(String(describing: window))")
if hit {
return maximizeButton
}
return nil
}
@ -144,6 +155,7 @@ final class StatusView: NSView {
// MARK: - Button actions
@objc private func didTapMaximize() {
dlog("StatusView: didTapMaximize button clicked")
// Explicitly request the main window to show (don’t toggle modes implicitly)
NotificationCenter.default.post(name: .showMainWindow, object: nil)
}
@ -172,7 +184,7 @@ final class MarqueeView: NSView {
// Configure labels
for lbl in [label1, label2] {
lbl.font = NSFont.monospacedSystemFont(ofSize: NSFont.systemFontSize(for: .small), weight: .regular)
lbl.font = NSFont.monospacedSystemFont(ofSize: 10, weight: .regular)
lbl.textColor = .secondaryLabelColor
lbl.alignment = .left
lbl.backgroundColor = .clear
@ -244,7 +256,7 @@ final class MarqueeView: NSView {
}
private func intrinsicLineHeight() -> CGFloat {
let f = label1.font ?? NSFont.monospacedSystemFont(ofSize: NSFont.systemFontSize(for: .small), weight: .regular)
let f = label1.font ?? NSFont.monospacedSystemFont(ofSize: 10, weight: .regular)
return ceil(f.ascender - f.descender)
}
@ -295,7 +307,7 @@ private func widthForMonospaceCharacters(_ count: Int, font: NSFont) -> CGFloat
private extension StatusView {
func configureMarqueeWidth() {
// Match font with timestamp for visual cohesion
let font = NSFont.monospacedSystemFont(ofSize: NSFont.systemFontSize(for: .small), weight: .regular)
let font = NSFont.monospacedSystemFont(ofSize: 10, weight: .regular)
marquee.heightAnchor.constraint(equalToConstant: ceil(font.capHeight * 1.8)).isActive = true
let width = widthForMonospaceCharacters(64, font: font)
let wConstraint = marquee.widthAnchor.constraint(equalToConstant: width)