mirror of
https://github.com/dredx/prole.git
synced 2026-09-27 14:14:31 +00:00
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:
parent
325d180e9e
commit
6fa949cabc
@ -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
|
||||
@ -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)
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user