mirror of
https://github.com/dredx/prole.git
synced 2026-09-24 15:44:33 +00:00
ProleStatus: app-window default, light splash restored, status bar controls; endpoint config via properties; build + docs
- Default to Application Window mode with full app menu; status bar overlay remains available - Restore clickable startup Splash Tip (5s) with animated GIF and live counter; use light theme (Aqua) - Status bar item: monospaced "P" icon; left click shows overlay (status mode) or main window (app mode) - Overlay: add Maximize button (□) to toggle back to main window; keep click‑through elsewhere - Global hotkey Cmd+Opt+Shift+P toggles modes; Prole menu item mirrors the same toggle and updates title dynamically - Application menu (Prole): Show Status Bar / Show Main Window (contextual), Refresh Now, Quit - Application Window layout: non‑scrolling, vertical 1‑line rows (svc, k3s aggregate, local) with top‑right timestamp; bottom‑left controls (⟳ Refresh, _ Minimize) - Parameterize endpoints via `prole.properties` (bundled + user override). Replace legacy raspberry with retropie defaults - ServiceChecker + UI read endpoints from Config loader; tooltips/labels reflect configured hosts/ports - Build script: generate `Info.plist` with `LSUIElement=false`; bundle resources (`prole-type.gif`, `prole.properties`); ad‑hoc codesign. Universal build supported - Documentation: rewrite README with technical build/run/config details and operational posture Files: - proleStatus/Sources/: AppDelegate.swift, OverlayWindow.swift, StatusView.swift, StatusItemController.swift, SplashTipWindowController.swift, MainWindowController.swift, AppStatusView.swift, ServiceChecker.swift, Config.swift - proleStatus/build.sh - proleStatus/prole.properties - proleStatus/README.md Notes: - Build verified via `./build.sh build` (arm64). App starts in App Window mode with light theme; menu + hotkey + overlay toggle operate as intended
This commit is contained in:
parent
da8bca97f6
commit
0c69f90e71
@ -6,7 +6,11 @@ UI-agnostic command composition for the Prole macOS app build.
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, scrolledtext
|
||||
|
||||
from . import main as inst_main
|
||||
from . import screen as ui
|
||||
|
||||
def get_build_command(project_root: Path, env: str | None) -> str:
|
||||
"""Return the Prole app build command with an env comment suffix.
|
||||
@ -17,3 +21,26 @@ def get_build_command(project_root: Path, env: str | None) -> str:
|
||||
env_label = (env or "Dev").strip().title()
|
||||
prole_root = str(project_root)
|
||||
return f"cd \"{prole_root}\" && ./prole-app/build.sh build # {env_label}"
|
||||
|
||||
|
||||
# ---------------- Screen (UI) helpers ----------------
|
||||
def create_build_page(app):
|
||||
"""Create the Build page UI and register it via installer.main."""
|
||||
f = ttk.Frame(app.page_area)
|
||||
f.place(x=0, y=0, relwidth=1, relheight=1)
|
||||
|
||||
ttk.Label(f, text='Build', style='Title.TLabel').pack(anchor='w', padx=24, pady=(24, 6))
|
||||
ttk.Label(f, text='Choose a target and build the artifacts.', style='Body.TLabel').pack(anchor='w', padx=24)
|
||||
|
||||
wrap = ttk.Frame(f)
|
||||
wrap.pack(anchor='w', padx=24, pady=12)
|
||||
ttk.Label(wrap, text='Target Environment:', style='Body.TLabel').pack(side='left')
|
||||
app.deploy_env_var = tk.StringVar(value='Dev')
|
||||
ttk.Combobox(wrap, textvariable=app.deploy_env_var, values=['Dev', 'Service', 'Prod'], state='readonly', width=18).pack(side='left', padx=10)
|
||||
|
||||
# Build output console
|
||||
app.build_output = scrolledtext.ScrolledText(f, height=12, bg='#fafafa', fg='#1d1d1f')
|
||||
app.build_output.pack(fill='both', expand=True, padx=24, pady=12)
|
||||
|
||||
inst_main.register_page(app, 'build', f)
|
||||
return f
|
||||
|
||||
@ -8,7 +8,11 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import subprocess
|
||||
from tkinter import messagebox
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
|
||||
from . import main as inst_main
|
||||
from . import screen as ui
|
||||
|
||||
|
||||
def check_xcode_tools() -> bool:
|
||||
@ -55,3 +59,59 @@ def build_prole_app(project_root: Path) -> None:
|
||||
messagebox.showinfo("Copied", f"Copied to {dest}")
|
||||
except Exception as copy_err:
|
||||
print(f"Copy to /Applications failed: {copy_err}")
|
||||
|
||||
|
||||
# ---------------- Screen (UI) helpers ----------------
|
||||
def _draw_status(canvas: tk.Canvas, status: str):
|
||||
canvas.delete('all')
|
||||
if status == 'success' or status == 'completed':
|
||||
canvas.create_oval(2, 2, 18, 18, fill='#34c759', outline='')
|
||||
canvas.create_line(5, 10, 9, 14, fill='white', width=2)
|
||||
canvas.create_line(9, 14, 16, 6, fill='white', width=2)
|
||||
elif status == 'running':
|
||||
canvas.create_oval(2, 2, 18, 18, fill='#ffd60a', outline='')
|
||||
elif status == 'error':
|
||||
canvas.create_oval(2, 2, 18, 18, fill='#ff3b30', outline='')
|
||||
else:
|
||||
canvas.create_oval(2, 2, 18, 18, outline='#b0b0b0')
|
||||
|
||||
|
||||
def _create_deploy_row(app, parent, step: dict):
|
||||
row = ttk.Frame(parent)
|
||||
row.pack(fill='x', pady=6)
|
||||
canvas = tk.Canvas(row, width=20, height=20, highlightthickness=0)
|
||||
canvas.pack(side='left', padx=8)
|
||||
lbl = ttk.Label(row, text=step['name'], style='Body.TLabel')
|
||||
lbl.pack(side='left')
|
||||
status = ttk.Label(row, text='Pending', style='Dim.TLabel')
|
||||
status.pack(side='right', padx=8)
|
||||
app.deploy_widgets[step['name']] = {'canvas': canvas, 'label': status, 'step': step}
|
||||
_draw_status(canvas, 'pending')
|
||||
|
||||
|
||||
def create_deploy_page(app):
|
||||
"""Create the Deploy page UI and register it via installer.main."""
|
||||
f = ttk.Frame(app.page_area)
|
||||
f.place(x=0, y=0, relwidth=1, relheight=1)
|
||||
|
||||
ttk.Label(f, text='Deploy', style='Title.TLabel').pack(anchor='w', padx=24, pady=(24, 6))
|
||||
ttk.Label(f, text='Preparing to deploy. We will verify steps and perform actions as needed.', style='Body.TLabel', wraplength=800).pack(anchor='w', padx=24)
|
||||
|
||||
app.deploy_steps = [
|
||||
{'name': 'Build Prole macOS app', 'status': 'pending'},
|
||||
{'name': 'Check Docker is running', 'status': 'pending'},
|
||||
{'name': 'Check or configure container registry', 'status': 'pending'},
|
||||
{'name': 'Ensure target cluster', 'status': 'pending'},
|
||||
{'name': 'Build prole-db Docker image', 'status': 'pending'},
|
||||
{'name': 'Tag Docker image for registry', 'status': 'pending'},
|
||||
{'name': 'Push image to registry', 'status': 'pending'},
|
||||
{'name': 'Import image to k3d cluster (Dev only)', 'status': 'pending'},
|
||||
]
|
||||
app.deploy_widgets = {}
|
||||
container = ttk.Frame(f)
|
||||
container.pack(fill='both', expand=True, padx=16, pady=8)
|
||||
for step in app.deploy_steps:
|
||||
_create_deploy_row(app, container, step)
|
||||
|
||||
inst_main.register_page(app, 'deploy', f)
|
||||
return f
|
||||
|
||||
@ -19,6 +19,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
private var statusItemController: StatusItemController!
|
||||
private var splashTipController: SplashTipWindowController?
|
||||
private var appMenuToggleStatusBarItem: NSMenuItem?
|
||||
private var pfManager: PortForwardManager?
|
||||
|
||||
// We keep the app in one of two simple modes.
|
||||
// - statusBar: shows the thin overlay near the macOS menu bar
|
||||
@ -42,8 +43,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
// ServiceChecker does the lightweight TCP checks on a background timer
|
||||
serviceChecker = ServiceChecker()
|
||||
dlog("ServiceChecker created")
|
||||
// Optional: Port-forward supervision (Dev)
|
||||
if Config.shared.pfEnabled {
|
||||
let cmds = Config.shared.pfCommands
|
||||
if !cmds.isEmpty {
|
||||
let mgr = PortForwardManager(commands: cmds)
|
||||
self.pfManager = mgr
|
||||
mgr.start()
|
||||
}
|
||||
}
|
||||
|
||||
// Overlay (Status Bar mode) — a thin, mostly click‑through window near the top
|
||||
overlayWindowController = OverlayWindowController(serviceChecker: serviceChecker)
|
||||
overlayWindowController = OverlayWindowController(serviceChecker: serviceChecker, pfManager: pfManager)
|
||||
dlog("OverlayWindowController created; computing initial frame/visibility")
|
||||
overlayWindowController.showIfMenuBarVisible()
|
||||
|
||||
@ -51,7 +62,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
mainWindowController = MainWindowController(serviceChecker: serviceChecker, actions: .init(
|
||||
onRefresh: { [weak self] in self?.refreshNow() },
|
||||
onMinimizeToStatusBar: { [weak self] in self?.switchToStatusBarMode() }
|
||||
))
|
||||
), pfManager: pfManager)
|
||||
dlog("MainWindowController created")
|
||||
|
||||
// Create status bar item with icon and click handler
|
||||
@ -194,6 +205,32 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
appMenu.addItem(NSMenuItem.separator())
|
||||
|
||||
// Reset Port Forwards (Dev)
|
||||
if pfManager != nil {
|
||||
let resetPF = NSMenuItem(title: "Reset Port Forwards", action: #selector(menuResetPortForwards), keyEquivalent: "")
|
||||
resetPF.target = self
|
||||
appMenu.addItem(resetPF)
|
||||
|
||||
// Details submenu with PID/state
|
||||
let detailsItem = NSMenuItem(title: "Port Forward Details", action: nil, keyEquivalent: "")
|
||||
let sub = NSMenu(title: "Port Forward Details")
|
||||
if let mgr = pfManager {
|
||||
for d in mgr.details {
|
||||
let pid = d.pid != nil ? String(d.pid!) : "-"
|
||||
let status = d.running ? "running" : "stopped"
|
||||
let label = "PID \(pid) • \(status) — \(d.command)"
|
||||
let it = NSMenuItem(title: label, action: nil, keyEquivalent: "")
|
||||
sub.addItem(it)
|
||||
}
|
||||
if mgr.details.isEmpty {
|
||||
sub.addItem(NSMenuItem(title: "No port-forwards configured", action: nil, keyEquivalent: ""))
|
||||
}
|
||||
}
|
||||
detailsItem.submenu = sub
|
||||
appMenu.addItem(detailsItem)
|
||||
appMenu.addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
// Refresh
|
||||
let refresh = NSMenuItem(title: "Refresh Now", action: #selector(menuRefresh), keyEquivalent: "r")
|
||||
refresh.target = self
|
||||
@ -202,7 +239,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
appMenu.addItem(NSMenuItem.separator())
|
||||
|
||||
// Quit
|
||||
let quit = NSMenuItem(title: "Quit ProleStatus", action: #selector(NSApp.terminate(_:)), keyEquivalent: "q")
|
||||
let quit = NSMenuItem(title: "Quit Prole", action: #selector(NSApp.terminate(_:)), keyEquivalent: "q")
|
||||
appMenu.addItem(quit)
|
||||
|
||||
appMenuItem.submenu = appMenu
|
||||
@ -219,6 +256,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
@objc private func menuToggleMode() { toggleMode() }
|
||||
@objc private func menuRefresh() { refreshNow() }
|
||||
@objc private func menuResetPortForwards() { pfManager?.reset() }
|
||||
|
||||
// Invoked by overlay maximize button
|
||||
@objc private func showMainWindowRequested() {
|
||||
|
||||
@ -1,90 +1,3 @@
|
||||
import AppKit
|
||||
|
||||
// Application-window specific status view: vertical, one line per item, no marquee.
|
||||
final class AppStatusView: NSView {
|
||||
private let lightSvc = TrafficLight()
|
||||
private let lightK3s = TrafficLight()
|
||||
private let lightLocal = TrafficLight()
|
||||
|
||||
private let labelSvc = NSTextField(labelWithString: "")
|
||||
private let labelK3s = NSTextField(labelWithString: "")
|
||||
private let labelLocal = NSTextField(labelWithString: "")
|
||||
|
||||
// Top-right timestamp
|
||||
private let timestampLabel: NSTextField = {
|
||||
let tf = NSTextField(labelWithString: "0000-00-00 00:00:00 Z")
|
||||
tf.font = NSFont.monospacedDigitSystemFont(ofSize: NSFont.systemFontSize(for: .small), weight: .regular)
|
||||
tf.textColor = .secondaryLabelColor
|
||||
tf.alignment = .right
|
||||
tf.translatesAutoresizingMaskIntoConstraints = false
|
||||
return tf
|
||||
}()
|
||||
|
||||
private var checker: ServiceChecker?
|
||||
private let timeFormatter: DateFormatter = {
|
||||
let df = DateFormatter()
|
||||
df.locale = .current
|
||||
df.timeZone = .current
|
||||
df.dateFormat = "yyyy-MM-dd HH:mm:ss ZZZZZ"
|
||||
return df
|
||||
}()
|
||||
|
||||
override init(frame frameRect: NSRect) {
|
||||
super.init(frame: frameRect)
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// Configure labels
|
||||
[labelSvc, labelK3s, labelLocal].forEach { l in
|
||||
l.font = NSFont.systemFont(ofSize: NSFont.systemFontSize(for: .regular))
|
||||
l.textColor = .labelColor
|
||||
l.lineBreakMode = .byTruncatingTail
|
||||
l.translatesAutoresizingMaskIntoConstraints = false
|
||||
}
|
||||
|
||||
// Rows: [light][label]
|
||||
func row(light: NSView, label: NSTextField) -> NSStackView {
|
||||
let s = NSStackView(views: [light, label])
|
||||
s.orientation = .horizontal
|
||||
s.alignment = .centerY
|
||||
s.spacing = 8
|
||||
s.translatesAutoresizingMaskIntoConstraints = false
|
||||
return s
|
||||
}
|
||||
|
||||
let row1 = row(light: lightSvc, label: labelSvc)
|
||||
let row2 = row(light: lightK3s, label: labelK3s)
|
||||
let row3 = row(light: lightLocal, label: labelLocal)
|
||||
|
||||
let vstack = NSStackView(views: [row1, row2, row3])
|
||||
vstack.orientation = .vertical
|
||||
vstack.alignment = .leading
|
||||
vstack.spacing = 10
|
||||
vstack.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
addSubview(vstack)
|
||||
addSubview(timestampLabel)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
vstack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
|
||||
vstack.topAnchor.constraint(equalTo: topAnchor, constant: 24),
|
||||
|
||||
timestampLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
|
||||
timestampLabel.topAnchor.constraint(equalTo: topAnchor, constant: 10),
|
||||
timestampLabel.leadingAnchor.constraint(greaterThanOrEqualTo: vstack.trailingAnchor, constant: 12)
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
func bindTo(serviceChecker: ServiceChecker) {
|
||||
self.checker = serviceChecker
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(updateUI), name: ServiceChecker.statusDidChangeNotification, object: serviceChecker)
|
||||
updateUI()
|
||||
}
|
||||
|
||||
@objc private func updateUI() {
|
||||
guard let c = checker else { return }
|
||||
let cfg = Config.shared
|
||||
|
||||
// Timestamp
|
||||
let now = Date()
|
||||
@ -106,6 +19,60 @@ final class AppStatusView: NSView {
|
||||
labelLocal.stringValue = "local: \(cfg.localHost):\(cfg.localPort) \(c.localReachable ? "✓" : "✗") \(c.localLatency >= 0 ? "(\(c.localLatency) ms)" : "")"
|
||||
labelLocal.toolTip = c.tooltipForLocal()
|
||||
|
||||
// Port forwards
|
||||
if let m = pfManager {
|
||||
let running = m.runningCount
|
||||
let total = m.details.count
|
||||
lightPF.state = m.allRunning ? .green : .red
|
||||
labelPF.stringValue = total > 0 ? "kubectl port-forwards: \(running)/\(total) running" : "kubectl port-forwards: none configured"
|
||||
labelPF.toolTip = "Managed processes: \(running)/\(total) running"
|
||||
renderPFDetails(m)
|
||||
} else {
|
||||
lightPF.state = .red
|
||||
labelPF.stringValue = "kubectl port-forwards: disabled"
|
||||
labelPF.toolTip = "To enable, set pf.enabled=true in prole.properties"
|
||||
clearPFDetails()
|
||||
}
|
||||
|
||||
needsLayout = true
|
||||
}
|
||||
|
||||
// MARK: - PF Details rendering
|
||||
private func clearPFDetails() {
|
||||
pfDetailsStack.arrangedSubviews.forEach { v in
|
||||
pfDetailsStack.removeArrangedSubview(v)
|
||||
v.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
private func renderPFDetails(_ manager: PortForwardManager) {
|
||||
clearPFDetails()
|
||||
let details = manager.details
|
||||
guard !details.isEmpty else { return }
|
||||
|
||||
for d in details {
|
||||
let dot = TrafficLight()
|
||||
dot.state = d.running ? .green : .red
|
||||
|
||||
let pidText: String
|
||||
if let pid = d.pid, pid > 0 {
|
||||
pidText = "PID \(pid)"
|
||||
} else {
|
||||
pidText = "PID -"
|
||||
}
|
||||
|
||||
let label = NSTextField(labelWithString: "\(pidText) • \(d.running ? "running" : "stopped") — \(d.command)")
|
||||
label.font = NSFont.systemFont(ofSize: NSFont.systemFontSize(for: .small))
|
||||
label.textColor = .secondaryLabelColor
|
||||
label.lineBreakMode = .byTruncatingMiddle
|
||||
|
||||
let row = NSStackView(views: [dot, label])
|
||||
row.orientation = .horizontal
|
||||
row.alignment = .centerY
|
||||
row.spacing = 8
|
||||
row.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
pfDetailsStack.addArrangedSubview(row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,4 +78,26 @@ final class Config {
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
// Dev port-forward supervision
|
||||
var pfEnabled: Bool {
|
||||
let v = (props["pf.enabled"] ?? "").lowercased()
|
||||
return v == "1" || v == "true" || v == "yes" || v == "on"
|
||||
}
|
||||
|
||||
var pfCommands: [String] {
|
||||
// Collect keys pf.1, pf.2, ... in ascending order
|
||||
let keys = props.keys
|
||||
.filter { $0.hasPrefix("pf.") }
|
||||
.compactMap { k -> (Int, String)? in
|
||||
let tail = k.dropFirst(3)
|
||||
if let idx = Int(tail) { return (idx, k) }
|
||||
return nil
|
||||
}
|
||||
.sorted { $0.0 < $1.0 }
|
||||
.map { $0.1 }
|
||||
return keys.compactMap { props[$0] }
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ final class MainWindowController: NSWindowController {
|
||||
private let statusView = AppStatusView()
|
||||
private let actions: Actions
|
||||
|
||||
init(serviceChecker: ServiceChecker, 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]
|
||||
@ -71,6 +71,7 @@ final class MainWindowController: NSWindowController {
|
||||
])
|
||||
|
||||
statusView.bindTo(serviceChecker: serviceChecker)
|
||||
statusView.setPortForwardManager(pfManager)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
@ -2,9 +2,10 @@ import AppKit
|
||||
|
||||
final class OverlayWindowController: NSWindowController {
|
||||
private let statusView = StatusView()
|
||||
private var pfManager: PortForwardManager?
|
||||
// Fully transparent background; no visual effect overlay
|
||||
|
||||
init(serviceChecker: ServiceChecker) {
|
||||
init(serviceChecker: ServiceChecker, pfManager: PortForwardManager? = nil) {
|
||||
let style: NSWindow.StyleMask = [.borderless]
|
||||
let window = OverlayWindow(contentRect: .zero, styleMask: style, backing: .buffered, defer: false)
|
||||
super.init(window: window)
|
||||
@ -32,6 +33,8 @@ final class OverlayWindowController: NSWindowController {
|
||||
])
|
||||
|
||||
statusView.bindTo(serviceChecker: serviceChecker)
|
||||
self.pfManager = pfManager
|
||||
statusView.setPortForwardManager(pfManager)
|
||||
|
||||
// Initial placement
|
||||
recomputeFrameAndVisibility()
|
||||
@ -99,12 +102,31 @@ final class OverlayWindowController: NSWindowController {
|
||||
dlog("Enabling mouse events for context menu interaction")
|
||||
window.ignoresMouseEvents = false
|
||||
}
|
||||
let menu = NSMenu(title: "ProleStatus")
|
||||
let menu = NSMenu(title: "Prole")
|
||||
menu.addItem(withTitle: statusView.aggregateStatusSummary(), action: nil, keyEquivalent: "")
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(withTitle: "Refresh Now", action: #selector(refreshNow), keyEquivalent: "r").target = self
|
||||
if let mgr = pfManager {
|
||||
menu.addItem(withTitle: "Reset Port Forwards", action: #selector(resetPortForwards), keyEquivalent: "").target = self
|
||||
|
||||
// Details submenu
|
||||
let detailsItem = NSMenuItem(title: "Port Forward Details", action: nil, keyEquivalent: "")
|
||||
let sub = NSMenu(title: "Port Forward Details")
|
||||
for d in mgr.details {
|
||||
let pid = d.pid != nil ? String(d.pid!) : "-"
|
||||
let status = d.running ? "running" : "stopped"
|
||||
let label = "PID \(pid) • \(status) — \(d.command)"
|
||||
let it = NSMenuItem(title: label, action: nil, keyEquivalent: "")
|
||||
sub.addItem(it)
|
||||
}
|
||||
if mgr.details.isEmpty {
|
||||
sub.addItem(NSMenuItem(title: "No port-forwards configured", action: nil, keyEquivalent: ""))
|
||||
}
|
||||
detailsItem.submenu = sub
|
||||
menu.addItem(detailsItem)
|
||||
}
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(withTitle: "Quit ProleStatus", action: #selector(quit), keyEquivalent: "q").target = self
|
||||
menu.addItem(withTitle: "Quit Prole", action: #selector(quit), keyEquivalent: "q").target = self
|
||||
|
||||
let point = NSPoint(x: window.frame.midX, y: window.frame.minY)
|
||||
menu.popUp(positioning: nil, at: point, in: nil)
|
||||
@ -123,6 +145,10 @@ final class OverlayWindowController: NSWindowController {
|
||||
dlog("Quit requested via context menu; terminating app")
|
||||
NSApp.terminate(nil)
|
||||
}
|
||||
|
||||
@objc private func resetPortForwards() {
|
||||
pfManager?.reset()
|
||||
}
|
||||
}
|
||||
|
||||
final class OverlayWindow: NSWindow {
|
||||
|
||||
@ -4,6 +4,7 @@ final class StatusView: NSView {
|
||||
private let light1 = TrafficLight()
|
||||
private let light2 = TrafficLight()
|
||||
private let light3 = TrafficLight()
|
||||
private let lightPF = TrafficLight()
|
||||
private let timestampLabel: NSTextField = {
|
||||
let tf = NSTextField(labelWithString: "0000-00-00 00:00:00 +00:00")
|
||||
tf.textColor = .secondaryLabelColor
|
||||
@ -46,7 +47,7 @@ final class StatusView: NSView {
|
||||
wantsLayer = true
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let stack = NSStackView(views: [light1, light2, light3, timestampLabel, marquee, maximizeButton])
|
||||
let stack = NSStackView(views: [light1, light2, light3, lightPF, timestampLabel, marquee, maximizeButton])
|
||||
stack.orientation = .horizontal
|
||||
stack.alignment = .centerY
|
||||
stack.distribution = .equalSpacing
|
||||
@ -73,18 +74,32 @@ final class StatusView: NSView {
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
override func hitTest(_ point: NSPoint) -> NSView? {
|
||||
// Allow clicks only on our buttons; pass everything else through
|
||||
let v = super.hitTest(point)
|
||||
if let v = v as? NSButton { return v }
|
||||
// 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 }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private var pfManager: PortForwardManager?
|
||||
|
||||
func bindTo(serviceChecker: ServiceChecker) {
|
||||
self.checker = serviceChecker
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(updateLights), name: ServiceChecker.statusDidChangeNotification, object: serviceChecker)
|
||||
updateLights()
|
||||
}
|
||||
|
||||
func setPortForwardManager(_ manager: PortForwardManager?) {
|
||||
self.pfManager = manager
|
||||
if let m = manager {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(updateLights), name: PortForwardManager.statusDidChangeNotification, object: m)
|
||||
}
|
||||
updateLights()
|
||||
}
|
||||
|
||||
@objc private func updateLights() {
|
||||
guard let c = checker else { return }
|
||||
// Light 1: svc.prole.org:443
|
||||
@ -100,6 +115,15 @@ final class StatusView: NSView {
|
||||
light3.state = c.localReachable ? .green : .red
|
||||
light3.toolTip = c.tooltipForLocal()
|
||||
|
||||
// Light 4: port-forward supervision status (Dev)
|
||||
if let m = pfManager {
|
||||
lightPF.state = m.allRunning ? .green : .red
|
||||
lightPF.toolTip = m.allRunning ? "kubectl port-forwards: running" : "kubectl port-forwards: not all running"
|
||||
} else {
|
||||
lightPF.state = .red
|
||||
lightPF.toolTip = "kubectl port-forwards: disabled"
|
||||
}
|
||||
|
||||
// Timestamp label
|
||||
let now = Date()
|
||||
timestampLabel.stringValue = timeFormatter.string(from: now)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user