mirror of
https://github.com/dredx/prole.git
synced 2026-09-28 22:14:31 +00:00
- 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
112 lines
4.4 KiB
Swift
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
|
|
}
|
|
}
|