prole/prole-app/Sources/AppStatusView.swift
chrisfu 8a6e8738db 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
2025-12-01 21:08:30 -08:00

112 lines
4.4 KiB
Swift

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()
timestampLabel.stringValue = timeFormatter.string(from: now)
// SVC
lightSvc.state = c.svcReachable ? .green : .red
labelSvc.stringValue = "svc: \(cfg.svcHost):\(cfg.svcPort) \(c.svcReachable ? "✓" : "✗") \(c.svcLatency >= 0 ? "(\(c.svcLatency) ms)" : "")"
labelSvc.toolTip = c.tooltipForSvc()
// K3s aggregate: retropie + pi
let aggCount = (c.raspberryReachable ? 1 : 0) + (c.piReachable ? 1 : 0)
lightK3s.state = aggCount == 2 ? .green : (aggCount == 1 ? .yellow : .red)
labelK3s.stringValue = "k3s: \(cfg.retropieHost) \(c.raspberryReachable ? "✓" : "✗"), \(cfg.piHost) \(c.piReachable ? "✓" : "✗")"
labelK3s.toolTip = c.tooltipForAggregateK3s()
// Local
lightLocal.state = c.localReachable ? .green : .red
labelLocal.stringValue = "local: \(cfg.localHost):\(cfg.localPort) \(c.localReachable ? "✓" : "✗") \(c.localLatency >= 0 ? "(\(c.localLatency) ms)" : "")"
labelLocal.toolTip = c.tooltipForLocal()
needsLayout = true
}
}